First implementation of transformers. Seems to work, all old tests pass.

Uptime doesn't use flavor metric yet.

Transformers need testing.
This commit is contained in:
adriant 2014-03-12 16:55:47 +13:00
parent 0cf0680220
commit 9188c7a6cf
6 changed files with 168 additions and 278 deletions

View File

@ -1,154 +0,0 @@
import datetime
from constants import date_format, other_date_format
class Artifact(object):
"""
Provides base artifact controls; generic typing information
for the artifact structures.
"""
def __init__(self, resource, usage, start, end):
self.resource = resource
self.usage = usage # Raw meter data from Ceilometer
self.start = start
self.end = end
def __getitem__(self, item):
if item in self._data:
return self._data[item]
raise KeyError("no such item %s" % item)
def volume(self):
"""
Default billable number for this volume
"""
return sum([x["counter_volume"] for x in self.usage])
class Cumulative(Artifact):
def volume(self):
measurements = self.usage
measurements = sorted(measurements, key=lambda x: x["timestamp"])
count = 0
usage = 0
last_measure = None
for measure in measurements:
if last_measure is not None and (measure["counter_volume"] <
last_measure["counter_volume"]):
usage = usage + last_measure["counter_volume"]
count = count + 1
last_measure = measure
usage = usage + measurements[-1]["counter_volume"]
if count > 1:
total_usage = usage - measurements[0]["counter_volume"]
return total_usage
# Gauge and Delta have very little to do: They are expected only to
# exist as "not a cumulative" sort of artifact.
class Gauge(Artifact):
def volume(self):
"""
Default billable number for this volume
"""
# print "Usage is %s" % self.usage
usage = sorted(self.usage, key=lambda x: x["timestamp"])
blocks = []
curr = [usage[0]]
last = usage[0]
try:
last["timestamp"] = datetime.datetime.strptime(last["timestamp"],
date_format)
except ValueError:
last["timestamp"] = datetime.datetime.strptime(last["timestamp"],
other_date_format)
except TypeError:
pass
for val in usage[1:]:
try:
val["timestamp"] = datetime.datetime.strptime(val["timestamp"],
date_format)
except ValueError:
val["timestamp"] = datetime.datetime.strptime(val["timestamp"],
other_date_format)
except TypeError:
pass
difference = (val['timestamp'] - last["timestamp"])
if difference > datetime.timedelta(hours=1):
blocks.append(curr)
curr = [val]
last = val
else:
curr.append(val)
# this adds the last remaining values as a block of their own on exit
# might mean people are billed twice for an hour at times...
# but solves the issue of not billing if there isn't enough data for
# full hour.
blocks.append(curr)
# We are now sorted into 1-hour blocks
totals = []
for block in blocks:
usage = max([v["counter_volume"] for v in block])
totals.append(usage)
# totals = [max(x, key=lambda val: val["counter_volume"] ) for x in blocks]
# totals is now an array of max values per hour for a given month.
return sum(totals)
# This continues to be wrong.
def uptime(self, tracked):
"""Calculates uptime accurately for the given 'tracked' states.
- Will ignore all other states.
- Relies heavily on the existence of a state meter, and
should only ever be called on the state meter.
Returns: uptime in seconds"""
usage = sorted(self.usage, key=lambda x: x["timestamp"])
last = usage[0]
try:
last["timestamp"] = datetime.datetime.strptime(last["timestamp"],
date_format)
except ValueError:
last["timestamp"] = datetime.datetime.strptime(last["timestamp"],
other_date_format)
except TypeError:
pass
uptime = 0.0
for val in usage[1:]:
try:
val["timestamp"] = datetime.datetime.strptime(val["timestamp"],
date_format)
except ValueError:
val["timestamp"] = datetime.datetime.strptime(val["timestamp"],
other_date_format)
except TypeError:
pass
if val["counter_volume"] in tracked:
difference = val["timestamp"] - last["timestamp"]
uptime = uptime + difference.seconds
last = val
return uptime
class Delta(Artifact):
pass

