Merge "Flatten the v2 API"
This commit is contained in:
commit
04e54c4f3e
@ -3,6 +3,7 @@
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
|
||||
# Angus Salkeld <asalkeld@redhat.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
@ -18,75 +19,26 @@
|
||||
"""Version 2 of the API.
|
||||
"""
|
||||
|
||||
# [ ] / -- information about this version of the API
|
||||
# [GET ] / -- information about this version of the API
|
||||
#
|
||||
# [ ] /extensions -- list of available extensions
|
||||
# [ ] /extensions/<extension> -- details about a specific extension
|
||||
# [GET ] /resources -- list the resources
|
||||
# [GET ] /resources/<resource> -- information about the resource
|
||||
# [GET ] /meters -- list the meters
|
||||
# [POST ] /meters -- insert a new sample (and meter/resource if needed)
|
||||
# [GET ] /meters/<meter> -- list the samples for this meter
|
||||
# [PUT ] /meters/<meter> -- update the meter (not the samples)
|
||||
# [DELETE] /meters/<meter> -- delete the meter and samples
|
||||
#
|
||||
# [ ] /sources -- list of known sources (where do we get this?)
|
||||
# [ ] /sources/components -- list of components which provide metering
|
||||
# data (where do we get this)?
|
||||
#
|
||||
# [x] /projects/<project>/resources -- list of resource ids
|
||||
# [x] /resources -- list of resource ids
|
||||
# [x] /sources/<source>/resources -- list of resource ids
|
||||
# [x] /users/<user>/resources -- list of resource ids
|
||||
#
|
||||
# [x] /users -- list of user ids
|
||||
# [x] /sources/<source>/users -- list of user ids
|
||||
#
|
||||
# [x] /projects -- list of project ids
|
||||
# [x] /sources/<source>/projects -- list of project ids
|
||||
#
|
||||
# [ ] /resources/<resource> -- metadata
|
||||
#
|
||||
# [ ] /projects/<project>/meters -- list of meters reporting for parent obj
|
||||
# [ ] /resources/<resource>/meters -- list of meters reporting for parent obj
|
||||
# [ ] /sources/<source>/meters -- list of meters reporting for parent obj
|
||||
# [ ] /users/<user>/meters -- list of meters reporting for parent obj
|
||||
#
|
||||
# [x] /projects/<project>/meters/<meter> -- events
|
||||
# [x] /resources/<resource>/meters/<meter> -- events
|
||||
# [x] /sources/<source>/meters/<meter> -- events
|
||||
# [x] /users/<user>/meters/<meter> -- events
|
||||
#
|
||||
# [ ] /projects/<project>/meters/<meter>/duration -- total time for selected
|
||||
# meter
|
||||
# [x] /resources/<resource>/meters/<meter>/duration -- total time for selected
|
||||
# meter
|
||||
# [ ] /sources/<source>/meters/<meter>/duration -- total time for selected
|
||||
# meter
|
||||
# [ ] /users/<user>/meters/<meter>/duration -- total time for selected meter
|
||||
#
|
||||
# [ ] /projects/<project>/meters/<meter>/volume -- total or max volume for
|
||||
# selected meter
|
||||
# [x] /projects/<project>/meters/<meter>/volume/max -- max volume for
|
||||
# selected meter
|
||||
# [x] /projects/<project>/meters/<meter>/volume/sum -- total volume for
|
||||
# selected meter
|
||||
# [ ] /resources/<resource>/meters/<meter>/volume -- total or max volume for
|
||||
# selected meter
|
||||
# [x] /resources/<resource>/meters/<meter>/volume/max -- max volume for
|
||||
# selected meter
|
||||
# [x] /resources/<resource>/meters/<meter>/volume/sum -- total volume for
|
||||
# selected meter
|
||||
# [ ] /sources/<source>/meters/<meter>/volume -- total or max volume for
|
||||
# selected meter
|
||||
# [ ] /users/<user>/meters/<meter>/volume -- total or max volume for selected
|
||||
# meter
|
||||
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import inspect
|
||||
import pecan
|
||||
from pecan import request
|
||||
from pecan.rest import RestController
|
||||
|
||||
import wsme
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
from wsme.types import Base, text, wsattr
|
||||
from wsme.types import Base, text, Enum
|
||||
|
||||
from ceilometer.openstack.common import jsonutils
|
||||
from ceilometer.openstack.common import log as logging
|
||||
from ceilometer.openstack.common import timeutils
|
||||
from ceilometer import storage
|
||||
@ -95,148 +47,131 @@ from ceilometer import storage
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# FIXME(dhellmann): Change APIs that use this to return float?
|
||||
class MeterVolume(Base):
|
||||
volume = wsattr(float, mandatory=False)
|
||||
|
||||
def __init__(self, volume, **kw):
|
||||
if volume is not None:
|
||||
volume = float(volume)
|
||||
super(MeterVolume, self).__init__(volume=volume, **kw)
|
||||
operation_kind = Enum(str, 'lt', 'le', 'eq', 'ne', 'ge', 'gt')
|
||||
|
||||
|
||||
class DateRange(Base):
|
||||
start = datetime.datetime
|
||||
end = datetime.datetime
|
||||
search_offset = int
|
||||
class Query(Base):
|
||||
def get_op(self):
|
||||
return self._op or 'eq'
|
||||
|
||||
def __init__(self, start=None, end=None, search_offset=0):
|
||||
if start is not None:
|
||||
start = start.replace(tzinfo=None)
|
||||
if end is not None:
|
||||
end = end.replace(tzinfo=None)
|
||||
super(DateRange, self).__init__(start=start,
|
||||
end=end,
|
||||
search_offset=search_offset,
|
||||
)
|
||||
def set_op(self, value):
|
||||
self._op = value
|
||||
|
||||
@property
|
||||
def query_start(self):
|
||||
"""The timestamp the query should use to start, including
|
||||
the search offset.
|
||||
"""
|
||||
if self.start is None:
|
||||
return None
|
||||
return (self.start -
|
||||
datetime.timedelta(minutes=self.search_offset))
|
||||
field = text
|
||||
#op = wsme.wsattr(operation_kind, default='eq')
|
||||
# this ^ doesn't seem to work.
|
||||
op = wsme.wsproperty(operation_kind, get_op, set_op)
|
||||
value = text
|
||||
|
||||
@property
|
||||
def query_end(self):
|
||||
"""The timestamp the query should use to end, including
|
||||
the search offset.
|
||||
"""
|
||||
if self.end is None:
|
||||
return None
|
||||
return (self.end +
|
||||
datetime.timedelta(minutes=self.search_offset))
|
||||
|
||||
def to_dict(self):
|
||||
return {'query_start': self.query_start,
|
||||
'query_end': self.query_end,
|
||||
'start_timestamp': self.start,
|
||||
'end_timestamp': self.end,
|
||||
'search_offset': self.search_offset,
|
||||
}
|
||||
def __repr__(self):
|
||||
# for logging calls
|
||||
return '<Query %r %s %r>' % (self.field, self.op, self.value)
|
||||
|
||||
|
||||
class MeterVolumeController(object):
|
||||
|
||||
@wsme_pecan.wsexpose(MeterVolume, DateRange)
|
||||
def max(self, daterange=None):
|
||||
"""Find the maximum volume for the matching meter events.
|
||||
"""
|
||||
if daterange is None:
|
||||
daterange = DateRange()
|
||||
q_ts = daterange.to_dict()
|
||||
|
||||
try:
|
||||
meter = request.context['meter_id']
|
||||
except KeyError:
|
||||
raise ValueError('No meter specified')
|
||||
|
||||
resource = request.context.get('resource_id')
|
||||
project = request.context.get('project_id')
|
||||
|
||||
# Query the database for the max volume
|
||||
f = storage.EventFilter(meter=meter,
|
||||
resource=resource,
|
||||
start=q_ts['query_start'],
|
||||
end=q_ts['query_end'],
|
||||
project=project,
|
||||
)
|
||||
|
||||
# TODO(sberler): do we want to return an error if the resource
|
||||
# does not exist?
|
||||
results = list(request.storage_conn.get_volume_max(f))
|
||||
|
||||
value = None
|
||||
if results:
|
||||
if resource:
|
||||
# If the caller specified a resource there should only
|
||||
# be one result.
|
||||
value = results[0].get('value')
|
||||
def _query_to_kwargs(query, db_func):
|
||||
# TODO(dhellmann): This function needs tests of its own.
|
||||
valid_keys = inspect.getargspec(db_func)[0]
|
||||
if 'self' in valid_keys:
|
||||
valid_keys.remove('self')
|
||||
translation = {'user_id': 'user',
|
||||
'project_id': 'project',
|
||||
'resource_id': 'resource'}
|
||||
stamp = {}
|
||||
trans = {}
|
||||
metaquery = {}
|
||||
for i in query:
|
||||
if i.field == 'timestamp':
|
||||
# FIXME(dhellmann): This logic is not consistent with the
|
||||
# way the timestamps are treated inside the mongo driver
|
||||
# (the end timestamp is always tested using $lt). We
|
||||
# should just pass a single timestamp through to the
|
||||
# storage layer with the operator and let the storage
|
||||
# layer use that operator.
|
||||
if i.op in ('lt', 'le'):
|
||||
stamp['end_timestamp'] = i.value
|
||||
elif i.op in ('gt', 'ge'):
|
||||
stamp['start_timestamp'] = i.value
|
||||
else:
|
||||
# FIXME(sberler): Currently get_volume_max is really
|
||||
# always grouping by resource_id. We should add a new
|
||||
# function in the storage driver that does not do this
|
||||
# grouping (and potentially rename the existing one to
|
||||
# get_volume_max_by_resource())
|
||||
value = max(result.get('value') for result in results)
|
||||
|
||||
return MeterVolume(volume=value)
|
||||
|
||||
@wsme_pecan.wsexpose(MeterVolume, DateRange)
|
||||
def sum(self, daterange=None):
|
||||
"""Compute the total volume for the matching meter events.
|
||||
"""
|
||||
if daterange is None:
|
||||
daterange = DateRange()
|
||||
q_ts = daterange.to_dict()
|
||||
|
||||
try:
|
||||
meter = request.context['meter_id']
|
||||
except KeyError:
|
||||
raise ValueError('No meter specified')
|
||||
|
||||
resource = request.context.get('resource_id')
|
||||
project = request.context.get('project_id')
|
||||
|
||||
f = storage.EventFilter(meter=meter,
|
||||
project=project,
|
||||
start=q_ts['query_start'],
|
||||
end=q_ts['query_end'],
|
||||
resource=resource,
|
||||
)
|
||||
|
||||
# TODO(sberler): do we want to return an error if the resource
|
||||
# does not exist?
|
||||
results = list(request.storage_conn.get_volume_sum(f))
|
||||
|
||||
value = None
|
||||
if results:
|
||||
if resource:
|
||||
# If the caller specified a resource there should only
|
||||
# be one result.
|
||||
value = results[0].get('value')
|
||||
LOG.warn('_query_to_kwargs ignoring %r unexpected op %r"' %
|
||||
(i.field, i.op))
|
||||
else:
|
||||
if i.op != 'eq':
|
||||
LOG.warn('_query_to_kwargs ignoring %r unimplemented op %r' %
|
||||
(i.field, i.op))
|
||||
elif i.field == 'search_offset':
|
||||
stamp['search_offset'] = i.value
|
||||
elif i.field.startswith('metadata.'):
|
||||
metaquery[i.field] = i.value
|
||||
else:
|
||||
# FIXME(sberler): Currently get_volume_max is really
|
||||
# always grouping by resource_id. We should add a new
|
||||
# function in the storage driver that does not do this
|
||||
# grouping (and potentially rename the existing one to
|
||||
# get_volume_max_by_resource())
|
||||
value = sum(result.get('value') for result in results)
|
||||
trans[translation.get(i.field, i.field)] = i.value
|
||||
|
||||
return MeterVolume(volume=value)
|
||||
kwargs = {}
|
||||
if metaquery and 'metaquery' in valid_keys:
|
||||
kwargs['metaquery'] = metaquery
|
||||
if stamp:
|
||||
q_ts = _get_query_timestamps(stamp)
|
||||
if 'start' in valid_keys:
|
||||
kwargs['start'] = q_ts['query_start']
|
||||
kwargs['end'] = q_ts['query_end']
|
||||
elif 'start_timestamp' in valid_keys:
|
||||
kwargs['start_timestamp'] = q_ts['query_start']
|
||||
kwargs['end_timestamp'] = q_ts['query_end']
|
||||
else:
|
||||
raise wsme.exc.UnknownArgument('timestamp',
|
||||
"not valid for this resource")
|
||||
|
||||
if trans:
|
||||
for k in trans:
|
||||
if k not in valid_keys:
|
||||
raise wsme.exc.UnknownArgument(i.field,
|
||||
"unrecognized query field")
|
||||
kwargs[k] = trans[k]
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def _get_query_timestamps(args={}):
|
||||
"""Return any optional timestamp information in the request.
|
||||
|
||||
Determine the desired range, if any, from the GET arguments. Set
|
||||
up the query range using the specified offset.
|
||||
|
||||
[query_start ... start_timestamp ... end_timestamp ... query_end]
|
||||
|
||||
Returns a dictionary containing:
|
||||
|
||||
query_start: First timestamp to use for query
|
||||
start_timestamp: start_timestamp parameter from request
|
||||
query_end: Final timestamp to use for query
|
||||
end_timestamp: end_timestamp parameter from request
|
||||
search_offset: search_offset parameter from request
|
||||
|
||||
"""
|
||||
search_offset = int(args.get('search_offset', 0))
|
||||
|
||||
start_timestamp = args.get('start_timestamp')
|
||||
if start_timestamp:
|
||||
start_timestamp = timeutils.parse_isotime(start_timestamp)
|
||||
start_timestamp = start_timestamp.replace(tzinfo=None)
|
||||
query_start = (start_timestamp -
|
||||
datetime.timedelta(minutes=search_offset))
|
||||
else:
|
||||
query_start = None
|
||||
|
||||
end_timestamp = args.get('end_timestamp')
|
||||
if end_timestamp:
|
||||
end_timestamp = timeutils.parse_isotime(end_timestamp)
|
||||
end_timestamp = end_timestamp.replace(tzinfo=None)
|
||||
query_end = end_timestamp + datetime.timedelta(minutes=search_offset)
|
||||
else:
|
||||
query_end = None
|
||||
|
||||
return {'query_start': query_start,
|
||||
'query_end': query_end,
|
||||
'start_timestamp': start_timestamp,
|
||||
'end_timestamp': end_timestamp,
|
||||
'search_offset': search_offset,
|
||||
}
|
||||
|
||||
|
||||
def _flatten_metadata(metadata):
|
||||
@ -248,7 +183,7 @@ def _flatten_metadata(metadata):
|
||||
if type(v) not in set([list, dict, set]))
|
||||
|
||||
|
||||
class Event(Base):
|
||||
class Sample(Base):
|
||||
source = text
|
||||
counter_name = text
|
||||
counter_type = text
|
||||
@ -265,78 +200,37 @@ class Event(Base):
|
||||
if counter_volume is not None:
|
||||
counter_volume = float(counter_volume)
|
||||
resource_metadata = _flatten_metadata(resource_metadata)
|
||||
super(Event, self).__init__(counter_volume=counter_volume,
|
||||
resource_metadata=resource_metadata,
|
||||
**kwds)
|
||||
super(Sample, self).__init__(counter_volume=counter_volume,
|
||||
resource_metadata=resource_metadata,
|
||||
**kwds)
|
||||
|
||||
|
||||
class Duration(Base):
|
||||
start_timestamp = datetime.datetime
|
||||
end_timestamp = datetime.datetime
|
||||
class Statistics(Base):
|
||||
min = float
|
||||
max = float
|
||||
avg = float
|
||||
sum = float
|
||||
count = int
|
||||
duration = float
|
||||
duration_start = datetime.datetime
|
||||
duration_end = datetime.datetime
|
||||
|
||||
def __init__(self, start_timestamp=None, end_timestamp=None, **kwds):
|
||||
super(Statistics, self).__init__(**kwds)
|
||||
self._update_duration(start_timestamp, end_timestamp)
|
||||
|
||||
class MeterController(RestController):
|
||||
"""Manages operations on a single meter.
|
||||
"""
|
||||
|
||||
volume = MeterVolumeController()
|
||||
|
||||
_custom_actions = {
|
||||
'duration': ['GET'],
|
||||
}
|
||||
|
||||
def __init__(self, meter_id):
|
||||
request.context['meter_id'] = meter_id
|
||||
self._id = meter_id
|
||||
|
||||
@wsme_pecan.wsexpose([Event], DateRange)
|
||||
def get_all(self, daterange=None):
|
||||
"""Return all events for the meter.
|
||||
"""
|
||||
if daterange is None:
|
||||
daterange = DateRange()
|
||||
f = storage.EventFilter(
|
||||
user=request.context.get('user_id'),
|
||||
project=request.context.get('project_id'),
|
||||
start=daterange.query_start,
|
||||
end=daterange.query_end,
|
||||
resource=request.context.get('resource_id'),
|
||||
meter=self._id,
|
||||
source=request.context.get('source_id'),
|
||||
)
|
||||
return [Event(**e)
|
||||
for e in request.storage_conn.get_raw_events(f)
|
||||
]
|
||||
|
||||
@wsme_pecan.wsexpose(Duration, DateRange)
|
||||
def duration(self, daterange=None):
|
||||
"""Computes the duration of the meter events in the time range given.
|
||||
"""
|
||||
if daterange is None:
|
||||
daterange = DateRange()
|
||||
|
||||
# Query the database for the interval of timestamps
|
||||
# within the desired range.
|
||||
f = storage.EventFilter(user=request.context.get('user_id'),
|
||||
project=request.context.get('project_id'),
|
||||
start=daterange.query_start,
|
||||
end=daterange.query_end,
|
||||
resource=request.context.get('resource_id'),
|
||||
meter=self._id,
|
||||
source=request.context.get('source_id'),
|
||||
)
|
||||
min_ts, max_ts = request.storage_conn.get_event_interval(f)
|
||||
|
||||
def _update_duration(self, start_timestamp, end_timestamp):
|
||||
# "Clamp" the timestamps we return to the original time
|
||||
# range, excluding the offset.
|
||||
LOG.debug('start_timestamp %s, end_timestamp %s, min_ts %s, max_ts %s',
|
||||
daterange.start, daterange.end, min_ts, max_ts)
|
||||
if daterange.start and min_ts and min_ts < daterange.start:
|
||||
min_ts = daterange.start
|
||||
if (start_timestamp and
|
||||
self.duration_start and
|
||||
self.duration_start < start_timestamp):
|
||||
self.duration_start = start_timestamp
|
||||
LOG.debug('clamping min timestamp to range')
|
||||
if daterange.end and max_ts and max_ts > daterange.end:
|
||||
max_ts = daterange.end
|
||||
if (end_timestamp and
|
||||
self.duration_end and
|
||||
self.duration_end > end_timestamp):
|
||||
self.duration_end = end_timestamp
|
||||
LOG.debug('clamping max timestamp to range')
|
||||
|
||||
# If we got valid timestamps back, compute a duration in minutes.
|
||||
@ -349,23 +243,65 @@ class MeterController(RestController):
|
||||
# If the timestamps are invalid, return None as a
|
||||
# sentinal indicating that there is something "funny"
|
||||
# about the range.
|
||||
if min_ts and max_ts and (min_ts <= max_ts):
|
||||
if (self.duration_start and
|
||||
self.duration_end and
|
||||
self.duration_start <= self.duration_end):
|
||||
# Can't use timedelta.total_seconds() because
|
||||
# it is not available in Python 2.6.
|
||||
diff = max_ts - min_ts
|
||||
duration = (diff.seconds + (diff.days * 24 * 60 ** 2)) / 60
|
||||
diff = self.duration_end - self.duration_start
|
||||
self.duration = (diff.seconds + (diff.days * 24 * 60 ** 2)) / 60
|
||||
else:
|
||||
min_ts = max_ts = duration = None
|
||||
self.duration_start = self.duration_end = self.duration = None
|
||||
|
||||
return Duration(start_timestamp=min_ts,
|
||||
end_timestamp=max_ts,
|
||||
duration=duration,
|
||||
)
|
||||
|
||||
class MeterController(RestController):
|
||||
"""Manages operations on a single meter.
|
||||
"""
|
||||
_custom_actions = {
|
||||
'statistics': ['GET'],
|
||||
}
|
||||
|
||||
def __init__(self, meter_id):
|
||||
request.context['meter_id'] = meter_id
|
||||
self._id = meter_id
|
||||
|
||||
@wsme_pecan.wsexpose([Sample], [Query])
|
||||
def get_all(self, q=[]):
|
||||
"""Return all events for the meter.
|
||||
"""
|
||||
kwargs = _query_to_kwargs(q, storage.EventFilter.__init__)
|
||||
kwargs['meter'] = self._id
|
||||
f = storage.EventFilter(**kwargs)
|
||||
return [Sample(**e)
|
||||
for e in request.storage_conn.get_raw_events(f)
|
||||
]
|
||||
|
||||
@wsme_pecan.wsexpose(Statistics, [Query])
|
||||
def statistics(self, q=[]):
|
||||
"""Computes the statistics of the meter events in the time range given.
|
||||
"""
|
||||
kwargs = _query_to_kwargs(q, storage.EventFilter.__init__)
|
||||
kwargs['meter'] = self._id
|
||||
f = storage.EventFilter(**kwargs)
|
||||
computed = request.storage_conn.get_meter_statistics(f)
|
||||
# Find the original timestamp in the query to use for clamping
|
||||
# the duration returned in the statistics.
|
||||
start = end = None
|
||||
for i in q:
|
||||
if i.field == 'timestamp' and i.op in ('lt', 'le'):
|
||||
end = timeutils.parse_isotime(i.value).replace(tzinfo=None)
|
||||
elif i.field == 'timestamp' and i.op in ('gt', 'ge'):
|
||||
start = timeutils.parse_isotime(i.value).replace(tzinfo=None)
|
||||
stat = Statistics(start_timestamp=start,
|
||||
end_timestamp=end,
|
||||
**computed)
|
||||
return stat
|
||||
|
||||
|
||||
class Meter(Base):
|
||||
name = text
|
||||
type = text
|
||||
unit = text
|
||||
resource_id = text
|
||||
project_id = text
|
||||
user_id = text
|
||||
@ -378,18 +314,23 @@ class MetersController(RestController):
|
||||
def _lookup(self, meter_id, *remainder):
|
||||
return MeterController(meter_id), remainder
|
||||
|
||||
@wsme_pecan.wsexpose([Meter])
|
||||
def get_all(self):
|
||||
user_id = request.context.get('user_id')
|
||||
project_id = request.context.get('project_id')
|
||||
resource_id = request.context.get('resource_id')
|
||||
source_id = request.context.get('source_id')
|
||||
@wsme_pecan.wsexpose([Meter], [Query])
|
||||
def get_all(self, q=[]):
|
||||
kwargs = _query_to_kwargs(q, request.storage_conn.get_meters)
|
||||
return [Meter(**m)
|
||||
for m in request.storage_conn.get_meters(user=user_id,
|
||||
project=project_id,
|
||||
resource=resource_id,
|
||||
source=source_id,
|
||||
)]
|
||||
for m in request.storage_conn.get_meters(**kwargs)]
|
||||
|
||||
|
||||
class Resource(Base):
|
||||
resource_id = text
|
||||
project_id = text
|
||||
user_id = text
|
||||
timestamp = datetime.datetime
|
||||
metadata = {text: text}
|
||||
|
||||
def __init__(self, metadata={}, **kwds):
|
||||
metadata = _flatten_metadata(metadata)
|
||||
super(Resource, self).__init__(metadata=metadata, **kwds)
|
||||
|
||||
|
||||
class ResourceController(RestController):
|
||||
@ -399,28 +340,11 @@ class ResourceController(RestController):
|
||||
def __init__(self, resource_id):
|
||||
request.context['resource_id'] = resource_id
|
||||
|
||||
meters = MetersController()
|
||||
|
||||
|
||||
class MeterDescription(Base):
|
||||
counter_name = text
|
||||
counter_type = text
|
||||
|
||||
|
||||
class Resource(Base):
|
||||
resource_id = text
|
||||
project_id = text
|
||||
user_id = text
|
||||
timestamp = datetime.datetime
|
||||
metadata = {text: text}
|
||||
meter = wsattr([MeterDescription])
|
||||
|
||||
def __init__(self, meter=[], metadata={}, **kwds):
|
||||
meter = [MeterDescription(**m) for m in meter]
|
||||
metadata = _flatten_metadata(metadata)
|
||||
super(Resource, self).__init__(meter=meter,
|
||||
metadata=metadata,
|
||||
**kwds)
|
||||
@wsme_pecan.wsexpose([Resource])
|
||||
def get_all(self):
|
||||
r = request.storage_conn.get_resources(
|
||||
resource=request.context.get('resource_id'))[0]
|
||||
return Resource(**r)
|
||||
|
||||
|
||||
class ResourcesController(RestController):
|
||||
@ -430,153 +354,17 @@ class ResourcesController(RestController):
|
||||
def _lookup(self, resource_id, *remainder):
|
||||
return ResourceController(resource_id), remainder
|
||||
|
||||
@wsme_pecan.wsexpose([Resource])
|
||||
def get_all(self, start_timestamp=None, end_timestamp=None):
|
||||
if start_timestamp:
|
||||
start_timestamp = timeutils.parse_isotime(start_timestamp)
|
||||
if end_timestamp:
|
||||
end_timestamp = timeutils.parse_isotime(end_timestamp)
|
||||
|
||||
@wsme_pecan.wsexpose([Resource], [Query])
|
||||
def get_all(self, q=[]):
|
||||
kwargs = _query_to_kwargs(q, request.storage_conn.get_resources)
|
||||
resources = [
|
||||
Resource(**r)
|
||||
for r in request.storage_conn.get_resources(
|
||||
source=request.context.get('source_id'),
|
||||
user=request.context.get('user_id'),
|
||||
project=request.context.get('project_id'),
|
||||
start_timestamp=start_timestamp,
|
||||
end_timestamp=end_timestamp,
|
||||
)
|
||||
]
|
||||
for r in request.storage_conn.get_resources(**kwargs)]
|
||||
return resources
|
||||
|
||||
|
||||
class ProjectController(RestController):
|
||||
"""Works on resources."""
|
||||
|
||||
def __init__(self, project_id):
|
||||
request.context['project_id'] = project_id
|
||||
|
||||
meters = MetersController()
|
||||
resources = ResourcesController()
|
||||
|
||||
|
||||
class ProjectsController(RestController):
|
||||
"""Works on projects."""
|
||||
|
||||
@pecan.expose()
|
||||
def _lookup(self, project_id, *remainder):
|
||||
return ProjectController(project_id), remainder
|
||||
|
||||
@wsme_pecan.wsexpose([text])
|
||||
def get_all(self):
|
||||
source_id = request.context.get('source_id')
|
||||
projects = list(request.storage_conn.get_projects(source=source_id))
|
||||
return projects
|
||||
|
||||
meters = MetersController()
|
||||
|
||||
|
||||
class UserController(RestController):
|
||||
"""Works on reusers."""
|
||||
|
||||
def __init__(self, user_id):
|
||||
request.context['user_id'] = user_id
|
||||
|
||||
meters = MetersController()
|
||||
resources = ResourcesController()
|
||||
|
||||
|
||||
class UsersController(RestController):
|
||||
"""Works on users."""
|
||||
|
||||
@pecan.expose()
|
||||
def _lookup(self, user_id, *remainder):
|
||||
return UserController(user_id), remainder
|
||||
|
||||
@wsme_pecan.wsexpose([text])
|
||||
def get_all(self):
|
||||
source_id = request.context.get('source_id')
|
||||
users = list(request.storage_conn.get_users(source=source_id))
|
||||
return users
|
||||
|
||||
|
||||
class Source(Base):
|
||||
name = text
|
||||
data = {text: text}
|
||||
|
||||
@staticmethod
|
||||
def sample():
|
||||
return Source(name='openstack',
|
||||
data={'key': 'value'})
|
||||
|
||||
|
||||
class SourceController(RestController):
|
||||
"""Works on resources."""
|
||||
|
||||
def __init__(self, source_id, data):
|
||||
request.context['source_id'] = source_id
|
||||
self._id = source_id
|
||||
self._data = data
|
||||
|
||||
@wsme_pecan.wsexpose(Source)
|
||||
def get(self):
|
||||
response = Source(name=self._id, data=self._data)
|
||||
return response
|
||||
|
||||
meters = MetersController()
|
||||
resources = ResourcesController()
|
||||
projects = ProjectsController()
|
||||
users = UsersController()
|
||||
|
||||
|
||||
class SourcesController(RestController):
|
||||
"""Works on sources."""
|
||||
|
||||
def __init__(self):
|
||||
self._sources = None
|
||||
|
||||
@property
|
||||
def sources(self):
|
||||
# FIXME(dhellmann): Add a configuration option for the filename.
|
||||
#
|
||||
# FIXME(dhellmann): We only want to load the file once in a process,
|
||||
# but we want to be able to mock the loading out in separate tests.
|
||||
#
|
||||
if not self._sources:
|
||||
self._sources = self._load_sources(os.path.abspath("sources.json"))
|
||||
return self._sources
|
||||
|
||||
@staticmethod
|
||||
def _load_sources(filename):
|
||||
try:
|
||||
with open(filename, "r") as f:
|
||||
sources = jsonutils.load(f)
|
||||
except IOError as err:
|
||||
LOG.warning('Could not load data source definitions from %s: %s' %
|
||||
(filename, err))
|
||||
sources = {}
|
||||
return sources
|
||||
|
||||
@pecan.expose()
|
||||
def _lookup(self, source_id, *remainder):
|
||||
try:
|
||||
data = self.sources[source_id]
|
||||
except KeyError:
|
||||
# Unknown source
|
||||
pecan.abort(404, detail='No source %s' % source_id)
|
||||
return SourceController(source_id, data), remainder
|
||||
|
||||
@wsme_pecan.wsexpose([Source])
|
||||
def get_all(self):
|
||||
return [Source(name=key, data=value)
|
||||
for key, value in self.sources.iteritems()]
|
||||
|
||||
|
||||
class V2Controller(object):
|
||||
"""Version 2 API controller root."""
|
||||
|
||||
projects = ProjectsController()
|
||||
resources = ResourcesController()
|
||||
sources = SourcesController()
|
||||
users = UsersController()
|
||||
meters = MetersController()
|
||||
|
@ -83,7 +83,7 @@ class Connection(object):
|
||||
@abc.abstractmethod
|
||||
def get_resources(self, user=None, project=None, source=None,
|
||||
start_timestamp=None, end_timestamp=None,
|
||||
metaquery={}):
|
||||
metaquery={}, resource=None):
|
||||
"""Return an iterable of dictionaries containing resource information.
|
||||
|
||||
{ 'resource_id': UUID of the resource,
|
||||
@ -99,7 +99,8 @@ class Connection(object):
|
||||
:param source: Optional source filter.
|
||||
:param start_timestamp: Optional modified timestamp start range.
|
||||
:param end_timestamp: Optional modified timestamp end range.
|
||||
:param metaquery: Optional dict with metadata to match on..
|
||||
:param metaquery: Optional dict with metadata to match on.
|
||||
:param resource: Optional resource filter.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@ -159,3 +160,22 @@ class Connection(object):
|
||||
|
||||
( datetime.datetime(), datetime.datetime() )
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_meter_statistics(self, event_filter):
|
||||
"""Return a dictionary containing meter statistics.
|
||||
described by the query parameters.
|
||||
|
||||
The filter must have a meter value set.
|
||||
|
||||
{ 'min':
|
||||
'max':
|
||||
'avg':
|
||||
'sum':
|
||||
'count':
|
||||
'duration':
|
||||
'duration_start':
|
||||
'duration_end':
|
||||
}
|
||||
|
||||
"""
|
||||
|
@ -73,7 +73,7 @@ class Connection(base.Connection):
|
||||
|
||||
def get_resources(self, user=None, project=None, source=None,
|
||||
start_timestamp=None, end_timestamp=None,
|
||||
metaquery={}):
|
||||
metaquery={}, resource=None):
|
||||
"""Return an iterable of dictionaries containing resource information.
|
||||
|
||||
{ 'resource_id': UUID of the resource,
|
||||
@ -90,6 +90,7 @@ class Connection(base.Connection):
|
||||
:param start_timestamp: Optional modified timestamp start range.
|
||||
:param end_timestamp: Optional modified timestamp end range.
|
||||
:param metaquery: Optional dict with metadata to match on.
|
||||
:param resource: Optional resource filter.
|
||||
"""
|
||||
|
||||
def get_meters(self, user=None, project=None, resource=None, source=None,
|
||||
@ -129,3 +130,21 @@ class Connection(base.Connection):
|
||||
"""Return the min and max timestamp for events
|
||||
matching the event_filter.
|
||||
"""
|
||||
|
||||
def get_meter_statistics(self, event_filter):
|
||||
"""Return a dictionary containing meter statistics.
|
||||
described by the query parameters.
|
||||
|
||||
The filter must have a meter value set.
|
||||
|
||||
{ 'min':
|
||||
'max':
|
||||
'avg':
|
||||
'sum':
|
||||
'count':
|
||||
'duration':
|
||||
'duration_start':
|
||||
'duration_end':
|
||||
}
|
||||
|
||||
"""
|
||||
|
@ -176,6 +176,36 @@ class Connection(base.Connection):
|
||||
}
|
||||
""")
|
||||
|
||||
MAP_STATS = bson.code.Code("""
|
||||
function () {
|
||||
emit('statistics', { min : this.counter_volume,
|
||||
max : this.counter_volume,
|
||||
qty : this.counter_volume,
|
||||
count : 1,
|
||||
timestamp_min : this.timestamp,
|
||||
timestamp_max : this.timestamp } )
|
||||
}
|
||||
""")
|
||||
|
||||
REDUCE_STATS = bson.code.Code("""
|
||||
function (key, values) {
|
||||
var res = values[0];
|
||||
for ( var i=1; i<values.length; i++ ) {
|
||||
if ( values[i].min < res.min )
|
||||
res.min = values[i].min;
|
||||
if ( values[i].max > res.max )
|
||||
res.max = values[i].max;
|
||||
res.count += values[i].count;
|
||||
res.qty += values[i].qty;
|
||||
if ( values[i].timestamp_min < res.timestamp_min )
|
||||
res.timestamp_min = values[i].timestamp_min;
|
||||
if ( values[i].timestamp_max > res.timestamp_max )
|
||||
res.timestamp_max = values[i].timestamp_max;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
""")
|
||||
|
||||
def __init__(self, conf):
|
||||
opts = self._parse_connection_url(conf.database_connection)
|
||||
LOG.info('connecting to MongoDB on %s:%s', opts['host'], opts['port'])
|
||||
@ -308,7 +338,7 @@ class Connection(base.Connection):
|
||||
|
||||
def get_resources(self, user=None, project=None, source=None,
|
||||
start_timestamp=None, end_timestamp=None,
|
||||
metaquery={}):
|
||||
metaquery={}, resource=None):
|
||||
"""Return an iterable of dictionaries containing resource information.
|
||||
|
||||
{ 'resource_id': UUID of the resource,
|
||||
@ -325,6 +355,7 @@ class Connection(base.Connection):
|
||||
:param start_timestamp: Optional modified timestamp start range.
|
||||
:param end_timestamp: Optional modified timestamp end range.
|
||||
:param metaquery: Optional dict with metadata to match on.
|
||||
:param resource: Optional resource filter.
|
||||
"""
|
||||
q = {}
|
||||
if user is not None:
|
||||
@ -333,6 +364,8 @@ class Connection(base.Connection):
|
||||
q['project_id'] = project
|
||||
if source is not None:
|
||||
q['source'] = source
|
||||
if resource is not None:
|
||||
q['_id'] = resource
|
||||
q.update(metaquery)
|
||||
|
||||
# FIXME(dhellmann): This may not perform very well,
|
||||
@ -412,6 +445,56 @@ class Connection(base.Connection):
|
||||
del e['_id']
|
||||
yield e
|
||||
|
||||
def get_meter_statistics(self, event_filter):
|
||||
"""Return a dictionary containing meter statistics.
|
||||
described by the query parameters.
|
||||
|
||||
The filter must have a meter value set.
|
||||
|
||||
{ 'min':
|
||||
'max':
|
||||
'avg':
|
||||
'sum':
|
||||
'count':
|
||||
'duration':
|
||||
'duration_start':
|
||||
'duration_end':
|
||||
}
|
||||
|
||||
"""
|
||||
q = make_query_from_filter(event_filter)
|
||||
results = self.db.meter.map_reduce(self.MAP_STATS,
|
||||
self.REDUCE_STATS,
|
||||
{'inline': 1},
|
||||
query=q,
|
||||
)
|
||||
if results['results']:
|
||||
r = results['results'][0]['value']
|
||||
(start, end) = self._fix_interval_min_max(r['timestamp_min'],
|
||||
r['timestamp_max'])
|
||||
else:
|
||||
start = None
|
||||
end = None
|
||||
r = {'count': 0,
|
||||
'min': None,
|
||||
'max': None,
|
||||
'avg': None,
|
||||
'qty': None,
|
||||
'duration': None,
|
||||
'duration_start': None,
|
||||
'duration_end': None,
|
||||
}
|
||||
count = int(r['count'])
|
||||
return {'min': r['min'],
|
||||
'sum': r['qty'],
|
||||
'count': count,
|
||||
'avg': (r['qty'] / count) if count > 0 else None,
|
||||
'max': r['max'],
|
||||
'duration': 0,
|
||||
'duration_start': start,
|
||||
'duration_end': end,
|
||||
}
|
||||
|
||||
def get_volume_sum(self, event_filter):
|
||||
"""Return the sum of the volume field for the events
|
||||
described by the query parameters.
|
||||
@ -438,6 +521,34 @@ class Connection(base.Connection):
|
||||
return ({'resource_id': r['_id'], 'value': r['value']}
|
||||
for r in results['results'])
|
||||
|
||||
def _fix_interval_min_max(self, a_min, a_max):
|
||||
if hasattr(a_min, 'valueOf') and a_min.valueOf is not None:
|
||||
# NOTE (dhellmann): HACK ALERT
|
||||
#
|
||||
# The real MongoDB server can handle Date objects and
|
||||
# the driver converts them to datetime instances
|
||||
# correctly but the in-memory implementation in MIM
|
||||
# (used by the tests) returns a spidermonkey.Object
|
||||
# representing the "value" dictionary and there
|
||||
# doesn't seem to be a way to recursively introspect
|
||||
# that object safely to convert the min and max values
|
||||
# back to datetime objects. In this method, we know
|
||||
# what type the min and max values are expected to be,
|
||||
# so it is safe to do the conversion
|
||||
# here. JavaScript's time representation uses
|
||||
# different units than Python's, so we divide to
|
||||
# convert to the right units and then create the
|
||||
# datetime instances to return.
|
||||
#
|
||||
# The issue with MIM is documented at
|
||||
# https://sourceforge.net/p/merciless/bugs/3/
|
||||
#
|
||||
a_min = datetime.datetime.fromtimestamp(
|
||||
a_min.valueOf() // 1000)
|
||||
a_max = datetime.datetime.fromtimestamp(
|
||||
a_max.valueOf() // 1000)
|
||||
return (a_min, a_max)
|
||||
|
||||
def get_event_interval(self, event_filter):
|
||||
"""Return the min and max timestamps from events,
|
||||
using the event_filter to limit the events seen.
|
||||
@ -452,32 +563,5 @@ class Connection(base.Connection):
|
||||
)
|
||||
if results['results']:
|
||||
answer = results['results'][0]['value']
|
||||
a_min = answer['min']
|
||||
a_max = answer['max']
|
||||
if hasattr(a_min, 'valueOf') and a_min.valueOf is not None:
|
||||
# NOTE (dhellmann): HACK ALERT
|
||||
#
|
||||
# The real MongoDB server can handle Date objects and
|
||||
# the driver converts them to datetime instances
|
||||
# correctly but the in-memory implementation in MIM
|
||||
# (used by the tests) returns a spidermonkey.Object
|
||||
# representing the "value" dictionary and there
|
||||
# doesn't seem to be a way to recursively introspect
|
||||
# that object safely to convert the min and max values
|
||||
# back to datetime objects. In this method, we know
|
||||
# what type the min and max values are expected to be,
|
||||
# so it is safe to do the conversion
|
||||
# here. JavaScript's time representation uses
|
||||
# different units than Python's, so we divide to
|
||||
# convert to the right units and then create the
|
||||
# datetime instances to return.
|
||||
#
|
||||
# The issue with MIM is documented at
|
||||
# https://sourceforge.net/p/merciless/bugs/3/
|
||||
#
|
||||
a_min = datetime.datetime.fromtimestamp(
|
||||
a_min.valueOf() // 1000)
|
||||
a_max = datetime.datetime.fromtimestamp(
|
||||
a_max.valueOf() // 1000)
|
||||
return (a_min, a_max)
|
||||
return self._fix_interval_min_max(answer['min'], answer['max'])
|
||||
return (None, None)
|
||||
|
@ -222,7 +222,7 @@ class Connection(base.Connection):
|
||||
|
||||
def get_resources(self, user=None, project=None, source=None,
|
||||
start_timestamp=None, end_timestamp=None,
|
||||
metaquery=None):
|
||||
metaquery=None, resource=None):
|
||||
"""Return an iterable of dictionaries containing resource information.
|
||||
|
||||
{ 'resource_id': UUID of the resource,
|
||||
@ -239,6 +239,7 @@ class Connection(base.Connection):
|
||||
:param start_timestamp: Optional modified timestamp start range.
|
||||
:param end_timestamp: Optional modified timestamp end range.
|
||||
:param metaquery: Optional dict with metadata to match on.
|
||||
:param resource: Optional resource filter.
|
||||
"""
|
||||
query = model_query(Resource, session=self.session)
|
||||
if user is not None:
|
||||
@ -251,6 +252,8 @@ class Connection(base.Connection):
|
||||
query = query.filter(Resource.timestamp < end_timestamp)
|
||||
if project is not None:
|
||||
query = query.filter(Resource.project_id == project)
|
||||
if resource is not None:
|
||||
query = query.filter(Resource.id == resource)
|
||||
query = query.options(
|
||||
sqlalchemy_session.sqlalchemy.orm.joinedload('meters'))
|
||||
if metaquery is not None:
|
||||
@ -368,6 +371,41 @@ class Connection(base.Connection):
|
||||
a_min, a_max = results[0]
|
||||
return (a_min, a_max)
|
||||
|
||||
def get_meter_statistics(self, event_filter):
|
||||
"""Return a dictionary containing meter statistics.
|
||||
described by the query parameters.
|
||||
|
||||
The filter must have a meter value set.
|
||||
|
||||
{ 'min':
|
||||
'max':
|
||||
'avg':
|
||||
'sum':
|
||||
'count':
|
||||
'duration':
|
||||
'duration_start':
|
||||
'duration_end':
|
||||
}
|
||||
"""
|
||||
query = self.session.query(func.min(Meter.timestamp),
|
||||
func.max(Meter.timestamp),
|
||||
func.sum(Meter.counter_volume),
|
||||
func.min(Meter.counter_volume),
|
||||
func.max(Meter.counter_volume),
|
||||
func.count(Meter.counter_volume))
|
||||
query = make_query_from_filter(query, event_filter)
|
||||
results = query.all()
|
||||
res = results[0]
|
||||
count = int(res[5])
|
||||
return {'count': count,
|
||||
'min': res[3],
|
||||
'max': res[4],
|
||||
'avg': (res[2] / count) if count > 0 else None,
|
||||
'sum': res[2],
|
||||
'duration': None,
|
||||
'duration_start': res[0],
|
||||
'duration_end': res[1],
|
||||
}
|
||||
|
||||
############################
|
||||
|
||||
|
@ -133,21 +133,10 @@ class FunctionalTest(unittest.TestCase):
|
||||
self.stubs = stubout.StubOutForTesting()
|
||||
|
||||
self.app = self._make_app()
|
||||
self._stubout_sources()
|
||||
|
||||
def _make_app(self):
|
||||
return load_test_app(self.config)
|
||||
|
||||
def _stubout_sources(self):
|
||||
"""Source data is usually read from a file, but
|
||||
we want to let tests define their own. The class
|
||||
attribute SOURCE_DATA is injected into the controller
|
||||
as though it was read from the usual configuration
|
||||
file.
|
||||
"""
|
||||
self.stubs.SmartSet(v2.SourcesController, 'sources',
|
||||
self.SOURCE_DATA)
|
||||
|
||||
def tearDown(self):
|
||||
self.mox.UnsetStubs()
|
||||
self.stubs.UnsetAll()
|
||||
@ -156,11 +145,19 @@ class FunctionalTest(unittest.TestCase):
|
||||
set_config({}, overwrite=True)
|
||||
|
||||
def get_json(self, path, expect_errors=False, headers=None,
|
||||
extra_params={}, **params):
|
||||
q=[], **params):
|
||||
full_path = self.PATH_PREFIX + path
|
||||
query_params = {'q.field': [],
|
||||
'q.value': [],
|
||||
'q.op': [],
|
||||
}
|
||||
for query in q:
|
||||
for name in ['field', 'op', 'value']:
|
||||
query_params['q.%s' % name].append(query.get(name, ''))
|
||||
all_params = {}
|
||||
all_params.update(params)
|
||||
all_params.update(extra_params)
|
||||
if q:
|
||||
all_params.update(query_params)
|
||||
print 'GET: %s %r' % (full_path, all_params)
|
||||
response = self.app.get(full_path,
|
||||
params=all_params,
|
||||
|
@ -44,11 +44,11 @@ class TestAPIACL(FunctionalTest):
|
||||
return result
|
||||
|
||||
def test_non_authenticated(self):
|
||||
response = self.get_json('/sources', expect_errors=True)
|
||||
response = self.get_json('/meters', expect_errors=True)
|
||||
self.assertEqual(response.status_int, 401)
|
||||
|
||||
def test_authenticated_wrong_role(self):
|
||||
response = self.get_json('/sources',
|
||||
response = self.get_json('/meters',
|
||||
expect_errors=True,
|
||||
headers={
|
||||
"X-Roles": "Member",
|
||||
@ -74,7 +74,7 @@ class TestAPIACL(FunctionalTest):
|
||||
# self.assertEqual(response.status_int, 401)
|
||||
|
||||
def test_authenticated(self):
|
||||
response = self.get_json('/sources',
|
||||
response = self.get_json('/meters',
|
||||
expect_errors=True,
|
||||
headers={
|
||||
"X-Roles": "admin",
|
||||
|
@ -51,31 +51,40 @@ class TestComputeDurationByResource(FunctionalTest):
|
||||
|
||||
def _stub_interval_func(self, func):
|
||||
self.stubs.Set(impl_test.TestConnection,
|
||||
'get_event_interval',
|
||||
'get_meter_statistics',
|
||||
func)
|
||||
|
||||
def _set_interval(self, start, end):
|
||||
def get_interval(ignore_self, event_filter):
|
||||
assert event_filter.start
|
||||
assert event_filter.end
|
||||
return (start, end)
|
||||
return {'count': 0,
|
||||
'min': None,
|
||||
'max': None,
|
||||
'avg': None,
|
||||
'qty': None,
|
||||
'duration': None,
|
||||
'duration_start': start,
|
||||
'duration_end': end,
|
||||
}
|
||||
self._stub_interval_func(get_interval)
|
||||
|
||||
def _invoke_api(self):
|
||||
return self.get_json(
|
||||
'/resources/resource-id/meters/instance:m1.tiny/duration',
|
||||
extra_params={
|
||||
'daterange.start': self.start.isoformat(),
|
||||
'daterange.end': self.end.isoformat(),
|
||||
# this value doesn't matter, db call is mocked
|
||||
'daterange.search_offset': 10,
|
||||
})
|
||||
return self.get_json('/meters/instance:m1.tiny/statistics',
|
||||
q=[{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': self.start.isoformat()},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': self.end.isoformat()},
|
||||
{'field': 'search_offset',
|
||||
'value': 10}])
|
||||
|
||||
def test_before_range(self):
|
||||
self._set_interval(self.early1, self.early2)
|
||||
data = self._invoke_api()
|
||||
assert data['start_timestamp'] is None
|
||||
assert data['end_timestamp'] is None
|
||||
assert data['duration_start'] is None
|
||||
assert data['duration_end'] is None
|
||||
assert data['duration'] is None
|
||||
|
||||
def _assert_times_match(self, actual, expected):
|
||||
@ -88,62 +97,81 @@ class TestComputeDurationByResource(FunctionalTest):
|
||||
def test_overlap_range_start(self):
|
||||
self._set_interval(self.early1, self.middle1)
|
||||
data = self._invoke_api()
|
||||
self._assert_times_match(data['start_timestamp'], self.start)
|
||||
self._assert_times_match(data['end_timestamp'], self.middle1)
|
||||
self._assert_times_match(data['duration_start'], self.start)
|
||||
self._assert_times_match(data['duration_end'], self.middle1)
|
||||
assert data['duration'] == 8 * 60
|
||||
|
||||
def test_within_range(self):
|
||||
self._set_interval(self.middle1, self.middle2)
|
||||
data = self._invoke_api()
|
||||
self._assert_times_match(data['start_timestamp'], self.middle1)
|
||||
self._assert_times_match(data['end_timestamp'], self.middle2)
|
||||
self._assert_times_match(data['duration_start'], self.middle1)
|
||||
self._assert_times_match(data['duration_end'], self.middle2)
|
||||
assert data['duration'] == 10 * 60
|
||||
|
||||
def test_within_range_zero_duration(self):
|
||||
self._set_interval(self.middle1, self.middle1)
|
||||
data = self._invoke_api()
|
||||
self._assert_times_match(data['start_timestamp'], self.middle1)
|
||||
self._assert_times_match(data['end_timestamp'], self.middle1)
|
||||
self._assert_times_match(data['duration_start'], self.middle1)
|
||||
self._assert_times_match(data['duration_end'], self.middle1)
|
||||
assert data['duration'] == 0
|
||||
|
||||
def test_overlap_range_end(self):
|
||||
self._set_interval(self.middle2, self.late1)
|
||||
data = self._invoke_api()
|
||||
self._assert_times_match(data['start_timestamp'], self.middle2)
|
||||
self._assert_times_match(data['end_timestamp'], self.end)
|
||||
self._assert_times_match(data['duration_start'], self.middle2)
|
||||
self._assert_times_match(data['duration_end'], self.end)
|
||||
assert data['duration'] == (6 * 60) - 1
|
||||
|
||||
def test_after_range(self):
|
||||
self._set_interval(self.late1, self.late2)
|
||||
data = self._invoke_api()
|
||||
assert data['start_timestamp'] is None
|
||||
assert data['end_timestamp'] is None
|
||||
assert data['duration_start'] is None
|
||||
assert data['duration_end'] is None
|
||||
assert data['duration'] is None
|
||||
|
||||
def test_without_end_timestamp(self):
|
||||
def get_interval(ignore_self, event_filter):
|
||||
return (self.late1, self.late2)
|
||||
return {'count': 0,
|
||||
'min': None,
|
||||
'max': None,
|
||||
'avg': None,
|
||||
'qty': None,
|
||||
'duration': None,
|
||||
'duration_start': self.late1,
|
||||
'duration_end': self.late2,
|
||||
}
|
||||
self._stub_interval_func(get_interval)
|
||||
data = self.get_json(
|
||||
'/resources/resource-id/meters/instance:m1.tiny/duration',
|
||||
extra_params={
|
||||
'daterange.start': self.late1.isoformat(),
|
||||
# this value doesn't matter, db call is mocked
|
||||
'daterange.search_offset': 10,
|
||||
})
|
||||
self._assert_times_match(data['start_timestamp'], self.late1)
|
||||
self._assert_times_match(data['end_timestamp'], self.late2)
|
||||
data = self.get_json('/meters/instance:m1.tiny/statistics',
|
||||
q=[{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': self.late1.isoformat()},
|
||||
{'field': 'resource_id',
|
||||
'value': 'resource-id'},
|
||||
{'field': 'search_offset',
|
||||
'value': 10}])
|
||||
self._assert_times_match(data['duration_start'], self.late1)
|
||||
self._assert_times_match(data['duration_end'], self.late2)
|
||||
|
||||
def test_without_start_timestamp(self):
|
||||
def get_interval(ignore_self, event_filter):
|
||||
return {'count': 0,
|
||||
'min': None,
|
||||
'max': None,
|
||||
'avg': None,
|
||||
'qty': None,
|
||||
'duration': None,
|
||||
'duration_start': self.early1,
|
||||
'duration_end': self.early2,
|
||||
}
|
||||
return (self.early1, self.early2)
|
||||
self._stub_interval_func(get_interval)
|
||||
data = self.get_json(
|
||||
'/resources/resource-id/meters/instance:m1.tiny/duration',
|
||||
extra_params={
|
||||
'daterange.end': self.early2.isoformat(),
|
||||
# this value doesn't matter, db call is mocked
|
||||
'daterange.search_offset': 10,
|
||||
})
|
||||
self._assert_times_match(data['start_timestamp'], self.early1)
|
||||
self._assert_times_match(data['end_timestamp'], self.early2)
|
||||
data = self.get_json('/meters/instance:m1.tiny/statistics',
|
||||
q=[{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': self.early2.isoformat()},
|
||||
{'field': 'resource_id',
|
||||
'value': 'resource-id'},
|
||||
{'field': 'search_offset',
|
||||
'value': 10}])
|
||||
self._assert_times_match(data['duration_start'], self.early1)
|
||||
self._assert_times_match(data['duration_end'], self.early2)
|
||||
|
@ -1,80 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Author: Steven Berler <steven.berler@dreamhost.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Test the _get_query_timestamps helper function.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import datetime
|
||||
|
||||
from ceilometer.api.controllers import v2 as api
|
||||
|
||||
|
||||
class DateRangeTest(unittest.TestCase):
|
||||
|
||||
def test_get_query_timestamps_none_specified(self):
|
||||
result = api.DateRange().to_dict()
|
||||
expected = {'start_timestamp': None,
|
||||
'end_timestamp': None,
|
||||
'query_start': None,
|
||||
'query_end': None,
|
||||
'search_offset': 0,
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
def test_get_query_timestamps_start(self):
|
||||
d = datetime.datetime(2012, 9, 20, 12, 13, 14)
|
||||
result = api.DateRange(start=d).to_dict()
|
||||
expected = {
|
||||
'start_timestamp': datetime.datetime(2012, 9, 20, 12, 13, 14),
|
||||
'end_timestamp': None,
|
||||
'query_start': datetime.datetime(2012, 9, 20, 12, 13, 14),
|
||||
'query_end': None,
|
||||
'search_offset': 0,
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
def test_get_query_timestamps_end(self):
|
||||
d = datetime.datetime(2012, 9, 20, 12, 13, 14)
|
||||
result = api.DateRange(end=d).to_dict()
|
||||
expected = {
|
||||
'end_timestamp': datetime.datetime(2012, 9, 20, 12, 13, 14),
|
||||
'start_timestamp': None,
|
||||
'query_end': datetime.datetime(2012, 9, 20, 12, 13, 14),
|
||||
'query_start': None,
|
||||
'search_offset': 0,
|
||||
}
|
||||
|
||||
assert result == expected
|
||||
|
||||
def test_get_query_timestamps_with_offset(self):
|
||||
result = api.DateRange(
|
||||
end=datetime.datetime(2012, 9, 20, 13, 24, 25),
|
||||
start=datetime.datetime(2012, 9, 20, 12, 13, 14),
|
||||
search_offset=20,
|
||||
).to_dict()
|
||||
expected = {
|
||||
'query_end': datetime.datetime(2012, 9, 20, 13, 44, 25),
|
||||
'query_start': datetime.datetime(2012, 9, 20, 11, 53, 14),
|
||||
'end_timestamp': datetime.datetime(2012, 9, 20, 13, 24, 25),
|
||||
'start_timestamp': datetime.datetime(2012, 9, 20, 12, 13, 14),
|
||||
'search_offset': 20,
|
||||
}
|
||||
|
||||
assert result == expected
|
@ -75,45 +75,70 @@ class TestListEvents(FunctionalTest):
|
||||
self.conn.record_metering_data(msg2)
|
||||
|
||||
def test_all(self):
|
||||
data = self.get_json('/resources')
|
||||
data = self.get_json('/meters/instance')
|
||||
self.assertEquals(2, len(data))
|
||||
|
||||
def test_empty_project(self):
|
||||
data = self.get_json('/projects/no-such-project/meters/instance')
|
||||
data = self.get_json('/meters/instance',
|
||||
q=[{'field': 'project_id',
|
||||
'value': 'no-such-project',
|
||||
}])
|
||||
self.assertEquals([], data)
|
||||
|
||||
def test_by_project(self):
|
||||
data = self.get_json('/projects/project1/meters/instance')
|
||||
data = self.get_json('/meters/instance',
|
||||
q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
}])
|
||||
self.assertEquals(1, len(data))
|
||||
|
||||
def test_empty_resource(self):
|
||||
data = self.get_json('/resources/no-such-resource/meters/instance')
|
||||
data = self.get_json('/meters/instance',
|
||||
q=[{'field': 'resource_id',
|
||||
'value': 'no-such-resource',
|
||||
}])
|
||||
self.assertEquals([], data)
|
||||
|
||||
def test_by_resource(self):
|
||||
data = self.get_json('/resources/resource-id/meters/instance')
|
||||
data = self.get_json('/meters/instance',
|
||||
q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
}])
|
||||
self.assertEquals(1, len(data))
|
||||
|
||||
def test_empty_source(self):
|
||||
data = self.get_json('/sources/no-such-source/meters/instance',
|
||||
expect_errors=True)
|
||||
self.assertEquals(data.status_int, 404)
|
||||
data = self.get_json('/meters/instance',
|
||||
q=[{'field': 'source',
|
||||
'value': 'no-such-source',
|
||||
}])
|
||||
self.assertEquals(0, len(data))
|
||||
|
||||
def test_by_source(self):
|
||||
data = self.get_json('/sources/test_source/meters/instance')
|
||||
data = self.get_json('/meters/instance',
|
||||
q=[{'field': 'source',
|
||||
'value': 'test_source',
|
||||
}])
|
||||
self.assertEquals(1, len(data))
|
||||
|
||||
def test_empty_user(self):
|
||||
data = self.get_json('/users/no-such-user/meters/instance')
|
||||
data = self.get_json('/meters/instance',
|
||||
q=[{'field': 'user_id',
|
||||
'value': 'no-such-user',
|
||||
}])
|
||||
self.assertEquals([], data)
|
||||
|
||||
def test_by_user(self):
|
||||
data = self.get_json('/users/user-id/meters/instance')
|
||||
data = self.get_json('/meters/instance',
|
||||
q=[{'field': 'user_id',
|
||||
'value': 'user-id',
|
||||
}])
|
||||
self.assertEquals(1, len(data))
|
||||
|
||||
def test_metadata(self):
|
||||
data = self.get_json('/resources/resource-id/meters/instance')
|
||||
self.assertEquals(1, len(data))
|
||||
data = self.get_json('/meters/instance',
|
||||
q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
}])
|
||||
sample = data[0]
|
||||
self.assert_('resource_metadata' in sample)
|
||||
self.assertEqual(
|
||||
|
@ -116,12 +116,16 @@ class TestListMeters(FunctionalTest):
|
||||
'meter.mine']))
|
||||
|
||||
def test_with_resource(self):
|
||||
data = self.get_json('/resources/resource-id/meters')
|
||||
data = self.get_json('/meters', q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
}])
|
||||
ids = set(r['name'] for r in data)
|
||||
self.assertEquals(set(['meter.test']), ids)
|
||||
|
||||
def test_with_source(self):
|
||||
data = self.get_json('/sources/test_source/meters')
|
||||
data = self.get_json('/meters', q=[{'field': 'source',
|
||||
'value': 'test_source',
|
||||
}])
|
||||
ids = set(r['resource_id'] for r in data)
|
||||
self.assertEquals(set(['resource-id',
|
||||
'resource-id2',
|
||||
@ -129,13 +133,22 @@ class TestListMeters(FunctionalTest):
|
||||
'resource-id4']), ids)
|
||||
|
||||
def test_with_source_non_existent(self):
|
||||
data = self.get_json('/sources/test_source_doesnt_exist/meters',
|
||||
expect_errors=True)
|
||||
self.assert_('No source test_source_doesnt_exist' in
|
||||
data.json['error_message'])
|
||||
data = self.get_json('/meters',
|
||||
q=[{'field': 'source',
|
||||
'value': 'test_source_doesnt_exist',
|
||||
}],
|
||||
)
|
||||
assert not data
|
||||
|
||||
def test_with_user(self):
|
||||
data = self.get_json('/users/user-id/meters')
|
||||
data = self.get_json('/meters',
|
||||
q=[{'field': 'user_id',
|
||||
'value': 'user-id',
|
||||
}],
|
||||
)
|
||||
|
||||
uids = set(r['user_id'] for r in data)
|
||||
self.assertEquals(set(['user-id']), uids)
|
||||
|
||||
nids = set(r['name'] for r in data)
|
||||
self.assertEquals(set(['meter.mine', 'meter.test']), nids)
|
||||
@ -144,14 +157,26 @@ class TestListMeters(FunctionalTest):
|
||||
self.assertEquals(set(['resource-id', 'resource-id2']), rids)
|
||||
|
||||
def test_with_user_non_existent(self):
|
||||
data = self.get_json('/users/user-id-foobar123/meters')
|
||||
data = self.get_json('/meters',
|
||||
q=[{'field': 'user_id',
|
||||
'value': 'user-id-foobar123',
|
||||
}],
|
||||
)
|
||||
self.assertEquals(data, [])
|
||||
|
||||
def test_with_project(self):
|
||||
data = self.get_json('/projects/project-id2/meters')
|
||||
data = self.get_json('/meters',
|
||||
q=[{'field': 'project_id',
|
||||
'value': 'project-id2',
|
||||
}],
|
||||
)
|
||||
ids = set(r['resource_id'] for r in data)
|
||||
self.assertEquals(set(['resource-id3', 'resource-id4']), ids)
|
||||
|
||||
def test_with_project_non_existent(self):
|
||||
data = self.get_json('/projects/jd-was-here/meters')
|
||||
data = self.get_json('/meters',
|
||||
q=[{'field': 'project_id',
|
||||
'value': 'jd-was-here',
|
||||
}],
|
||||
)
|
||||
self.assertEquals(data, [])
|
||||
|
@ -1,122 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Test listing users.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from ceilometer.collector import meter
|
||||
from ceilometer import counter
|
||||
|
||||
from ceilometer.openstack.common import cfg
|
||||
|
||||
from .base import FunctionalTest
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestListProjects(FunctionalTest):
|
||||
|
||||
def test_empty(self):
|
||||
data = self.get_json('/projects')
|
||||
self.assertEquals([], data)
|
||||
|
||||
def test_projects(self):
|
||||
counter1 = counter.Counter(
|
||||
'instance',
|
||||
'cumulative',
|
||||
'',
|
||||
1,
|
||||
'user-id',
|
||||
'project-id',
|
||||
'resource-id',
|
||||
timestamp=datetime.datetime(2012, 7, 2, 10, 40),
|
||||
resource_metadata={'display_name': 'test-server',
|
||||
'tag': 'self.counter',
|
||||
}
|
||||
)
|
||||
msg = meter.meter_message_from_counter(counter1,
|
||||
cfg.CONF.metering_secret,
|
||||
'test_source',
|
||||
)
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
counter2 = counter.Counter(
|
||||
'instance',
|
||||
'cumulative',
|
||||
'',
|
||||
1,
|
||||
'user-id2',
|
||||
'project-id2',
|
||||
'resource-id-alternate',
|
||||
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
|
||||
resource_metadata={'display_name': 'test-server',
|
||||
'tag': 'self.counter2',
|
||||
}
|
||||
)
|
||||
msg2 = meter.meter_message_from_counter(counter2,
|
||||
cfg.CONF.metering_secret,
|
||||
'test_source',
|
||||
)
|
||||
self.conn.record_metering_data(msg2)
|
||||
|
||||
data = self.get_json('/projects')
|
||||
self.assertEquals(['project-id', 'project-id2'], data)
|
||||
|
||||
def test_with_source(self):
|
||||
counter1 = counter.Counter(
|
||||
'instance',
|
||||
'cumulative',
|
||||
'',
|
||||
1,
|
||||
'user-id',
|
||||
'project-id',
|
||||
'resource-id',
|
||||
timestamp=datetime.datetime(2012, 7, 2, 10, 40),
|
||||
resource_metadata={'display_name': 'test-server',
|
||||
'tag': 'self.counter',
|
||||
}
|
||||
)
|
||||
msg = meter.meter_message_from_counter(counter1,
|
||||
cfg.CONF.metering_secret,
|
||||
'test_source',
|
||||
)
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
counter2 = counter.Counter(
|
||||
'instance',
|
||||
'cumulative',
|
||||
'',
|
||||
1,
|
||||
'user-id2',
|
||||
'project-id2',
|
||||
'resource-id-alternate',
|
||||
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
|
||||
resource_metadata={'display_name': 'test-server',
|
||||
'tag': 'self.counter2',
|
||||
}
|
||||
)
|
||||
msg2 = meter.meter_message_from_counter(counter2,
|
||||
cfg.CONF.metering_secret,
|
||||
'not-test',
|
||||
)
|
||||
self.conn.record_metering_data(msg2)
|
||||
|
||||
data = self.get_json('/sources/test_source/projects')
|
||||
self.assertEquals(['project-id'], data)
|
@ -119,7 +119,9 @@ class TestListResources(FunctionalTest):
|
||||
)
|
||||
self.conn.record_metering_data(msg2)
|
||||
|
||||
data = self.get_json('/sources/test_list_resources/resources')
|
||||
data = self.get_json('/resources', q=[{'field': 'source',
|
||||
'value': 'test_list_resources',
|
||||
}])
|
||||
ids = [r['resource_id'] for r in data]
|
||||
self.assertEquals(['resource-id'], ids)
|
||||
|
||||
@ -162,7 +164,9 @@ class TestListResources(FunctionalTest):
|
||||
)
|
||||
self.conn.record_metering_data(msg2)
|
||||
|
||||
data = self.get_json('/users/user-id/resources')
|
||||
data = self.get_json('/resources', q=[{'field': 'user_id',
|
||||
'value': 'user-id',
|
||||
}])
|
||||
ids = [r['resource_id'] for r in data]
|
||||
self.assertEquals(['resource-id'], ids)
|
||||
|
||||
@ -205,7 +209,9 @@ class TestListResources(FunctionalTest):
|
||||
)
|
||||
self.conn.record_metering_data(msg2)
|
||||
|
||||
data = self.get_json('/projects/project-id/resources')
|
||||
data = self.get_json('/resources', q=[{'field': 'project_id',
|
||||
'value': 'project-id',
|
||||
}])
|
||||
ids = [r['resource_id'] for r in data]
|
||||
self.assertEquals(['resource-id'], ids)
|
||||
|
||||
|
@ -1,47 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 Julien Danjou
|
||||
#
|
||||
# Author: Julien Danjou <julien@danjou.info>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Test listing users.
|
||||
"""
|
||||
|
||||
from .base import FunctionalTest
|
||||
|
||||
|
||||
class TestListSource(FunctionalTest):
|
||||
|
||||
def test_all(self):
|
||||
ydata = self.get_json('/sources')
|
||||
self.assertEqual(len(ydata), 1)
|
||||
source = ydata[0]
|
||||
self.assertEqual(source['name'], 'test_source')
|
||||
|
||||
def test_source(self):
|
||||
ydata = self.get_json('/sources/test_source')
|
||||
self.assert_("data" in ydata)
|
||||
self.assert_("somekey" in ydata['data'])
|
||||
self.assertEqual(ydata['data']["somekey"], '666')
|
||||
|
||||
def test_unknownsource(self):
|
||||
ydata = self.get_json(
|
||||
'/sources/test_source_that_does_not_exist',
|
||||
expect_errors=True)
|
||||
print 'GOT:', ydata
|
||||
self.assertEqual(ydata.status_int, 404)
|
||||
self.assert_(
|
||||
"No source test_source_that_does_not_exist" in
|
||||
ydata.json['error_message']
|
||||
)
|
@ -1,123 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Test listing users.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from ceilometer.collector import meter
|
||||
from ceilometer import counter
|
||||
|
||||
from ceilometer.openstack.common import cfg
|
||||
|
||||
from .base import FunctionalTest
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestListUsers(FunctionalTest):
|
||||
|
||||
SOURCE_DATA = {'test_list_users': {}}
|
||||
|
||||
def test_empty(self):
|
||||
data = self.get_json('/users')
|
||||
self.assertEquals([], data)
|
||||
|
||||
def test_users(self):
|
||||
counter1 = counter.Counter(
|
||||
'instance',
|
||||
'cumulative',
|
||||
'',
|
||||
1,
|
||||
'user-id',
|
||||
'project-id',
|
||||
'resource-id',
|
||||
timestamp=datetime.datetime(2012, 7, 2, 10, 40),
|
||||
resource_metadata={'display_name': 'test-server',
|
||||
'tag': 'self.counter'}
|
||||
)
|
||||
msg = meter.meter_message_from_counter(counter1,
|
||||
cfg.CONF.metering_secret,
|
||||
'test_list_users',
|
||||
)
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
counter2 = counter.Counter(
|
||||
'instance',
|
||||
'cumulative',
|
||||
'',
|
||||
1,
|
||||
'user-id2',
|
||||
'project-id',
|
||||
'resource-id-alternate',
|
||||
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
|
||||
resource_metadata={'display_name': 'test-server',
|
||||
'tag': 'self.counter2',
|
||||
}
|
||||
)
|
||||
msg2 = meter.meter_message_from_counter(counter2,
|
||||
cfg.CONF.metering_secret,
|
||||
'test_list_users',
|
||||
)
|
||||
self.conn.record_metering_data(msg2)
|
||||
|
||||
data = self.get_json('/users')
|
||||
self.assertEquals(['user-id', 'user-id2'], data)
|
||||
|
||||
def test_with_source(self):
|
||||
counter1 = counter.Counter(
|
||||
'instance',
|
||||
'cumulative',
|
||||
'',
|
||||
1,
|
||||
'user-id',
|
||||
'project-id',
|
||||
'resource-id',
|
||||
timestamp=datetime.datetime(2012, 7, 2, 10, 40),
|
||||
resource_metadata={'display_name': 'test-server',
|
||||
'tag': 'self.counter',
|
||||
}
|
||||
)
|
||||
msg = meter.meter_message_from_counter(counter1,
|
||||
cfg.CONF.metering_secret,
|
||||
'test_list_users',
|
||||
)
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
counter2 = counter.Counter(
|
||||
'instance',
|
||||
'cumulative',
|
||||
'',
|
||||
1,
|
||||
'user-id2',
|
||||
'project-id',
|
||||
'resource-id-alternate',
|
||||
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
|
||||
resource_metadata={'display_name': 'test-server',
|
||||
'tag': 'self.counter2',
|
||||
}
|
||||
)
|
||||
msg2 = meter.meter_message_from_counter(counter2,
|
||||
cfg.CONF.metering_secret,
|
||||
'not-test',
|
||||
)
|
||||
self.conn.record_metering_data(msg2)
|
||||
|
||||
data = self.get_json('/sources/test_list_users/users')
|
||||
self.assertEquals(['user-id'], data)
|
@ -31,7 +31,7 @@ from .base import FunctionalTest
|
||||
|
||||
class TestMaxProjectVolume(FunctionalTest):
|
||||
|
||||
PATH = '/projects/project1/meters/volume.size/volume/max'
|
||||
PATH = '/meters/volume.size/statistics'
|
||||
|
||||
def setUp(self):
|
||||
super(TestMaxProjectVolume, self).setUp()
|
||||
@ -60,42 +60,72 @@ class TestMaxProjectVolume(FunctionalTest):
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
def test_no_time_bounds(self):
|
||||
data = self.get_json(self.PATH)
|
||||
expected = {'volume': 7}
|
||||
self.assertEqual(data, expected)
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
}])
|
||||
self.assertEqual(data['max'], 7)
|
||||
self.assertEqual(data['count'], 3)
|
||||
|
||||
def test_start_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T11:30:00'})
|
||||
expected = {'volume': 7}
|
||||
self.assertEqual(data, expected)
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
])
|
||||
self.assertEqual(data['max'], 7)
|
||||
self.assertEqual(data['count'], 2)
|
||||
|
||||
def test_start_timestamp_after(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T12:34:00'})
|
||||
expected = {'volume': None}
|
||||
self.assertEqual(data, expected)
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T12:34:00',
|
||||
},
|
||||
])
|
||||
self.assertEqual(data['max'], None)
|
||||
self.assertEqual(data['count'], 0)
|
||||
|
||||
def test_end_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.end': '2012-09-25T11:30:00'})
|
||||
expected = {'volume': 5}
|
||||
self.assertEqual(data, expected)
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
])
|
||||
self.assertEqual(data['max'], 5)
|
||||
self.assertEqual(data['count'], 1)
|
||||
|
||||
def test_end_timestamp_before(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.end': '2012-09-25T09:54:00'})
|
||||
expected = {'volume': None}
|
||||
self.assertEqual(data, expected)
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T09:54:00',
|
||||
},
|
||||
])
|
||||
self.assertEqual(data['max'], None)
|
||||
self.assertEqual(data['count'], 0)
|
||||
|
||||
def test_start_end_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T11:30:00',
|
||||
'daterange.end': '2012-09-25T11:32:00'})
|
||||
expected = {'volume': 6}
|
||||
self.assertEqual(data, expected)
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T11:32:00',
|
||||
},
|
||||
])
|
||||
self.assertEqual(data['max'], 6)
|
||||
self.assertEqual(data['count'], 1)
|
||||
|
@ -30,7 +30,7 @@ from ceilometer.tests.db import require_map_reduce
|
||||
|
||||
class TestMaxResourceVolume(FunctionalTest):
|
||||
|
||||
PATH = '/resources/resource-id/meters/volume.size/volume/max'
|
||||
PATH = '/meters/volume.size/statistics'
|
||||
|
||||
def setUp(self):
|
||||
super(TestMaxResourceVolume, self).setUp()
|
||||
@ -59,42 +59,72 @@ class TestMaxResourceVolume(FunctionalTest):
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
def test_no_time_bounds(self):
|
||||
data = self.get_json(self.PATH)
|
||||
expected = {'volume': 7}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
}])
|
||||
assert data['max'] == 7
|
||||
assert data['count'] == 3
|
||||
|
||||
def test_start_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T11:30:00'})
|
||||
expected = {'volume': 7}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
])
|
||||
assert data['max'] == 7
|
||||
assert data['count'] == 2
|
||||
|
||||
def test_start_timestamp_after(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T12:34:00'})
|
||||
expected = {'volume': None}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T12:34:00',
|
||||
},
|
||||
])
|
||||
assert data['max'] is None
|
||||
assert data['count'] == 0
|
||||
|
||||
def test_end_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.end': '2012-09-25T11:30:00'})
|
||||
expected = {'volume': 5}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
])
|
||||
assert data['max'] == 5
|
||||
assert data['count'] == 1
|
||||
|
||||
def test_end_timestamp_before(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.end': '2012-09-25T09:54:00'})
|
||||
expected = {'volume': None}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T09:54:00',
|
||||
},
|
||||
])
|
||||
assert data['max'] is None
|
||||
assert data['count'] == 0
|
||||
|
||||
def test_start_end_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T11:30:00',
|
||||
'daterange.end': '2012-09-25T11:32:00'})
|
||||
expected = {'volume': 6}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T11:32:00',
|
||||
},
|
||||
])
|
||||
assert data['max'] == 6
|
||||
assert data['count'] == 1
|
||||
|
118
tests/api/v2/test_statistics.py
Normal file
118
tests/api/v2/test_statistics.py
Normal file
@ -0,0 +1,118 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
|
||||
#
|
||||
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Test listing raw events.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import unittest
|
||||
|
||||
from ceilometer.api.controllers import v2
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestStatisticsDuration(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestStatisticsDuration, self).setUp()
|
||||
|
||||
# Create events relative to the range and pretend
|
||||
# that the intervening events exist.
|
||||
|
||||
self.early1 = datetime.datetime(2012, 8, 27, 7, 0)
|
||||
self.early2 = datetime.datetime(2012, 8, 27, 17, 0)
|
||||
|
||||
self.start = datetime.datetime(2012, 8, 28, 0, 0)
|
||||
|
||||
self.middle1 = datetime.datetime(2012, 8, 28, 8, 0)
|
||||
self.middle2 = datetime.datetime(2012, 8, 28, 18, 0)
|
||||
|
||||
self.end = datetime.datetime(2012, 8, 28, 23, 59)
|
||||
|
||||
self.late1 = datetime.datetime(2012, 8, 29, 9, 0)
|
||||
self.late2 = datetime.datetime(2012, 8, 29, 19, 0)
|
||||
|
||||
def test_nulls(self):
|
||||
s = v2.Statistics(duration_start=None,
|
||||
duration_end=None,
|
||||
start_timestamp=None,
|
||||
end_timestamp=None,
|
||||
)
|
||||
assert s.duration_start is None
|
||||
assert s.duration_end is None
|
||||
assert s.duration is None
|
||||
|
||||
def test_overlap_range_start(self):
|
||||
s = v2.Statistics(duration_start=self.early1,
|
||||
duration_end=self.middle1,
|
||||
start_timestamp=self.start,
|
||||
end_timestamp=self.end,
|
||||
)
|
||||
assert s.duration_start == self.start
|
||||
assert s.duration_end == self.middle1
|
||||
assert s.duration == 8 * 60
|
||||
|
||||
def test_within_range(self):
|
||||
s = v2.Statistics(duration_start=self.middle1,
|
||||
duration_end=self.middle2,
|
||||
start_timestamp=self.start,
|
||||
end_timestamp=self.end,
|
||||
)
|
||||
assert s.duration_start == self.middle1
|
||||
assert s.duration_end == self.middle2
|
||||
assert s.duration == 10 * 60
|
||||
|
||||
def test_within_range_zero_duration(self):
|
||||
s = v2.Statistics(duration_start=self.middle1,
|
||||
duration_end=self.middle1,
|
||||
start_timestamp=self.start,
|
||||
end_timestamp=self.end,
|
||||
)
|
||||
assert s.duration_start == self.middle1
|
||||
assert s.duration_end == self.middle1
|
||||
assert s.duration == 0
|
||||
|
||||
def test_overlap_range_end(self):
|
||||
s = v2.Statistics(duration_start=self.middle2,
|
||||
duration_end=self.late1,
|
||||
start_timestamp=self.start,
|
||||
end_timestamp=self.end,
|
||||
)
|
||||
assert s.duration_start == self.middle2
|
||||
assert s.duration_end == self.end
|
||||
assert s.duration == (6 * 60) - 1
|
||||
|
||||
def test_after_range(self):
|
||||
s = v2.Statistics(duration_start=self.late1,
|
||||
duration_end=self.late2,
|
||||
start_timestamp=self.start,
|
||||
end_timestamp=self.end,
|
||||
)
|
||||
assert s.duration_start is None
|
||||
assert s.duration_end is None
|
||||
assert s.duration is None
|
||||
|
||||
def test_without_timestamp(self):
|
||||
s = v2.Statistics(duration_start=self.late1,
|
||||
duration_end=self.late2,
|
||||
start_timestamp=None,
|
||||
end_timestamp=None,
|
||||
)
|
||||
assert s.duration_start == self.late1
|
||||
assert s.duration_end == self.late2
|
@ -30,7 +30,7 @@ from ceilometer.tests.db import require_map_reduce
|
||||
|
||||
class TestSumProjectVolume(FunctionalTest):
|
||||
|
||||
PATH = '/projects/project1/meters/volume.size/volume/sum'
|
||||
PATH = '/meters/volume.size/statistics'
|
||||
|
||||
def setUp(self):
|
||||
super(TestSumProjectVolume, self).setUp()
|
||||
@ -59,42 +59,74 @@ class TestSumProjectVolume(FunctionalTest):
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
def test_no_time_bounds(self):
|
||||
data = self.get_json(self.PATH)
|
||||
expected = {'volume': 5 + 6 + 7}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
}])
|
||||
expected = 5 + 6 + 7
|
||||
assert data['sum'] == expected
|
||||
assert data['count'] == 3
|
||||
|
||||
def test_start_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T11:30:00'})
|
||||
expected = {'volume': 6 + 7}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
])
|
||||
expected = 6 + 7
|
||||
assert data['sum'] == expected
|
||||
assert data['count'] == 2
|
||||
|
||||
def test_start_timestamp_after(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T12:34:00'})
|
||||
expected = {'volume': None}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T12:34:00',
|
||||
},
|
||||
])
|
||||
assert data['sum'] is None
|
||||
assert data['count'] == 0
|
||||
|
||||
def test_end_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.end': '2012-09-25T11:30:00'})
|
||||
expected = {'volume': 5}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
])
|
||||
assert data['sum'] == 5
|
||||
assert data['count'] == 1
|
||||
|
||||
def test_end_timestamp_before(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.end': '2012-09-25T09:54:00'})
|
||||
expected = {'volume': None}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T09:54:00',
|
||||
},
|
||||
])
|
||||
assert data['sum'] is None
|
||||
assert data['count'] == 0
|
||||
|
||||
def test_start_end_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T11:30:00',
|
||||
'daterange.end': '2012-09-25T11:32:00'})
|
||||
expected = {'volume': 6}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'project_id',
|
||||
'value': 'project1',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T11:32:00',
|
||||
},
|
||||
])
|
||||
assert data['sum'] == 6
|
||||
assert data['count'] == 1
|
||||
|
@ -30,7 +30,7 @@ from ceilometer.tests.db import require_map_reduce
|
||||
|
||||
class TestSumResourceVolume(FunctionalTest):
|
||||
|
||||
PATH = '/resources/resource-id/meters/volume.size/volume/sum'
|
||||
PATH = '/meters/volume.size/statistics'
|
||||
|
||||
def setUp(self):
|
||||
super(TestSumResourceVolume, self).setUp()
|
||||
@ -59,42 +59,67 @@ class TestSumResourceVolume(FunctionalTest):
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
def test_no_time_bounds(self):
|
||||
data = self.get_json(self.PATH)
|
||||
expected = {'volume': 5 + 6 + 7}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
}])
|
||||
assert data['sum'] == 5 + 6 + 7
|
||||
assert data['count'] == 3
|
||||
|
||||
def test_start_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T11:30:00'})
|
||||
expected = {'volume': 6 + 7}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
}])
|
||||
assert data['sum'] == 6 + 7
|
||||
assert data['count'] == 2
|
||||
|
||||
def test_start_timestamp_after(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T12:34:00'})
|
||||
expected = {'volume': None}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T12:34:00',
|
||||
}])
|
||||
assert data['sum'] is None
|
||||
assert data['count'] == 0
|
||||
|
||||
def test_end_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.end': '2012-09-25T11:30:00'})
|
||||
expected = {'volume': 5}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
}])
|
||||
assert data['sum'] == 5
|
||||
assert data['count'] == 1
|
||||
|
||||
def test_end_timestamp_before(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.end': '2012-09-25T09:54:00'})
|
||||
expected = {'volume': None}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'le',
|
||||
'value': '2012-09-25T09:54:00',
|
||||
}])
|
||||
assert data['sum'] is None
|
||||
assert data['count'] == 0
|
||||
|
||||
def test_start_end_timestamp(self):
|
||||
data = self.get_json(
|
||||
self.PATH,
|
||||
extra_params={'daterange.start': '2012-09-25T11:30:00',
|
||||
'daterange.end': '2012-09-25T11:32:00'})
|
||||
expected = {'volume': 6}
|
||||
assert data == expected
|
||||
data = self.get_json(self.PATH, q=[{'field': 'resource_id',
|
||||
'value': 'resource-id',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'ge',
|
||||
'value': '2012-09-25T11:30:00',
|
||||
},
|
||||
{'field': 'timestamp',
|
||||
'op': 'lt',
|
||||
'value': '2012-09-25T11:32:00',
|
||||
}])
|
||||
assert data['sum'] == 6
|
||||
assert data['count'] == 1
|
||||
|
@ -526,3 +526,89 @@ class TestGetEventInterval(MongoDBEngineTestBase):
|
||||
s, e = self.conn.get_event_interval(self._filter)
|
||||
assert s is None
|
||||
assert e is None
|
||||
|
||||
|
||||
class StatisticsTest(MongoDBEngineTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(StatisticsTest, self).setUp()
|
||||
require_map_reduce(self.conn)
|
||||
self.counters = []
|
||||
for i in range(3):
|
||||
c = counter.Counter(
|
||||
'volume.size',
|
||||
'gauge',
|
||||
'GiB',
|
||||
5 + i,
|
||||
'user-id',
|
||||
'project1',
|
||||
'resource-id',
|
||||
timestamp=datetime.datetime(2012, 9, 25, 10 + i, 30 + i),
|
||||
resource_metadata={'display_name': 'test-volume',
|
||||
'tag': 'self.counter',
|
||||
}
|
||||
)
|
||||
self.counters.append(c)
|
||||
msg = meter.meter_message_from_counter(c,
|
||||
secret='not-so-secret',
|
||||
source='test',
|
||||
)
|
||||
self.conn.record_metering_data(msg)
|
||||
for i in range(3):
|
||||
c = counter.Counter(
|
||||
'volume.size',
|
||||
'gauge',
|
||||
'GiB',
|
||||
8 + i,
|
||||
'user-5',
|
||||
'project2',
|
||||
'resource-6',
|
||||
timestamp=datetime.datetime(2012, 9, 25, 10 + i, 30 + i),
|
||||
resource_metadata={'display_name': 'test-volume',
|
||||
'tag': 'self.counter',
|
||||
}
|
||||
)
|
||||
self.counters.append(c)
|
||||
msg = meter.meter_message_from_counter(c,
|
||||
secret='not-so-secret',
|
||||
source='test',
|
||||
)
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
def test_by_user(self):
|
||||
f = storage.EventFilter(
|
||||
user='user-5',
|
||||
meter='volume.size',
|
||||
)
|
||||
results = self.conn.get_meter_statistics(f)
|
||||
assert results['count'] == 3
|
||||
assert results['min'] == 8
|
||||
assert results['max'] == 10
|
||||
assert results['sum'] == 27
|
||||
assert results['avg'] == 9
|
||||
|
||||
def test_by_project(self):
|
||||
f = storage.EventFilter(
|
||||
meter='volume.size',
|
||||
resource='resource-id',
|
||||
start='2012-09-25T11:30:00',
|
||||
end='2012-09-25T11:32:00',
|
||||
)
|
||||
results = self.conn.get_meter_statistics(f)
|
||||
assert results['count'] == 1
|
||||
assert results['min'] == 6
|
||||
assert results['max'] == 6
|
||||
assert results['sum'] == 6
|
||||
assert results['avg'] == 6
|
||||
|
||||
def test_one_resource(self):
|
||||
f = storage.EventFilter(
|
||||
user='user-id',
|
||||
meter='volume.size',
|
||||
)
|
||||
results = self.conn.get_meter_statistics(f)
|
||||
assert results['count'] == 3
|
||||
assert results['min'] == 5
|
||||
assert results['max'] == 7
|
||||
assert results['sum'] == 18
|
||||
assert results['avg'] == 6
|
||||
|
@ -734,6 +734,91 @@ class MaxResourceTest(SQLAlchemyEngineSubBase):
|
||||
assert results == expected
|
||||
|
||||
|
||||
class StatisticsTest(SQLAlchemyEngineSubBase):
|
||||
|
||||
def setUp(self):
|
||||
super(StatisticsTest, self).setUp()
|
||||
self.counters = []
|
||||
for i in range(3):
|
||||
c = counter.Counter(
|
||||
'volume.size',
|
||||
'gauge',
|
||||
'GiB',
|
||||
5 + i,
|
||||
'user-id',
|
||||
'project1',
|
||||
'resource-id',
|
||||
timestamp=datetime.datetime(2012, 9, 25, 10 + i, 30 + i),
|
||||
resource_metadata={'display_name': 'test-volume',
|
||||
'tag': 'self.counter',
|
||||
}
|
||||
)
|
||||
self.counters.append(c)
|
||||
msg = meter.meter_message_from_counter(c,
|
||||
cfg.CONF.metering_secret,
|
||||
'source1',
|
||||
)
|
||||
self.conn.record_metering_data(msg)
|
||||
for i in range(3):
|
||||
c = counter.Counter(
|
||||
'volume.size',
|
||||
'gauge',
|
||||
'GiB',
|
||||
8 + i,
|
||||
'user-5',
|
||||
'project2',
|
||||
'resource-6',
|
||||
timestamp=datetime.datetime(2012, 9, 25, 10 + i, 30 + i),
|
||||
resource_metadata={'display_name': 'test-volume',
|
||||
'tag': 'self.counter',
|
||||
}
|
||||
)
|
||||
self.counters.append(c)
|
||||
msg = meter.meter_message_from_counter(c,
|
||||
cfg.CONF.metering_secret,
|
||||
'source1',
|
||||
)
|
||||
self.conn.record_metering_data(msg)
|
||||
|
||||
def test_by_user(self):
|
||||
f = storage.EventFilter(
|
||||
user='user-5',
|
||||
meter='volume.size',
|
||||
)
|
||||
results = self.conn.get_meter_statistics(f)
|
||||
assert results['count'] == 3
|
||||
assert results['min'] == 8
|
||||
assert results['max'] == 10
|
||||
assert results['sum'] == 27
|
||||
assert results['avg'] == 9
|
||||
|
||||
def test_by_project(self):
|
||||
f = storage.EventFilter(
|
||||
meter='volume.size',
|
||||
resource='resource-id',
|
||||
start='2012-09-25T11:30:00',
|
||||
end='2012-09-25T11:32:00',
|
||||
)
|
||||
results = self.conn.get_meter_statistics(f)
|
||||
assert results['count'] == 1
|
||||
assert results['min'] == 6
|
||||
assert results['max'] == 6
|
||||
assert results['sum'] == 6
|
||||
assert results['avg'] == 6
|
||||
|
||||
def test_one_resource(self):
|
||||
f = storage.EventFilter(
|
||||
user='user-id',
|
||||
meter='volume.size',
|
||||
)
|
||||
results = self.conn.get_meter_statistics(f)
|
||||
assert results['count'] == 3
|
||||
assert results['min'] == 5
|
||||
assert results['max'] == 7
|
||||
assert results['sum'] == 18
|
||||
assert results['avg'] == 6
|
||||
|
||||
|
||||
def test_model_table_args():
|
||||
cfg.CONF.database_connection = 'mysql://localhost'
|
||||
assert table_args()
|
||||
|
@ -8,6 +8,13 @@ then
|
||||
project_name=demo
|
||||
fi
|
||||
|
||||
if [ -z "$OS_USERNAME" ]
|
||||
then
|
||||
user=demo
|
||||
else
|
||||
user=$OS_USERNAME
|
||||
fi
|
||||
|
||||
# Convert a possible project name to an id, if we have
|
||||
# keystone installed.
|
||||
if which keystone >/dev/null
|
||||
@ -41,7 +48,7 @@ late2="2012-08-31T20:00:00"
|
||||
|
||||
mkdata() {
|
||||
${bindir}/make_test_data.py --project "$project" \
|
||||
--start "$2" --end "$3" \
|
||||
--user "$user" --start "$2" --end "$3" \
|
||||
"$1" instance:m1.tiny 1
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user