Doug Hellmann e14b326309 Update V2 API documentation
This changeset adds a Sphinx extension for auto-generating
much of the documentation for a Pecan/WSME API from the
comments and docstrings in the source code. It also updates
the V2 API to include more documentation for API endpoints
and the data types used in the API, as well as sample data
for generating the JSON and XML examples in the output
documentation.

Change-Id: I1bde7805550aa86e9b64495b5c6034ec328479e5
Signed-off-by: Doug Hellmann <doug.hellmann@dreamhost.com>
2013-02-04 17:34:01 -05:00

510 lines
17 KiB
Python

# -*- encoding: utf-8 -*-
#
# 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
# 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.
"""Version 2 of the API.
"""
# [GET ] / -- information about this version of the API
#
# [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
#
import datetime
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, Enum
from ceilometer.openstack.common import log as logging
from ceilometer.openstack.common import timeutils
from ceilometer import storage
LOG = logging.getLogger(__name__)
operation_kind = Enum(str, 'lt', 'le', 'eq', 'ne', 'ge', 'gt')
class Query(Base):
"""Query filter.
"""
_op = None # provide a default
def get_op(self):
return self._op or 'eq'
def set_op(self, value):
self._op = value
field = text
"The name of the field to test"
#op = wsme.wsattr(operation_kind, default='eq')
# this ^ doesn't seem to work.
op = wsme.wsproperty(operation_kind, get_op, set_op)
"The comparison operator. Defaults to 'eq'."
value = text
"The value to compare against the stored data"
def __repr__(self):
# for logging calls
return '<Query %r %s %r>' % (self.field, self.op, self.value)
@classmethod
def sample(cls):
return cls(field='resource_id',
op='eq',
value='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
)
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:
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:
trans[translation.get(i.field, i.field)] = i.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):
"""Return flattened resource metadata without nested structures
and with all values converted to unicode strings.
"""
return dict((k, unicode(v))
for k, v in metadata.iteritems()
if type(v) not in set([list, dict, set]))
class Sample(Base):
"""A single measurement for a given meter and resource.
"""
source = text
"An identity source ID"
counter_name = text
"The name of the meter"
# FIXME(dhellmann): Make this meter_name?
counter_type = text
"The type of the meter (see :ref:`measurements`)"
# FIXME(dhellmann): Make this meter_type?
counter_unit = text
"The unit of measure for the value in counter_volume"
# FIXME(dhellmann): Make this meter_unit?
counter_volume = float
"The actual measured value"
user_id = text
"The ID of the user who last triggered an update to the resource"
project_id = text
"The ID of the project or tenant that owns the resource"
resource_id = text
"The ID of the :class:`Resource` for which the measurements are taken"
timestamp = datetime.datetime
"UTC date and time when the measurement was made"
resource_metadata = {text: text}
"Arbitrary metadata associated with the resource"
message_id = text
"A unique identifier for the sample"
def __init__(self, counter_volume=None, resource_metadata={}, **kwds):
if counter_volume is not None:
counter_volume = float(counter_volume)
resource_metadata = _flatten_metadata(resource_metadata)
super(Sample, self).__init__(counter_volume=counter_volume,
resource_metadata=resource_metadata,
**kwds)
@classmethod
def sample(cls):
return cls(source='openstack',
counter_name='instance',
counter_type='gauge',
counter_unit='instance',
counter_volume=1,
resource_id='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
project_id='35b17138-b364-4e6a-a131-8f3099c5be68',
user_id='efd87807-12d2-4b38-9c70-5f5c2ac427ff',
timestamp=datetime.datetime.utcnow(),
metadata={'name1': 'value1',
'name2': 'value2'},
message_id='5460acce-4fd6-480d-ab18-9735ec7b1996',
)
class Statistics(Base):
"""Computed statistics for a query.
"""
min = float
"The minimum volume seen in the data"
max = float
"The maximum volume seen in the data"
avg = float
"The average of all of the volume values seen in the data"
sum = float
"The total of all of the volume values seen in the data"
count = int
"The number of samples seen"
duration = float
"The difference, in minutes, between the oldest and newest timestamp"
duration_start = datetime.datetime
"UTC date and time of the earliest timestamp, or the query start time"
duration_end = datetime.datetime
"UTC date and time of the oldest timestamp, or the query end time"
def __init__(self, start_timestamp=None, end_timestamp=None, **kwds):
super(Statistics, self).__init__(**kwds)
self._update_duration(start_timestamp, end_timestamp)
def _update_duration(self, start_timestamp, end_timestamp):
# "Clamp" the timestamps we return to the original time
# range, excluding the offset.
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 (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.
#
# If the min > max after clamping then we know the
# timestamps on the events fell outside of the time
# range we care about for the query, so treat them as
# "invalid."
#
# If the timestamps are invalid, return None as a
# sentinal indicating that there is something "funny"
# about the range.
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 = self.duration_end - self.duration_start
self.duration = (diff.seconds + (diff.days * 24 * 60 ** 2)) / 60
# FIXME(dhellmann): Shouldn't this value be returned in
# seconds, or something even smaller?
else:
self.duration_start = self.duration_end = self.duration = None
@classmethod
def sample(cls):
return cls(min=1,
max=9,
avg=4.5,
sum=45,
count=10,
duration_start=datetime.datetime(2013, 1, 4, 16, 42),
duration_end=datetime.datetime(2013, 1, 4, 16, 47),
)
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 sample data for the meter.
:param q: Filter rules for the data to be returned.
"""
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):
"""One category of measurements.
"""
name = text
"The unique name for the meter"
# FIXME(dhellmann): Make this an enum?
type = text
"The meter type (see :ref:`measurements`)"
unit = text
"The unit of measure"
resource_id = text
"The ID of the :class:`Resource` for which the measurements are taken"
project_id = text
"The ID of the project or tenant that owns the resource"
user_id = text
"The ID of the user who last triggered an update to the resource"
@classmethod
def sample(cls):
return cls(name='instance',
type='gauge',
unit='instance',
resource_id='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
project_id='35b17138-b364-4e6a-a131-8f3099c5be68',
user_id='efd87807-12d2-4b38-9c70-5f5c2ac427ff',
)
class MetersController(RestController):
"""Works on meters."""
@pecan.expose()
def _lookup(self, meter_id, *remainder):
return MeterController(meter_id), remainder
@wsme_pecan.wsexpose([Meter], [Query])
def get_all(self, q=[]):
"""Return all known meters, based on the data recorded so far.
:param q: Filter rules for the meters to be returned.
"""
kwargs = _query_to_kwargs(q, request.storage_conn.get_meters)
return [Meter(**m)
for m in request.storage_conn.get_meters(**kwargs)]
class Resource(Base):
"""An externally defined object for which samples have been received.
"""
resource_id = text
"The unique identifier for the resource"
project_id = text
"The ID of the owning project or tenant"
user_id = text
"The ID of the user who created the resource or updated it last"
timestamp = datetime.datetime
"UTC date and time of the last update to any meter for the resource"
metadata = {text: text}
"Arbitrary metadata associated with the resource"
def __init__(self, metadata={}, **kwds):
metadata = _flatten_metadata(metadata)
super(Resource, self).__init__(metadata=metadata, **kwds)
@classmethod
def sample(cls):
return cls(resource_id='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36',
project_id='35b17138-b364-4e6a-a131-8f3099c5be68',
user_id='efd87807-12d2-4b38-9c70-5f5c2ac427ff',
timestamp=datetime.datetime.utcnow(),
metadata={'name1': 'value1',
'name2': 'value2'},
)
class ResourcesController(RestController):
"""Works on resources."""
@wsme_pecan.wsexpose(Resource, unicode)
def get_one(self, resource_id):
"""Retrieve details about one resource.
:param resource_id: The UUID of the resource.
"""
r = request.storage_conn.get_resources(resource=resource_id)[0]
return Resource(**r)
@wsme_pecan.wsexpose([Resource], [Query])
def get_all(self, q=[]):
"""Retrieve definitions of all of the resources.
:param q: Filter rules for the resources to be returned.
"""
kwargs = _query_to_kwargs(q, request.storage_conn.get_resources)
resources = [
Resource(**r)
for r in request.storage_conn.get_resources(**kwargs)]
return resources
class V2Controller(object):
"""Version 2 API controller root."""
resources = ResourcesController()
meters = MetersController()