View File

@ -30,13 +30,7 @@ class Database(object):
in a resource, for all the resources given""" in a resource, for all the resources given"""
for resource in usage: for resource in usage:
for key in resource.usage_strategies: for service, volume in resource.usage().iteritems():
strategy = resource.usage_strategies[key]
volume = resource.get(strategy['usage'])
try:
service = resource.get(strategy['service'])
except AttributeError:
service = strategy['service']
resource_id = resource.get("resource_id") resource_id = resource.get("resource_id")
tenant_id = resource.get("tenant_id") tenant_id = resource.get("tenant_id")

View File

@ -1,7 +1,5 @@
import requests import requests
import json import json
from collections import defaultdict
import artifact
import auth import auth
from ceilometerclient.v2.client import Client as ceilometer from ceilometerclient.v2.client import Client as ceilometer
from .models import resources from .models import resources
@ -275,7 +273,27 @@ class Meter(object):
self.conn = conn self.conn = conn
self.start = start self.start = start
self.end = end self.end = end
# self.meter = meter
self.measurements = self.get_meter(start, end,
self.conn.auth.auth_token)
self.type = set([a["counter_type"] for a in self.measurements])
if len(self.type) > 1:
# That's a big problem
raise RuntimeError("Too many types for measurement!")
elif len(self.type) == 0:
raise RuntimeError("No types!")
else:
self.type = self.type.pop()
self.name = set([a["counter_name"] for a in self.measurements])
if len(self.name) > 1:
# That's a big problem
raise RuntimeError("Too many names for measurement!")
elif len(self.name) == 0:
raise RuntimeError("No types!")
else:
self.name = self.name.pop()
def get_meter(self, start, end, auth): def get_meter(self, start, end, auth):
# Meter is a href; in this case, it has a set of fields with it already. # Meter is a href; in this case, it has a set of fields with it already.
@ -294,35 +312,5 @@ class Meter(object):
) )
return json.loads(r.text) return json.loads(r.text)
def volume(self): def usage(self):
return self.usage(self.start, self.end) return self.measurements
def usage(self, start, end):
"""
Usage condenses the entirety of a meter into a single datapoint:
A volume value that we can plot as a single number against previous
usage for a given range.
"""
measurements = self.get_meter(start, end, self.conn.auth.auth_token)
# return measurements
# print measurements
# self.measurements = defaultdict(list)
self.type = set([a["counter_type"] for a in measurements])
if len(self.type) > 1:
# That's a big problem
raise RuntimeError("Too many types for measurement!")
elif len(self.type) == 0:
raise RuntimeError("No types!")
else:
self.type = self.type.pop()
type_ = None
if self.type == "cumulative":
type_ = artifact.Cumulative
elif self.type == "gauge":
type_ = artifact.Gauge
elif self.type == "delta":
type_ = artifact.Delta
return type_(self.resource, measurements, start, end)

View File

