gnocchi/gnocchi/rest/__init__.py

1467 lines
49 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- encoding: utf-8 -*-
#
# Copyright © 2016 Red Hat, Inc.
# Copyright © 2014-2015 eNovance
#
# 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.
import itertools
import uuid
from oslo_utils import strutils
import pecan
from pecan import rest
import six
from six.moves.urllib import parse as urllib_parse
from stevedore import extension
import voluptuous
import webob.exc
import werkzeug.http
from gnocchi import aggregates
from gnocchi import archive_policy
from gnocchi import indexer
from gnocchi import json
from gnocchi import resource_type
from gnocchi import storage
from gnocchi import utils
def arg_to_list(value):
if isinstance(value, list):
return value
elif value:
return [value]
return []
def abort(status_code, detail='', headers=None, comment=None, **kw):
"""Like pecan.abort, but make sure detail is a string."""
if status_code == 404 and not detail:
raise RuntimeError("http code 404 must have 'detail' set")
return pecan.abort(status_code, six.text_type(detail),
headers, comment, **kw)
def get_user_and_project():
headers = pecan.request.headers
user_id = headers.get("X-User-Id")
project_id = headers.get("X-Project-Id")
return (user_id, project_id)
# TODO(jd) Move this to oslo.utils as I stole it from Ceilometer
def recursive_keypairs(d, separator='.'):
"""Generator that produces sequence of keypairs for nested dictionaries."""
for name, value in sorted(six.iteritems(d)):
if isinstance(value, dict):
for subname, subvalue in recursive_keypairs(value, separator):
yield ('%s%s%s' % (name, separator, subname), subvalue)
else:
yield name, value
def enforce(rule, target):
"""Return the user and project the request should be limited to.
:param rule: The rule name
:param target: The target to enforce on.
"""
headers = pecan.request.headers
user_id, project_id = get_user_and_project()
creds = {
'roles': headers.get("X-Roles", "").split(","),
'user_id': user_id,
'project_id': project_id
}
if not isinstance(target, dict):
target = target.__dict__
# Flatten dict
target = dict(recursive_keypairs(target))
if not pecan.request.policy_enforcer.enforce(rule, target, creds):
abort(403)
def _get_list_resource_policy_filter(rule, resource_type, user, project):
try:
# Check if the policy allows the user to list any resource
enforce(rule, {
"resource_type": resource_type,
})
except webob.exc.HTTPForbidden:
policy_filter = []
try:
# Check if the policy allows the user to list resources linked
# to their project
enforce(rule, {
"resource_type": resource_type,
"project_id": project,
})
except webob.exc.HTTPForbidden:
pass
else:
policy_filter.append({"=": {"project_id": project}})
try:
# Check if the policy allows the user to list resources linked
# to their created_by_project
enforce(rule, {
"resource_type": resource_type,
"created_by_project_id": project,
})
except webob.exc.HTTPForbidden:
pass
else:
policy_filter.append({"=": {"created_by_project_id": project}})
if not policy_filter:
# We need to have at least one policy filter in place
abort(403, "Insufficient privileges")
return {"or": policy_filter}
def set_resp_location_hdr(location):
location = '%s%s' % (pecan.request.script_name, location)
# NOTE(sileht): according the pep-3333 the headers must be
# str in py2 and py3 even this is not the same thing in both
# version
# see: http://legacy.python.org/dev/peps/pep-3333/#unicode-issues
if six.PY2 and isinstance(location, six.text_type):
location = location.encode('utf-8')
location = urllib_parse.quote(location)
pecan.response.headers['Location'] = location
def deserialize():
mime_type, options = werkzeug.http.parse_options_header(
pecan.request.headers.get('Content-Type'))
if mime_type != "application/json":
abort(415)
try:
params = json.load(pecan.request.body_file_raw,
encoding=options.get('charset', 'ascii'))
except Exception as e:
abort(400, "Unable to decode body: " + six.text_type(e))
return params
def deserialize_and_validate(schema, required=True):
try:
return voluptuous.Schema(schema, required=required)(
deserialize())
except voluptuous.Error as e:
abort(400, "Invalid input: %s" % e)
def Timestamp(v):
t = utils.to_timestamp(v)
if t < utils.unix_universal_start:
raise ValueError("Timestamp must be after Epoch")
return t
def PositiveOrNullInt(value):
value = int(value)
if value < 0:
raise ValueError("Value must be positive")
return value
def PositiveNotNullInt(value):
value = int(value)
if value <= 0:
raise ValueError("Value must be positive and not null")
return value
def Timespan(value):
return utils.to_timespan(value).total_seconds()
def get_header_option(name, params):
type, options = werkzeug.http.parse_options_header(
pecan.request.headers.get('Accept'))
try:
return strutils.bool_from_string(
options.get(name, params.pop(name, 'false')),
strict=True)
except ValueError as e:
method = 'Accept' if name in options else 'query'
abort(
400,
"Unable to parse %s value in %s: %s"
% (name, method, six.text_type(e)))
def get_history(params):
return get_header_option('history', params)
def get_details(params):
return get_header_option('details', params)
RESOURCE_DEFAULT_PAGINATION = ['revision_start:asc',
'started_at:asc']
def get_pagination_options(params, default):
max_limit = pecan.request.conf.api.max_limit
limit = params.get('limit', max_limit)
marker = params.get('marker')
sorts = params.get('sort', default)
if not isinstance(sorts, list):
sorts = [sorts]
try:
limit = PositiveNotNullInt(limit)
except ValueError:
abort(400, "Invalid 'limit' value: %s" % params.get('limit'))
limit = min(limit, max_limit)
return {'limit': limit,
'marker': marker,
'sorts': sorts}
def ValidAggMethod(value):
value = six.text_type(value)
if value in archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS_VALUES:
return value
raise ValueError("Invalid aggregation method")
class ArchivePolicyController(rest.RestController):
def __init__(self, archive_policy):
self.archive_policy = archive_policy
@pecan.expose('json')
def get(self):
ap = pecan.request.indexer.get_archive_policy(self.archive_policy)
if ap:
enforce("get archive policy", ap)
return ap
abort(404, indexer.NoSuchArchivePolicy(self.archive_policy))
@pecan.expose()
def delete(self):
# NOTE(jd) I don't think there's any point in fetching and passing the
# archive policy here, as the rule is probably checking the actual role
# of the user, not the content of the AP.
enforce("delete archive policy", {})
try:
pecan.request.indexer.delete_archive_policy(self.archive_policy)
except indexer.NoSuchArchivePolicy as e:
abort(404, e)
except indexer.ArchivePolicyInUse as e:
abort(400, e)
class ArchivePoliciesController(rest.RestController):
@pecan.expose()
def _lookup(self, archive_policy, *remainder):
return ArchivePolicyController(archive_policy), remainder
@pecan.expose('json')
def post(self):
# NOTE(jd): Initialize this one at run-time because we rely on conf
conf = pecan.request.conf
enforce("create archive policy", {})
ArchivePolicySchema = voluptuous.Schema({
voluptuous.Required("name"): six.text_type,
voluptuous.Required("back_window", default=0): PositiveOrNullInt,
voluptuous.Required(
"aggregation_methods",
default=set(conf.archive_policy.default_aggregation_methods)):
[ValidAggMethod],
voluptuous.Required("definition"):
voluptuous.All([{
"granularity": Timespan,
"points": PositiveNotNullInt,
"timespan": Timespan,
}], voluptuous.Length(min=1)),
})
body = deserialize_and_validate(ArchivePolicySchema)
# Validate the data
try:
ap = archive_policy.ArchivePolicy.from_dict(body)
except ValueError as e:
abort(400, e)
enforce("create archive policy", ap)
try:
ap = pecan.request.indexer.create_archive_policy(ap)
except indexer.ArchivePolicyAlreadyExists as e:
abort(409, e)
location = "/archive_policy/" + ap.name
set_resp_location_hdr(location)
pecan.response.status = 201
return ap
@pecan.expose('json')
def get_all(self):
enforce("list archive policy", {})
return pecan.request.indexer.list_archive_policies()
class ArchivePolicyRulesController(rest.RestController):
@pecan.expose('json')
def post(self):
enforce("create archive policy rule", {})
ArchivePolicyRuleSchema = voluptuous.Schema({
voluptuous.Required("name"): six.text_type,
voluptuous.Required("metric_pattern"): six.text_type,
voluptuous.Required("archive_policy_name"): six.text_type,
})
body = deserialize_and_validate(ArchivePolicyRuleSchema)
enforce("create archive policy rule", body)
try:
ap = pecan.request.indexer.create_archive_policy_rule(
body['name'], body['metric_pattern'],
body['archive_policy_name']
)
except indexer.ArchivePolicyRuleAlreadyExists as e:
abort(409, e)
location = "/archive_policy_rule/" + ap.name
set_resp_location_hdr(location)
pecan.response.status = 201
return ap
@pecan.expose('json')
def get_one(self, name):
ap = pecan.request.indexer.get_archive_policy_rule(name)
if ap:
enforce("get archive policy rule", ap)
return ap
abort(404, indexer.NoSuchArchivePolicyRule(name))
@pecan.expose('json')
def get_all(self):
enforce("list archive policy rule", {})
return pecan.request.indexer.list_archive_policy_rules()
@pecan.expose()
def delete(self, name):
# NOTE(jd) I don't think there's any point in fetching and passing the
# archive policy rule here, as the rule is probably checking the actual
# role of the user, not the content of the AP rule.
enforce("delete archive policy rule", {})
try:
pecan.request.indexer.delete_archive_policy_rule(name)
except indexer.NoSuchArchivePolicyRule as e:
abort(404, e)
except indexer.ArchivePolicyRuleInUse as e:
abort(400, e)
class AggregatedMetricController(rest.RestController):
_custom_actions = {
'measures': ['GET']
}
def __init__(self, metric_ids):
self.metric_ids = metric_ids
@pecan.expose('json')
def get_measures(self, start=None, stop=None, aggregation='mean',
granularity=None, needed_overlap=100.0):
return self.get_cross_metric_measures_from_ids(
self.metric_ids, start, stop,
aggregation, granularity, needed_overlap)
@classmethod
def get_cross_metric_measures_from_ids(cls, metric_ids, start=None,
stop=None, aggregation='mean',
granularity=None,
needed_overlap=100.0):
# Check RBAC policy
metrics = pecan.request.indexer.list_metrics(ids=metric_ids)
missing_metric_ids = (set(metric_ids)
- set(six.text_type(m.id) for m in metrics))
if missing_metric_ids:
# Return one of the missing one in the error
abort(404, storage.MetricDoesNotExist(
missing_metric_ids.pop()))
return cls.get_cross_metric_measures_from_objs(
metrics, start, stop, aggregation, granularity, needed_overlap)
@staticmethod
def get_cross_metric_measures_from_objs(metrics, start=None, stop=None,
aggregation='mean',
granularity=None,
needed_overlap=100.0):
try:
needed_overlap = float(needed_overlap)
except ValueError:
abort(400, 'needed_overlap must be a number')
if start is not None:
try:
start = Timestamp(start)
except Exception:
abort(400, "Invalid value for start")
if stop is not None:
try:
stop = Timestamp(stop)
except Exception:
abort(400, "Invalid value for stop")
if (aggregation
not in archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS):
abort(
400,
'Invalid aggregation value %s, must be one of %s'
% (aggregation,
archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS))
for metric in metrics:
enforce("get metric", metric)
number_of_metrics = len(metrics)
try:
if number_of_metrics == 0:
return []
if granularity is not None:
try:
granularity = float(granularity)
except ValueError as e:
abort(400, "granularity must be a float: %s" % e)
if number_of_metrics == 1:
# NOTE(sileht): don't do the aggregation if we only have one
# metric
measures = pecan.request.storage.get_measures(
metrics[0], start, stop, aggregation,
granularity)
else:
measures = pecan.request.storage.get_cross_metric_measures(
metrics, start, stop, aggregation,
granularity,
needed_overlap)
# Replace timestamp keys by their string versions
return [(timestamp.isoformat(), offset, v)
for timestamp, offset, v in measures]
except storage.MetricUnaggregatable as e:
abort(400, ("One of the metrics being aggregated doesn't have "
"matching granularity: %s") % str(e))
except storage.MetricDoesNotExist as e:
abort(404, e)
except storage.AggregationDoesNotExist as e:
abort(404, e)
def MeasureSchema(m):
# NOTE(sileht): don't use voluptuous for performance reasons
try:
value = float(m['value'])
except Exception:
abort(400, "Invalid input for a value")
try:
timestamp = Timestamp(m['timestamp'])
except Exception as e:
abort(400,
"Invalid input for timestamp `%s': %s" % (m['timestamp'], e))
return storage.Measure(timestamp, value)
class MetricController(rest.RestController):
_custom_actions = {
'measures': ['POST', 'GET']
}
def __init__(self, metric):
self.metric = metric
mgr = extension.ExtensionManager(namespace='gnocchi.aggregates',
invoke_on_load=True)
self.custom_agg = dict((x.name, x.obj) for x in mgr)
def enforce_metric(self, rule):
enforce(rule, json.to_primitive(self.metric))
@pecan.expose('json')
def get_all(self):
self.enforce_metric("get metric")
return self.metric
@pecan.expose()
def post_measures(self):
self.enforce_metric("post measures")
params = deserialize()
if not isinstance(params, list):
abort(400, "Invalid input for measures")
if params:
pecan.request.storage.add_measures(
self.metric, six.moves.map(MeasureSchema, params))
pecan.response.status = 202
@pecan.expose('json')
def get_measures(self, start=None, stop=None, aggregation='mean',
granularity=None, **param):
self.enforce_metric("get measures")
if not (aggregation
in archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS
or aggregation in self.custom_agg):
msg = '''Invalid aggregation value %(agg)s, must be one of %(std)s
or %(custom)s'''
abort(400, msg % dict(
agg=aggregation,
std=archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS,
custom=str(self.custom_agg.keys())))
if start is not None:
try:
start = Timestamp(start)
except Exception:
abort(400, "Invalid value for start")
if stop is not None:
try:
stop = Timestamp(stop)
except Exception:
abort(400, "Invalid value for stop")
try:
if aggregation in self.custom_agg:
measures = self.custom_agg[aggregation].compute(
pecan.request.storage, self.metric,
start, stop, **param)
else:
measures = pecan.request.storage.get_measures(
self.metric, start, stop, aggregation,
float(granularity) if granularity is not None else None)
# Replace timestamp keys by their string versions
return [(timestamp.isoformat(), offset, v)
for timestamp, offset, v in measures]
except (storage.MetricDoesNotExist,
storage.GranularityDoesNotExist,
storage.AggregationDoesNotExist) as e:
abort(404, e)
except aggregates.CustomAggFailure as e:
abort(400, e)
@pecan.expose()
def delete(self):
self.enforce_metric("delete metric")
try:
pecan.request.indexer.delete_metric(self.metric.id)
except indexer.NoSuchMetric as e:
abort(404, e)
class MetricsController(rest.RestController):
@pecan.expose()
def _lookup(self, id, *remainder):
try:
metric_id = uuid.UUID(id)
except ValueError:
abort(404, indexer.NoSuchMetric(id))
metrics = pecan.request.indexer.list_metrics(
id=metric_id, details=True)
if not metrics:
abort(404, indexer.NoSuchMetric(id))
return MetricController(metrics[0]), remainder
_MetricSchema = voluptuous.Schema({
"user_id": six.text_type,
"project_id": six.text_type,
"archive_policy_name": six.text_type,
"name": six.text_type,
voluptuous.Optional("unit"):
voluptuous.All(six.text_type, voluptuous.Length(max=31)),
})
# NOTE(jd) Define this method as it was a voluptuous schema it's just a
# smarter version of a voluptuous schema, no?
@classmethod
def MetricSchema(cls, definition):
# First basic validation
definition = cls._MetricSchema(definition)
archive_policy_name = definition.get('archive_policy_name')
name = definition.get('name')
if archive_policy_name is None:
try:
ap = pecan.request.indexer.get_archive_policy_for_metric(name)
except indexer.NoArchivePolicyRuleMatch:
# NOTE(jd) Since this is a schema-like function, we
# should/could raise ValueError, but if we do so, voluptuous
# just returns a "invalid value" with no useful message so we
# prefer to use abort() to make sure the user has the right
# error message
abort(400, "No archive policy name specified "
"and no archive policy rule found matching "
"the metric name %s" % name)
else:
definition['archive_policy_name'] = ap.name
user_id, project_id = get_user_and_project()
enforce("create metric", {
"created_by_user_id": user_id,
"created_by_project_id": project_id,
"user_id": definition.get('user_id'),
"project_id": definition.get('project_id'),
"archive_policy_name": archive_policy_name,
"name": name,
"unit": definition.get('unit'),
})
return definition
@pecan.expose('json')
def post(self):
user, project = get_user_and_project()
body = deserialize_and_validate(self.MetricSchema)
try:
m = pecan.request.indexer.create_metric(
uuid.uuid4(),
user, project,
name=body.get('name'),
unit=body.get('unit'),
archive_policy_name=body['archive_policy_name'])
except indexer.NoSuchArchivePolicy as e:
abort(400, e)
set_resp_location_hdr("/metric/" + str(m.id))
pecan.response.status = 201
return m
@staticmethod
@pecan.expose('json')
def get_all(**kwargs):
try:
enforce("list all metric", {})
except webob.exc.HTTPForbidden:
enforce("list metric", {})
user_id, project_id = get_user_and_project()
provided_user_id = kwargs.get('user_id')
provided_project_id = kwargs.get('project_id')
if ((provided_user_id and user_id != provided_user_id)
or (provided_project_id and project_id != provided_project_id)):
abort(
403, "Insufficient privileges to filter by user/project")
else:
user_id = kwargs.get('user_id')
project_id = kwargs.get('project_id')
attr_filter = {}
if user_id is not None:
attr_filter['created_by_user_id'] = user_id
if project_id is not None:
attr_filter['created_by_project_id'] = project_id
return pecan.request.indexer.list_metrics(**attr_filter)
_MetricsSchema = voluptuous.Schema({
six.text_type: voluptuous.Any(utils.UUID,
MetricsController.MetricSchema),
})
def MetricsSchema(data):
# NOTE(jd) Before doing any kind of validation, copy the metric name
# into the metric definition. This is required so we have the name
# available when doing the metric validation with its own MetricSchema,
# and so we can do things such as applying archive policy rules.
if isinstance(data, dict):
for metric_name, metric_def in six.iteritems(data):
if isinstance(metric_def, dict):
metric_def['name'] = metric_name
return _MetricsSchema(data)
class NamedMetricController(rest.RestController):
def __init__(self, resource_id, resource_type):
self.resource_id = resource_id
self.resource_type = resource_type
@pecan.expose()
def _lookup(self, name, *remainder):
details = True if pecan.request.method == 'GET' else False
m = pecan.request.indexer.list_metrics(details=details,
name=name,
resource_id=self.resource_id)
if m:
return MetricController(m[0]), remainder
resource = pecan.request.indexer.get_resource(self.resource_type,
self.resource_id)
if resource:
abort(404, indexer.NoSuchMetric(name))
else:
abort(404, indexer.NoSuchResource(self.resource_id))
@pecan.expose()
def post(self):
resource = pecan.request.indexer.get_resource(
self.resource_type, self.resource_id)
if not resource:
abort(404, indexer.NoSuchResource(self.resource_id))
enforce("update resource", resource)
metrics = deserialize_and_validate(MetricsSchema)
try:
pecan.request.indexer.update_resource(
self.resource_type, self.resource_id, metrics=metrics,
append_metrics=True,
create_revision=False)
except (indexer.NoSuchMetric,
indexer.NoSuchArchivePolicy,
ValueError) as e:
abort(400, e)
except indexer.NamedMetricAlreadyExists as e:
abort(409, e)
except indexer.NoSuchResource as e:
abort(404, e)
@pecan.expose('json')
def get_all(self):
resource = pecan.request.indexer.get_resource(
self.resource_type, self.resource_id)
if not resource:
abort(404, indexer.NoSuchResource(self.resource_id))
enforce("get resource", resource)
return pecan.request.indexer.list_metrics(resource_id=self.resource_id)
class ResourceHistoryController(rest.RestController):
def __init__(self, resource_id, resource_type):
self.resource_id = resource_id
self.resource_type = resource_type
@pecan.expose('json')
def get(self, **kwargs):
details = get_details(kwargs)
pagination_opts = get_pagination_options(
kwargs, RESOURCE_DEFAULT_PAGINATION)
resource = pecan.request.indexer.get_resource(
self.resource_type, self.resource_id)
if not resource:
abort(404, "foo")
enforce("get resource", resource)
try:
# FIXME(sileht): next API version should returns
# {'resources': [...], 'links': [ ... pagination rel ...]}
return pecan.request.indexer.list_resources(
self.resource_type,
attribute_filter={"=": {"id": self.resource_id}},
details=details,
history=True,
**pagination_opts
)
except indexer.IndexerException as e:
abort(400, e)
def etag_precondition_check(obj):
etag, lastmodified = obj.etag, obj.lastmodified
# NOTE(sileht): Checks and order come from rfc7232
# in webob, the '*' and the absent of the header is handled by
# if_match.__contains__() and if_none_match.__contains__()
# and are identique...
if etag not in pecan.request.if_match:
abort(412)
elif (not pecan.request.environ.get("HTTP_IF_MATCH")
and pecan.request.if_unmodified_since
and pecan.request.if_unmodified_since < lastmodified):
abort(412)
if etag in pecan.request.if_none_match:
if pecan.request.method in ['GET', 'HEAD']:
abort(304)
else:
abort(412)
elif (not pecan.request.environ.get("HTTP_IF_NONE_MATCH")
and pecan.request.if_modified_since
and (pecan.request.if_modified_since >=
lastmodified)
and pecan.request.method in ['GET', 'HEAD']):
abort(304)
def etag_set_headers(obj):
pecan.response.etag = obj.etag
pecan.response.last_modified = obj.lastmodified
class ResourceTypeController(rest.RestController):
def __init__(self, name):
self._name = name
@pecan.expose('json')
def get(self):
try:
rt = pecan.request.indexer.get_resource_type(self._name)
except indexer.NoSuchResourceType as e:
abort(404, e)
enforce("get resource type", rt)
return rt
@pecan.expose()
def delete(self):
try:
pecan.request.indexer.get_resource_type(self._name)
except indexer.NoSuchResourceType as e:
abort(404, e)
enforce("delete resource type", resource_type)
try:
pecan.request.indexer.delete_resource_type(self._name)
except (indexer.NoSuchResourceType,
indexer.ResourceTypeInUse) as e:
abort(400, e)
class ResourceTypesController(rest.RestController):
@pecan.expose()
def _lookup(self, name, *remainder):
return ResourceTypeController(name), remainder
@pecan.expose('json')
def post(self):
schema = pecan.request.indexer.get_resource_type_schema()
body = deserialize_and_validate(schema)
rt = schema.resource_type_from_dict(**body)
enforce("create resource type", body)
try:
rt = pecan.request.indexer.create_resource_type(rt)
except indexer.ResourceTypeAlreadyExists as e:
abort(409, e)
set_resp_location_hdr("/resource_type/" + rt.name)
pecan.response.status = 201
return rt
@pecan.expose('json')
def get_all(self, **kwargs):
enforce("list resource type", {})
try:
return pecan.request.indexer.list_resource_types()
except indexer.IndexerException as e:
abort(400, e)
def ResourceSchema(schema):
base_schema = {
voluptuous.Optional('started_at'): Timestamp,
voluptuous.Optional('ended_at'): Timestamp,
voluptuous.Optional('user_id'): voluptuous.Any(None, six.text_type),
voluptuous.Optional('project_id'): voluptuous.Any(None, six.text_type),
voluptuous.Optional('metrics'): MetricsSchema,
}
base_schema.update(schema)
return base_schema
class ResourceController(rest.RestController):
def __init__(self, resource_type, id):
self._resource_type = resource_type
try:
self.id = utils.ResourceUUID(id)
except ValueError:
abort(404, indexer.NoSuchResource(id))
self.metric = NamedMetricController(str(self.id), self._resource_type)
self.history = ResourceHistoryController(str(self.id),
self._resource_type)
@pecan.expose('json')
def get(self):
resource = pecan.request.indexer.get_resource(
self._resource_type, self.id, with_metrics=True)
if resource:
enforce("get resource", resource)
etag_precondition_check(resource)
etag_set_headers(resource)
return resource
abort(404, indexer.NoSuchResource(self.id))
@pecan.expose('json')
def patch(self):
resource = pecan.request.indexer.get_resource(
self._resource_type, self.id, with_metrics=True)
if not resource:
abort(404, indexer.NoSuchResource(self.id))
enforce("update resource", resource)
etag_precondition_check(resource)
body = deserialize_and_validate(
schema_for(self._resource_type),
required=False)
if len(body) == 0:
etag_set_headers(resource)
return resource
for k, v in six.iteritems(body):
if k != 'metrics' and getattr(resource, k) != v:
create_revision = True
break
else:
if 'metrics' not in body:
# No need to go further, we assume the db resource
# doesn't change between the get and update
return resource
create_revision = False
try:
resource = pecan.request.indexer.update_resource(
self._resource_type,
self.id,
create_revision=create_revision,
**body)
except (indexer.NoSuchMetric,
indexer.NoSuchArchivePolicy,
ValueError) as e:
abort(400, e)
except indexer.NoSuchResource as e:
abort(404, e)
etag_set_headers(resource)
return resource
@pecan.expose()
def delete(self):
resource = pecan.request.indexer.get_resource(
self._resource_type, self.id)
if not resource:
abort(404, indexer.NoSuchResource(self.id))
enforce("delete resource", resource)
etag_precondition_check(resource)
try:
pecan.request.indexer.delete_resource(self.id)
except indexer.NoSuchResource as e:
abort(404, e)
def schema_for(resource_type):
resource_type = pecan.request.indexer.get_resource_type(resource_type)
return ResourceSchema(resource_type.schema)
def ResourceID(value):
return (six.text_type(value), utils.ResourceUUID(value))
class ResourcesController(rest.RestController):
def __init__(self, resource_type):
self._resource_type = resource_type
@pecan.expose()
def _lookup(self, id, *remainder):
return ResourceController(self._resource_type, id), remainder
@pecan.expose('json')
def post(self):
# NOTE(sileht): we need to copy the dict because when change it
# and we don't want that next patch call have the "id"
schema = dict(schema_for(self._resource_type))
schema["id"] = ResourceID
body = deserialize_and_validate(schema)
body["original_resource_id"], body["id"] = body["id"]
target = {
"resource_type": self._resource_type,
}
target.update(body)
enforce("create resource", target)
user, project = get_user_and_project()
rid = body['id']
del body['id']
try:
resource = pecan.request.indexer.create_resource(
self._resource_type, rid, user, project,
**body)
except (ValueError,
indexer.NoSuchMetric,
indexer.NoSuchArchivePolicy) as e:
abort(400, e)
except indexer.ResourceAlreadyExists as e:
abort(409, e)
set_resp_location_hdr("/resource/"
+ self._resource_type + "/"
+ six.text_type(resource.id))
etag_set_headers(resource)
pecan.response.status = 201
return resource
@pecan.expose('json')
def get_all(self, **kwargs):
details = get_details(kwargs)
history = get_history(kwargs)
pagination_opts = get_pagination_options(
kwargs, RESOURCE_DEFAULT_PAGINATION)
user, project = get_user_and_project()
policy_filter = _get_list_resource_policy_filter(
"list resource", self._resource_type, user, project)
try:
# FIXME(sileht): next API version should returns
# {'resources': [...], 'links': [ ... pagination rel ...]}
return pecan.request.indexer.list_resources(
self._resource_type,
attribute_filter=policy_filter,
details=details,
history=history,
**pagination_opts
)
except indexer.IndexerException as e:
abort(400, e)
class ResourcesByTypeController(rest.RestController):
@pecan.expose('json')
def get_all(self):
return dict(
(rt.name,
pecan.request.application_url + '/resource/' + rt.name)
for rt in pecan.request.indexer.list_resource_types())
@pecan.expose()
def _lookup(self, resource_type, *remainder):
try:
pecan.request.indexer.get_resource_type(resource_type)
except indexer.NoSuchResourceType as e:
abort(404, e)
return ResourcesController(resource_type), remainder
def _ResourceSearchSchema(v):
"""Helper method to indirect the recursivity of the search schema"""
return SearchResourceTypeController.ResourceSearchSchema(v)
class SearchResourceTypeController(rest.RestController):
def __init__(self, resource_type):
self._resource_type = resource_type
ResourceSearchSchema = voluptuous.Schema(
voluptuous.All(
voluptuous.Length(min=1, max=1),
{
voluptuous.Any(
u"=", u"==", u"eq",
u"<", u"lt",
u">", u"gt",
u"<=", u"", u"le",
u">=", u"", u"ge",
u"!=", u"", u"ne",
u"in",
u"like",
): voluptuous.All(voluptuous.Length(min=1, max=1), dict),
voluptuous.Any(
u"and", u"",
u"or", u"",
u"not",
): [_ResourceSearchSchema],
}
)
)
def _search(self, **kwargs):
if pecan.request.body:
attr_filter = deserialize_and_validate(self.ResourceSearchSchema)
else:
attr_filter = None
details = get_details(kwargs)
history = get_history(kwargs)
pagination_opts = get_pagination_options(
kwargs, RESOURCE_DEFAULT_PAGINATION)
user, project = get_user_and_project()
policy_filter = _get_list_resource_policy_filter(
"search resource", self._resource_type, user, project)
if policy_filter:
if attr_filter:
attr_filter = {"and": [
policy_filter,
attr_filter
]}
else:
attr_filter = policy_filter
return pecan.request.indexer.list_resources(
self._resource_type,
attribute_filter=attr_filter,
details=details,
history=history,
**pagination_opts)
@pecan.expose('json')
def post(self, **kwargs):
try:
return self._search(**kwargs)
except indexer.IndexerException as e:
abort(400, e)
class SearchResourceController(rest.RestController):
@pecan.expose()
def _lookup(self, resource_type, *remainder):
try:
pecan.request.indexer.get_resource_type(resource_type)
except indexer.NoSuchResourceType as e:
abort(404, e)
return SearchResourceTypeController(resource_type), remainder
def _MetricSearchSchema(v):
"""Helper method to indirect the recursivity of the search schema"""
return SearchMetricController.MetricSearchSchema(v)
def _MetricSearchOperationSchema(v):
"""Helper method to indirect the recursivity of the search schema"""
return SearchMetricController.MetricSearchOperationSchema(v)
class SearchMetricController(rest.RestController):
MetricSearchOperationSchema = voluptuous.Schema(
voluptuous.All(
voluptuous.Length(min=1, max=1),
{
voluptuous.Any(
u"=", u"==", u"eq",
u"<", u"lt",
u">", u"gt",
u"<=", u"", u"le",
u">=", u"", u"ge",
u"!=", u"", u"ne",
u"%", u"mod",
u"+", u"add",
u"-", u"sub",
u"*", u"×", u"mul",
u"/", u"÷", u"div",
u"**", u"^", u"pow",
): voluptuous.Any(
float, int,
voluptuous.All(
[float, int,
voluptuous.Any(_MetricSearchOperationSchema)],
voluptuous.Length(min=2, max=2),
),
),
},
)
)
MetricSearchSchema = voluptuous.Schema(
voluptuous.Any(
MetricSearchOperationSchema,
voluptuous.All(
voluptuous.Length(min=1, max=1),
{
voluptuous.Any(
u"and", u"",
u"or", u"",
u"not",
): [_MetricSearchSchema],
}
)
)
)
@pecan.expose('json')
def post(self, metric_id, start=None, stop=None, aggregation='mean'):
metrics = pecan.request.indexer.list_metrics(
ids=arg_to_list(metric_id))
for metric in metrics:
enforce("search metric", metric)
if not pecan.request.body:
abort(400, "No query specified in body")
query = deserialize_and_validate(self.MetricSearchSchema)
if start is not None:
try:
start = Timestamp(start)
except Exception:
abort(400, "Invalid value for start")
if stop is not None:
try:
stop = Timestamp(stop)
except Exception:
abort(400, "Invalid value for stop")
try:
return {
str(metric.id): values
for metric, values in six.iteritems(
pecan.request.storage.search_value(
metrics, query, start, stop, aggregation)
)
}
except storage.InvalidQuery as e:
abort(400, e)
class ResourcesMetricsMeasuresBatchController(rest.RestController):
MeasuresBatchSchema = voluptuous.Schema(
{utils.ResourceUUID: {six.text_type: [MeasureSchema]}}
)
@pecan.expose()
def post(self):
body = deserialize_and_validate(self.MeasuresBatchSchema)
known_metrics = []
unknown_metrics = []
for resource_id in body:
names = body[resource_id].keys()
metrics = pecan.request.indexer.list_metrics(
names=names, resource_id=resource_id)
if len(names) != len(metrics):
known_names = [m.name for m in metrics]
unknown_metrics.extend(
["%s/%s" % (six.text_type(resource_id), m)
for m in names if m not in known_names])
known_metrics.extend(metrics)
if unknown_metrics:
abort(400, "Unknown metrics: %s" % ", ".join(
sorted(unknown_metrics)))
for metric in known_metrics:
enforce("post measures", metric)
for metric in known_metrics:
measures = body[metric.resource_id][metric.name]
pecan.request.storage.add_measures(metric, measures)
pecan.response.status = 202
class MetricsMeasuresBatchController(rest.RestController):
# NOTE(sileht): we don't allow to mix both formats
# to not have to deal with id collision that can
# occurs between a metric_id and a resource_id.
# Because while json allow duplicate keys in dict payload
# only the last key will be retain by json python module to
# build the python dict.
MeasuresBatchSchema = voluptuous.Schema(
{utils.UUID: [MeasureSchema]}
)
@pecan.expose()
def post(self):
body = deserialize_and_validate(self.MeasuresBatchSchema)
metrics = pecan.request.indexer.list_metrics(ids=body.keys())
if len(metrics) != len(body):
missing_metrics = sorted(set(body) - set(m.id for m in metrics))
abort(400, "Unknown metrics: %s" % ", ".join(
six.moves.map(str, missing_metrics)))
for metric in metrics:
enforce("post measures", metric)
for metric in metrics:
pecan.request.storage.add_measures(metric, body[metric.id])
pecan.response.status = 202
class SearchController(object):
resource = SearchResourceController()
metric = SearchMetricController()
class AggregationResourceController(rest.RestController):
def __init__(self, resource_type, metric_name):
self.resource_type = resource_type
self.metric_name = metric_name
@pecan.expose('json')
def post(self, start=None, stop=None, aggregation='mean',
granularity=None, needed_overlap=100.0,
groupby=None):
# First, set groupby in the right format: a sorted list of unique
# strings.
groupby = sorted(set(arg_to_list(groupby)))
# NOTE(jd) Sort by groupby so we are sure we do not return multiple
# groups when using itertools.groupby later.
try:
resources = SearchResourceTypeController(
self.resource_type)._search(sort=groupby)
except indexer.InvalidPagination:
abort(400, "Invalid groupby attribute")
except indexer.IndexerException as e:
abort(400, e)
if resources is None:
return []
if not groupby:
metrics = list(filter(None,
(r.get_metric(self.metric_name)
for r in resources)))
return AggregatedMetricController.get_cross_metric_measures_from_objs( # noqa
metrics, start, stop, aggregation, granularity, needed_overlap)
def groupper(r):
return tuple((attr, r[attr]) for attr in groupby)
results = []
for key, resources in itertools.groupby(resources, groupper):
metrics = list(filter(None,
(r.get_metric(self.metric_name)
for r in resources)))
results.append({
"group": dict(key),
"measures": AggregatedMetricController.get_cross_metric_measures_from_objs( # noqa
metrics, start, stop, aggregation,
granularity, needed_overlap)
})
return results
class AggregationController(rest.RestController):
_custom_actions = {
'metric': ['GET'],
}
@pecan.expose()
def _lookup(self, object_type, resource_type, key, metric_name,
*remainder):
if object_type != "resource" or key != "metric":
# NOTE(sileht): we want the raw 404 message here
# so use directly pecan
pecan.abort(404)
try:
pecan.request.indexer.get_resource_type(resource_type)
except indexer.NoSuchResourceType as e:
abort(404, e)
return AggregationResourceController(resource_type,
metric_name), remainder
@pecan.expose('json')
def get_metric(self, metric=None, start=None,
stop=None, aggregation='mean',
granularity=None, needed_overlap=100.0):
return AggregatedMetricController.get_cross_metric_measures_from_ids(
arg_to_list(metric), start, stop, aggregation,
granularity, needed_overlap)
class CapabilityController(rest.RestController):
@staticmethod
@pecan.expose('json')
def get():
aggregation_methods = set(
archive_policy.ArchivePolicy.VALID_AGGREGATION_METHODS)
aggregation_methods.update(
ext.name for ext in extension.ExtensionManager(
namespace='gnocchi.aggregates'))
return dict(aggregation_methods=aggregation_methods)
class StatusController(rest.RestController):
@staticmethod
@pecan.expose('json')
def get(details=True):
enforce("get status", {})
report = pecan.request.storage.measures_report(details)
report_dict = {"storage": {"summary": report['summary']}}
if 'details' in report:
report_dict["storage"]["measures_to_process"] = report['details']
return report_dict
class MetricsBatchController(object):
measures = MetricsMeasuresBatchController()
class ResourcesMetricsBatchController(object):
measures = ResourcesMetricsMeasuresBatchController()
class ResourcesBatchController(object):
metrics = ResourcesMetricsBatchController()
class BatchController(object):
metrics = MetricsBatchController()
resources = ResourcesBatchController()
class V1Controller(object):
def __init__(self):
self.sub_controllers = {
"search": SearchController(),
"archive_policy": ArchivePoliciesController(),
"archive_policy_rule": ArchivePolicyRulesController(),
"metric": MetricsController(),
"batch": BatchController(),
"resource": ResourcesByTypeController(),
"resource_type": ResourceTypesController(),
"aggregation": AggregationController(),
"capabilities": CapabilityController(),
"status": StatusController(),
}
for name, ctrl in self.sub_controllers.items():
setattr(self, name, ctrl)
@pecan.expose('json')
def index(self):
return {
"version": "1.0",
"links": [
{"rel": "self",
"href": pecan.request.application_url}
] + [
{"rel": name,
"href": pecan.request.application_url + "/" + name}
for name in sorted(self.sub_controllers)
]
}
class VersionsController(object):
@staticmethod
@pecan.expose('json')
def index():
return {
"versions": [
{
"status": "CURRENT",
"links": [
{
"rel": "self",
"href": pecan.request.application_url + "/v1/"
}
],
"id": "v1.0",
"updated": "2015-03-19"
}
]
}