From 4ca9550cfb743e02b78592581ede5f56385544c1 Mon Sep 17 00:00:00 2001 From: Eoghan Glynn Date: Tue, 25 Feb 2014 11:26:28 +0000 Subject: [PATCH] Add simple capabilities API Precursor-to: BP selectable-aggregates The per-deployment variation in API capabilities is effectively determined by the lack of strict feature parity across storage drivers. Currently we require that callers either know a priori which features are provided and which not, or else to probe the API and handle the NotImplementedErrors. With the advent of more complexity in the API (complex queries, selectable aggregates etc.) it would be useful to allow a more direct interrogation of capabilities in the current deployment. We expose the current set of capabilities in the form of a simple nested dictionary, e.g.: { 'meters': {'pagination': True, 'query': {'simple': True, 'complex': False}}, 'resources': {'pagination': False, 'query': {'simple': True, 'metadata': True, 'complex': False}}, 'samples': {'pagination': True, 'groupby': True, 'query': {'simple': True, 'complex': True}}, 'statistics': {'pagination': True, 'groupby': True, 'query': {'simple': True, 'metadata': True, 'complex': False}, 'aggregation': {'standard': True, 'selectable': { 'max': True, 'min': True, 'sum': True, 'avg': True, 'count': True, 'stddev': True, 'cardinality': True, 'quartile': False}}}, 'alarms': {'query': {'simple': True, 'complex': True}, 'history': {'query': {'simple': True, 'complex': True}}}, 'events': {'query': {'simple': True}}, } which is flattened in the usual way (using a similar approach as used for resource metadata). Leaf nodes of the capabilities representation are boolean, with absence being implicitly False. Change-Id: I8de9e153d7641d5d38ca9b79bab66887257c3010 --- ceilometer/api/controllers/v2.py | 67 ++++++++++++++++++++++++++- ceilometer/storage/base.py | 44 ++++++++++++++++++ ceilometer/storage/impl_db2.py | 22 +++++++++ ceilometer/storage/impl_hbase.py | 16 +++++++ ceilometer/storage/impl_mongodb.py | 23 +++++++++ ceilometer/storage/impl_sqlalchemy.py | 25 ++++++++++ ceilometer/utils.py | 13 ++++++ 7 files changed, 209 insertions(+), 1 deletion(-) diff --git a/ceilometer/api/controllers/v2.py b/ceilometer/api/controllers/v2.py index dc6d74cda..8b8de6b1a 100644 --- a/ceilometer/api/controllers/v2.py +++ b/ceilometer/api/controllers/v2.py @@ -2213,6 +2213,71 @@ class QueryController(rest.RestController): alarms = QueryAlarmsController() +def _flatten_capabilities(capabilities): + return dict((k, v) for k, v in utils.recursive_keypairs(capabilities)) + + +class Capabilities(_Base): + """A representation of the API capabilities, usually constrained + by restrictions imposed by the storage driver. + """ + + api = {wtypes.text: bool} + "A flattened dictionary of API capabilities" + + @classmethod + def sample(cls): + return cls( + api=_flatten_capabilities({ + 'meters': {'pagination': True, + 'query': {'simple': True, + 'metadata': True, + 'complex': False}}, + 'resources': {'pagination': False, + 'query': {'simple': True, + 'metadata': True, + 'complex': False}}, + 'samples': {'pagination': True, + 'groupby': True, + 'query': {'simple': True, + 'metadata': True, + 'complex': True}}, + 'statistics': {'pagination': True, + 'groupby': True, + 'query': {'simple': True, + 'metadata': True, + 'complex': False}, + 'aggregation': {'standard': True, + 'selectable': { + 'max': True, + 'min': True, + 'sum': True, + 'avg': True, + 'count': True, + 'stddev': True, + 'cardinality': True, + 'quartile': False}}}, + 'alarms': {'query': {'simple': True, + 'complex': True}, + 'history': {'query': {'simple': True, + 'complex': True}}}, + 'events': {'query': {'simple': True}}, + }) + ) + + +class CapabilitiesController(rest.RestController): + """Manages capabilities queries. + """ + + @wsme_pecan.wsexpose(Capabilities) + def get(self): + # variation in API capabilities is effectively determined by + # the lack of strict feature parity across storage drivers + driver_capabilities = pecan.request.storage_conn.get_capabilities() + return Capabilities(api=_flatten_capabilities(driver_capabilities)) + + class V2Controller(object): """Version 2 API controller root.""" @@ -2222,5 +2287,5 @@ class V2Controller(object): alarms = AlarmsController() event_types = EventTypesController() events = EventsController() - query = QueryController() + capabilities = CapabilitiesController() diff --git a/ceilometer/storage/base.py b/ceilometer/storage/base.py index d4a3b2cf2..b3c7ac633 100644 --- a/ceilometer/storage/base.py +++ b/ceilometer/storage/base.py @@ -118,6 +118,44 @@ class StorageEngine(object): class Connection(object): """Base class for storage system connections.""" + """A dictionary representing the capabilities of this driver. + """ + DEFAULT_CAPABILITIES = { + 'meters': {'pagination': False, + 'query': {'simple': False, + 'metadata': False, + 'complex': False}}, + 'resources': {'pagination': False, + 'query': {'simple': False, + 'metadata': False, + 'complex': False}}, + 'samples': {'pagination': False, + 'groupby': False, + 'query': {'simple': False, + 'metadata': False, + 'complex': False}}, + 'statistics': {'pagination': False, + 'groupby': False, + 'query': {'simple': False, + 'metadata': False, + 'complex': False}, + 'aggregation': {'standard': False, + 'selectable': { + 'max': False, + 'min': False, + 'sum': False, + 'avg': False, + 'count': False, + 'stddev': False, + 'cardinality': False}} + }, + 'alarms': {'query': {'simple': False, + 'complex': False}, + 'history': {'query': {'simple': False, + 'complex': False}}}, + 'events': {'query': {'simple': False}}, + } + @staticmethod def __init__(conf): """Constructor.""" @@ -355,3 +393,9 @@ class Connection(object): raise NotImplementedError(_('Complex query for alarms \ history is not implemented.')) + + @staticmethod + def get_capabilities(): + """Return an dictionary representing the capabilities of this driver. + """ + raise NotImplementedError(_('Capabilities not implemented.')) diff --git a/ceilometer/storage/impl_db2.py b/ceilometer/storage/impl_db2.py index 26ae03515..3a1642fb2 100644 --- a/ceilometer/storage/impl_db2.py +++ b/ceilometer/storage/impl_db2.py @@ -38,6 +38,7 @@ from ceilometer import storage from ceilometer.storage import base from ceilometer.storage import models from ceilometer.storage import pymongo_base +from ceilometer import utils LOG = log.getLogger(__name__) @@ -409,3 +410,24 @@ class Connection(pymongo_base.Connection): stat.period_start = stat.duration_start stat.period_end = stat.duration_end yield stat + + def get_capabilities(self): + """Return an dictionary representing the capabilities of this driver. + """ + available = { + 'meters': {'query': {'simple': True, + 'metadata': True}}, + 'resources': {'query': {'simple': True, + 'metadata': True}}, + 'samples': {'query': {'simple': True, + 'metadata': True, + 'complex': True}}, + 'statistics': {'groupby': True, + 'query': {'simple': True, + 'metadata': True}, + 'aggregation': {'standard': True}}, + 'alarms': {'query': {'simple': True, + 'complex': True}, + 'history': {'query': {'simple': True}}}, + } + return utils.update_nested(self.DEFAULT_CAPABILITIES, available) diff --git a/ceilometer/storage/impl_hbase.py b/ceilometer/storage/impl_hbase.py index 5cb904c22..dec594b17 100644 --- a/ceilometer/storage/impl_hbase.py +++ b/ceilometer/storage/impl_hbase.py @@ -629,6 +629,22 @@ class Connection(base.Connection): self._update_meter_stats(results[-1], meter) return results + def get_capabilities(self): + """Return an dictionary representing the capabilities of this driver. + """ + available = { + 'meters': {'query': {'simple': True, + 'metadata': True}}, + 'resources': {'query': {'simple': True, + 'metadata': True}}, + 'samples': {'query': {'simple': True, + 'metadata': True}}, + 'statistics': {'query': {'simple': True, + 'metadata': True}, + 'aggregation': {'standard': True}}, + } + return utils.update_nested(self.DEFAULT_CAPABILITIES, available) + ############### # This is a very crude version of "in-memory HBase", which implements just diff --git a/ceilometer/storage/impl_mongodb.py b/ceilometer/storage/impl_mongodb.py index 8a4d4cf94..895a14006 100644 --- a/ceilometer/storage/impl_mongodb.py +++ b/ceilometer/storage/impl_mongodb.py @@ -39,6 +39,7 @@ from ceilometer import storage from ceilometer.storage import base from ceilometer.storage import models from ceilometer.storage import pymongo_base +from ceilometer import utils cfg.CONF.import_opt('time_to_live', 'ceilometer.storage', group="database") @@ -701,3 +702,25 @@ class Connection(pymongo_base.Connection): orderby, limit, models.AlarmChange) + + def get_capabilities(self): + """Return an dictionary representing the capabilities of this driver. + """ + available = { + 'meters': {'query': {'simple': True, + 'metadata': True}}, + 'resources': {'query': {'simple': True, + 'metadata': True}}, + 'samples': {'query': {'simple': True, + 'metadata': True, + 'complex': True}}, + 'statistics': {'groupby': True, + 'query': {'simple': True, + 'metadata': True}, + 'aggregation': {'standard': True}}, + 'alarms': {'query': {'simple': True, + 'complex': True}, + 'history': {'query': {'simple': True, + 'complex': True}}}, + } + return utils.update_nested(self.DEFAULT_CAPABILITIES, available) diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py index 3c1cd8a02..e0893da51 100644 --- a/ceilometer/storage/impl_sqlalchemy.py +++ b/ceilometer/storage/impl_sqlalchemy.py @@ -1282,3 +1282,28 @@ class QueryTransformer(object): def get_query(self): return self.query + + def get_capabilities(self): + """Return an dictionary representing the capabilities of this driver. + """ + available = { + 'meters': {'query': {'simple': True, + 'metadata': True}}, + 'resources': {'query': {'simple': True, + 'metadata': True}}, + 'samples': {'pagination': True, + 'groupby': True, + 'query': {'simple': True, + 'metadata': True, + 'complex': True}}, + 'statistics': {'groupby': True, + 'query': {'simple': True, + 'metadata': True}, + 'aggregation': {'standard': True}}, + 'alarms': {'query': {'simple': True, + 'complex': True}, + 'history': {'query': {'simple': True, + 'complex': True}}}, + 'events': {'query': {'simple': True}}, + } + return utils.update_nested(self.DEFAULT_CAPABILITIES, available) diff --git a/ceilometer/utils.py b/ceilometer/utils.py index 0bbde9ae1..bf869223b 100644 --- a/ceilometer/utils.py +++ b/ceilometer/utils.py @@ -132,3 +132,16 @@ def lowercase_values(mapping): items = mapping.items() for key, value in items: mapping[key] = value.lower() + + +def update_nested(d, u): + """Updates the leaf nodes in a nest dict, without replacing + entire sub-dicts. + """ + for k, v in u.iteritems(): + if isinstance(v, dict): + r = update_nested(d.get(k, {}), v) + d[k] = r + else: + d[k] = u[k] + return d