@ -1,7 +1,4 @@
from decimal import Decimal from . import transformers
import math
from artifice import constants
class BaseModelConstruct(object): class BaseModelConstruct(object):
@ -34,37 +31,28 @@ class BaseModelConstruct(object):
# information based on a meter, I guess? # information based on a meter, I guess?
return getattr(self, name) return getattr(self, name)
def usage(self): def meters(self):
dct = {} dct = {}
for meter in self.relevant_meters: for meter in self.relevant_meters:
try: try:
vol = self._raw.meter(meter, self.start, self.end).volume() mtr = self._raw.meter(meter, self.start, self.end)
dct[meter] = vol dct[meter] = mtr
except AttributeError: except AttributeError:
# This is OK. We're not worried about non-existent meters, # This is OK. We're not worried about non-existent meters,
# I think. For now, anyway. # I think. For now, anyway.
pass pass
return dct return dct
def save(self): def usage(self):
for meter in self.relevant_meters: meters = self.meters()
try: usage = self.transformer.transform_usage(meters)
self._raw.meter(meter, self.start, self.end).save() return usage
except AttributeError:
# This is OK. We're not worried about non-existent meters,
# I think. For now, anyway.
pass
def to_mb(bytes):
# function to make code easier to understand elsewhere.
return (bytes / 1000) / 1000
class VM(BaseModelConstruct): class VM(BaseModelConstruct):
relevant_meters = ["state"] relevant_meters = ["state"]
usage_strategies = {"uptime": {"usage": "uptime", "service": "flavor"}} transformer = transformers.Uptime()
type = "virtual_machine" type = "virtual_machine"
@ -73,34 +61,6 @@ class VM(BaseModelConstruct):
return {"name": self.name, return {"name": self.name,
"type": self.type} "type": self.type}
@property
def uptime(self):
# this NEEDS to be moved to a config file or
# possibly be queried from Clerk?
tracked_states = [constants.active, constants.building,
constants.paused, constants.rescued,
constants.resized]
seconds = self.usage()["state"].uptime(tracked_states)
# in hours, rounded up:
uptime = math.ceil((seconds / 60.0) / 60.0)
return Decimal(uptime)
@property
def flavor(self):
# TODO FIgure out what the hell is going on with ceilometer here,
# and why flavor.name isn't always there, and why
# sometimes instance_type is needed instead....
try:
# print "\"flavor.name\" was used"
return self._raw["metadata"]["flavor.name"].replace(".", "_")
except KeyError:
# print "\"instance_type\" was used"
return self._raw["metadata"]["instance_type"].replace(".", "_")
@property @property
def memory(self): def memory(self):
return self._raw["metadata"]["memory"] return self._raw["metadata"]["memory"]
@ -126,33 +86,25 @@ class FloatingIP(BaseModelConstruct):
relevant_meters = ["ip.floating"] relevant_meters = ["ip.floating"]
usage_strategies = {"duration": {"usage": "duration", "service": "type"}} transformer = transformers.GaugeMax()
type = "floating_ip" # object storage type = "floating_ip"
@property
def duration(self):
return Decimal(self.usage()["ip.floating"].volume())
class Object(BaseModelConstruct): class Object(BaseModelConstruct):
relevant_meters = ["storage.objects.size"] relevant_meters = ["storage.objects.size"]
usage_strategies = {"size": {"usage": "size", "service": "storage_size"}} transformer = transformers.GaugeMax()
type = "object_storage" # object storage type = "object_storage"
@property
def size(self):
return Decimal(to_mb(self.usage()["storage.objects.size"].volume()))
class Volume(BaseModelConstruct): class Volume(BaseModelConstruct):
relevant_meters = ["volume.size"] relevant_meters = ["volume.size"]
usage_strategies = {"size": {"usage": "size", "service": "volume_size"}} transformer = transformers.GaugeMax()
type = "volume" type = "volume"
@ -161,11 +113,6 @@ class Volume(BaseModelConstruct):
return {"name": self.name, return {"name": self.name,
"type": self.type} "type": self.type}
@property
def size(self):
# Size of the thing over time.
return Decimal(to_mb(self.usage()["volume.size"].volume()))
@property @property
def name(self): def name(self):
return self._raw["metadata"]["display_name"] return self._raw["metadata"]["display_name"]
@ -174,19 +121,6 @@ class Volume(BaseModelConstruct):
class Network(BaseModelConstruct): class Network(BaseModelConstruct):
relevant_meters = ["network.outgoing.bytes", "network.incoming.bytes"] relevant_meters = ["network.outgoing.bytes", "network.incoming.bytes"]
usage_strategies = {"outgoing": {"usage": "outgoing", transformer = transformers.CumulativeTotal()
"service": "outgoing_megabytes"},
"incoming": {"usage": "incoming",
"service": "incoming_megabytes"}}
type = "network_interface" type = "network_interface"
@property
def outgoing(self):
# Size of the thing over time.
return Decimal(to_mb(self.usage()["network.outgoing.bytes"].volume()))
@property
def incoming(self):
# Size of the thing over time.
return Decimal(to_mb(self.usage()["network.incoming.bytes"].volume()))

View File

@ -0,0 +1,128 @@
import datetime
from artifice import constants
class Transformer(object):
meter_type = None
required_meters = []
def transform_usage(self, meters):
self.validate_meters(meters)
return self._transform_usage(meters)
def validate_meters(self, meters):
if self.meter_type is None:
for meter in meters.values():
if meter.name not in self.required_meters:
raise AttributeError("Required meters: " +
str(self.required_meters))
else:
for meter in meters.values():
if meter.type != self.meter_type:
raise AttributeError("Meters must all be of type: " +
self.meter_type)
def _transform_usage(self, meters):
raise NotImplementedError
class Uptime(Transformer):
required_meters = ['state']
# required_meters = ['state', 'flavor']
def _transform_usage(self, meters):
# this NEEDS to be moved to a config file
tracked_states = [constants.active, constants.building,
constants.paused, constants.rescued,
constants.resized]
usage_dict = {}
state = meters['state']
usage = sorted(state.usage(), key=lambda x: x["timestamp"])
last = usage[0]
try:
last["timestamp"] = datetime.datetime.strptime(last["timestamp"],
constants.date_format)
except ValueError:
last["timestamp"] = datetime.datetime.strptime(last["timestamp"],
constants.other_date_format)
except TypeError:
pass
uptime = 0.0
for val in usage[1:]:
try:
val["timestamp"] = datetime.datetime.strptime(val["timestamp"],
constants.date_format)
except ValueError:
val["timestamp"] = datetime.datetime.strptime(val["timestamp"],
constants.other_date_format)
except TypeError:
pass
if val["counter_volume"] in tracked_states:
difference = val["timestamp"] - last["timestamp"]
uptime = uptime + difference.seconds
last = val
usage_dict['flavor1'] = uptime
return usage_dict
class GaugeMax(Transformer):
meter_type = 'gauge'
def _transform_usage(self, meters):
usage_dict = {}
for meter in meters.values():
usage = meter.usage()
max_vol = max([v["counter_volume"] for v in usage])
usage_dict[meter.name] = max_vol
return usage_dict
class GaugeAverage(Transformer):
meter_type = 'gauge'
def _transform_usage(self, meters):
usage_dict = {}
for meter in meters.values():
usage = meter.usage()
length = len(usage)
avg_vol = sum([v["counter_volume"] for v in usage]) / length
usage_dict[meter.name] = avg_vol
return usage_dict
class CumulativeTotal(Transformer):
meter_type = 'cumulative'
def _transform_usage(self, meters):
usage_dict = {}
for meter in meters.values():
measurements = meter.usage()
measurements = sorted(measurements, key=lambda x: x["timestamp"])
count = 0
usage = 0
last_measure = None
for measure in measurements:
if (last_measure is not None and
(measure["counter_volume"] <
last_measure["counter_volume"])):
usage = usage + last_measure["counter_volume"]
count = count + 1
last_measure = measure
usage = usage + measurements[-1]["counter_volume"]
if count > 1:
total_usage = usage - measurements[0]["counter_volume"]
usage_dict[meter.name] = total_usage
return usage_dict

View File

@ -19,7 +19,7 @@ class TestDatabaseModule(test_interface.TestInterface):
count = 0 count = 0
for val in usage.values(): for val in usage.values():
for strat in val.usage_strategies: for strat in val.relevant_meters:
count += 1 count += 1
self.assertEqual(self.session.query(models.UsageEntry).count(), count) self.assertEqual(self.session.query(models.UsageEntry).count(), count)