383
375
'unimplemented operator for %s' %
386
if has_timestamp and not ('start' in valid_keys or
387
'start_timestamp' in valid_keys):
388
raise wsme.exc.UnknownArgument('timestamp',
389
"not valid for this resource")
392
def _query_to_kwargs(query, db_func, internal_keys=[]):
393
_validate_query(query, db_func, internal_keys=internal_keys)
379
def _validate_timestamp_fields(query, field_name, operator_list,
382
if item.field == field_name:
383
#If *timestamp* or *search_offset* field was specified in the
384
#query, but timestamp is not supported on that resource, on
385
#which the query was invoked, then raise an exception.
386
if not is_timestamp_valid:
387
raise wsme.exc.UnknownArgument(field_name,
390
if item.op not in operator_list:
391
raise wsme.exc.InvalidInput('op', item.op,
392
'unimplemented operator for %s' %
398
def _query_to_kwargs(query, db_func, internal_keys=[],
399
is_timestamp_valid=True):
400
_validate_query(query, db_func, internal_keys=internal_keys,
401
is_timestamp_valid=is_timestamp_valid)
394
402
query = _sanitize_query(query, db_func)
395
403
internal_keys.append('self')
396
404
valid_keys = set(inspect.getargspec(db_func)[0]) - set(internal_keys)
988
1039
return Sample.from_db_model(samples[0])
1042
class ComplexQuery(_Base):
1043
"""Holds a sample query encoded in json."""
1045
filter = wtypes.text
1046
"The filter expression encoded in json."
1048
orderby = wtypes.text
1049
"List of single-element dicts for specifing the ordering of the results."
1052
"The maximum number of results to be returned."
1056
return cls(filter='{\"and\": [{\"and\": [{\"=\": ' +
1057
'{\"counter_name\": \"cpu_util\"}}, ' +
1058
'{\">\": {\"counter_volume\": 0.23}}, ' +
1059
'{\"<\": {\"counter_volume\": 0.26}}]}, ' +
1060
'{\"or\": [{\"and\": [{\">\": ' +
1061
'{\"timestamp\": \"2013-12-01T18:00:00\"}}, ' +
1063
'{\"timestamp\": \"2013-12-01T18:15:00\"}}]}, ' +
1064
'{\"and\": [{\">\": ' +
1065
'{\"timestamp\": \"2013-12-01T18:30:00\"}}, ' +
1067
'{\"timestamp\": \"2013-12-01T18:45:00\"}}]}]}]}',
1068
orderby='[{\"counter_volume\": \"ASC\"}, ' +
1069
'{\"timestamp\": \"DESC\"}]',
1074
def _list_to_regexp(items, regexp_prefix=""):
1075
regexp = ["^%s$" % item for item in items]
1076
regexp = regexp_prefix + "|".join(regexp)
1080
class ValidatedComplexQuery(object):
1081
complex_operators = ["and", "or"]
1082
order_directions = ["asc", "desc"]
1083
simple_ops = ["=", "!=", "<", ">", "<=", "=<", ">=", "=>"]
1084
regexp_prefix = "(?i)"
1086
complex_ops = _list_to_regexp(complex_operators, regexp_prefix)
1087
simple_ops = _list_to_regexp(simple_ops, regexp_prefix)
1088
order_directions = _list_to_regexp(order_directions, regexp_prefix)
1090
timestamp_fields = ["timestamp", "state_timestamp"]
1091
name_mapping = {"user": "user_id",
1092
"project": "project_id",
1093
"resource": "resource_id"}
1095
def __init__(self, query, db_model, additional_valid_keys,
1096
metadata_allowed=False):
1097
valid_keys = db_model.get_field_names()
1098
valid_keys = list(valid_keys) + additional_valid_keys
1099
valid_fields = _list_to_regexp(valid_keys)
1101
if metadata_allowed:
1102
valid_filter_fields = valid_fields + "|^metadata\.[\S]+$"
1104
valid_filter_fields = valid_fields
1107
"oneOf": [{"type": "string"},
1109
{"type": "boolean"}],
1115
"items": {"oneOf": [{"type": "string"},
1116
{"type": "number"}]}}
1120
"patternProperties": {valid_filter_fields: schema_value},
1121
"additionalProperties": False,
1127
"patternProperties": {valid_filter_fields: schema_value_in},
1128
"additionalProperties": False,
1134
"patternProperties": {"(?i)^in$": schema_field_in},
1135
"additionalProperties": False,
1139
schema_leaf_simple_ops = {
1141
"patternProperties": {self.simple_ops: schema_field},
1142
"additionalProperties": False,
1146
schema_and_or_array = {
1148
"items": {"$ref": "#"},
1153
"patternProperties": {self.complex_ops: schema_and_or_array},
1154
"additionalProperties": False,
1160
"patternProperties": {"(?i)^not$": {"$ref": "#"}},
1161
"additionalProperties": False,
1166
"oneOf": [{"$ref": "#/definitions/leaf_simple_ops"},
1167
{"$ref": "#/definitions/leaf_in"},
1168
{"$ref": "#/definitions/and_or"},
1169
{"$ref": "#/definitions/not"}],
1172
"definitions": {"leaf_simple_ops": schema_leaf_simple_ops,
1173
"leaf_in": schema_leaf_in,
1174
"and_or": schema_and_or,
1177
self.orderby_schema = {
1181
"patternProperties":
1184
"pattern": self.order_directions}},
1185
"additionalProperties": False,
1187
"maxProperties": 1}}
1189
self.original_query = query
1191
def validate(self, visibility_field):
1192
"""Validates the query content and does the necessary transformations.
1194
if self.original_query.filter is wtypes.Unset:
1195
self.filter_expr = None
1197
self.filter_expr = json.loads(self.original_query.filter)
1198
self._validate_filter(self.filter_expr)
1199
self._replace_isotime_with_datetime(self.filter_expr)
1200
self._convert_operator_to_lower_case(self.filter_expr)
1201
self._normalize_field_names_for_db_model(self.filter_expr)
1203
self._force_visibility(visibility_field)
1205
if self.original_query.orderby is wtypes.Unset:
1208
self.orderby = json.loads(self.original_query.orderby)
1209
self._validate_orderby(self.orderby)
1210
self._convert_orderby_to_lower_case(self.orderby)
1211
self._normalize_field_names_in_orderby(self.orderby)
1213
if self.original_query.limit is wtypes.Unset:
1216
self.limit = self.original_query.limit
1218
if self.limit is not None and self.limit <= 0:
1219
msg = _('Limit should be positive')
1220
raise ClientSideError(msg)
1223
def _convert_orderby_to_lower_case(orderby):
1224
for orderby_field in orderby:
1225
utils.lowercase_values(orderby_field)
1227
def _normalize_field_names_in_orderby(self, orderby):
1228
for orderby_field in orderby:
1229
self._replace_field_names(orderby_field)
1231
def _traverse_postorder(self, tree, visitor):
1233
if op.lower() in self.complex_operators:
1234
for i, operand in enumerate(tree[op]):
1235
self._traverse_postorder(operand, visitor)
1236
if op.lower() == "not":
1237
self._traverse_postorder(tree[op], visitor)
1241
def _check_cross_project_references(self, own_project_id,
1243
"""Do not allow other than own_project_id
1245
def check_project_id(subfilter):
1246
op = subfilter.keys()[0]
1247
if (op.lower() not in self.complex_operators
1248
and subfilter[op].keys()[0] == visibility_field
1249
and subfilter[op][visibility_field] != own_project_id):
1250
raise ProjectNotAuthorized(subfilter[op][visibility_field])
1252
self._traverse_postorder(self.filter_expr, check_project_id)
1254
def _force_visibility(self, visibility_field):
1255
"""If the tenant is not admin insert an extra
1256
"and <visibility_field>=<tenant's project_id>" clause to the query
1258
authorized_project = acl.get_limited_to_project(pecan.request.headers)
1259
is_admin = authorized_project is None
1261
self._restrict_to_project(authorized_project, visibility_field)
1262
self._check_cross_project_references(authorized_project,
1265
def _restrict_to_project(self, project_id, visibility_field):
1266
restriction = {"=": {visibility_field: project_id}}
1267
if self.filter_expr is None:
1268
self.filter_expr = restriction
1270
self.filter_expr = {"and": [restriction, self.filter_expr]}
1272
def _replace_isotime_with_datetime(self, filter_expr):
1273
def replace_isotime(subfilter):
1274
op = subfilter.keys()[0]
1275
if (op.lower() not in self.complex_operators
1276
and subfilter[op].keys()[0] in self.timestamp_fields):
1277
field = subfilter[op].keys()[0]
1278
date_time = self._convert_to_datetime(subfilter[op][field])
1279
subfilter[op][field] = date_time
1281
self._traverse_postorder(filter_expr, replace_isotime)
1283
def _normalize_field_names_for_db_model(self, filter_expr):
1284
def _normalize_field_names(subfilter):
1285
op = subfilter.keys()[0]
1286
if op.lower() not in self.complex_operators:
1287
self._replace_field_names(subfilter.values()[0])
1288
self._traverse_postorder(filter_expr,
1289
_normalize_field_names)
1291
def _replace_field_names(self, subfilter):
1292
field = subfilter.keys()[0]
1293
value = subfilter[field]
1294
if field in self.name_mapping:
1295
del subfilter[field]
1296
subfilter[self.name_mapping[field]] = value
1297
if field.startswith("metadata."):
1298
del subfilter[field]
1299
subfilter["resource_" + field] = value
1301
def _convert_operator_to_lower_case(self, filter_expr):
1302
self._traverse_postorder(filter_expr, utils.lowercase_keys)
1305
def _convert_to_datetime(isotime):
1307
date_time = timeutils.parse_isotime(isotime)
1308
date_time = date_time.replace(tzinfo=None)
1311
LOG.exception(_("String %s is not a valid isotime") % isotime)
1312
msg = _('Failed to parse the timestamp value %s') % isotime
1313
raise ClientSideError(msg)
1315
def _validate_filter(self, filter_expr):
1316
jsonschema.validate(filter_expr, self.schema)
1318
def _validate_orderby(self, orderby_expr):
1319
jsonschema.validate(orderby_expr, self.orderby_schema)
991
1322
class Resource(_Base):
992
1323
"""An externally defined object for which samples have been received.
1182
1519
'153462d0-a9b8-4b5b-8175-9e4b05e9b856'])
1522
class AlarmTimeConstraint(_Base):
1523
"""Representation of a time constraint on an alarm."""
1525
name = wsme.wsattr(wtypes.text, mandatory=True)
1526
"The name of the constraint"
1528
_description = None # provide a default
1530
def get_description(self):
1531
if not self._description:
1532
return 'Time constraint at %s lasting for %s seconds' \
1533
% (self.start, self.duration)
1534
return self._description
1536
def set_description(self, value):
1537
self._description = value
1539
description = wsme.wsproperty(wtypes.text, get_description,
1541
"The description of the constraint"
1543
start = wsme.wsattr(CronType(), mandatory=True)
1544
"Start point of the time constraint, in cron format"
1546
duration = wsme.wsattr(wtypes.IntegerType(minimum=0), mandatory=True)
1547
"How long the constraint should last, in seconds"
1549
timezone = wsme.wsattr(wtypes.text, default="")
1550
"Timezone of the constraint"
1553
return self.as_dict_from_keys(['name', 'description', 'start',
1554
'duration', 'timezone'])
1560
pytz.timezone(tc.timezone)
1562
raise ClientSideError(_("Timezone %s is not valid")
1568
return cls(name='SampleConstraint',
1569
description='nightly build every night at 23h for 3 hours',
1572
timezone='Europe/Ljubljana')
1185
1575
class Alarm(_Base):
1186
1576
"""Representation of an alarm.
1189
combination_rule and threshold_rule are mutually exclusive.
1579
combination_rule and threshold_rule are mutually exclusive. The *type*
1580
of the alarm should be set to *threshold* or *combination* and the
1581
appropriate rule should be filled.
1192
1584
alarm_id = wtypes.text
1841
2244
traits=event.traits)
2247
class QuerySamplesController(rest.RestController):
2248
"""Provides complex query possibilities for samples
2251
@wsme_pecan.wsexpose([Sample], body=ComplexQuery)
2252
def post(self, body):
2253
"""Define query for retrieving Sample data.
2255
:param body: Query rules for the samples to be returned.
2257
query = ValidatedComplexQuery(body,
2258
storage.models.Sample,
2259
["user", "project", "resource"],
2260
metadata_allowed=True)
2261
query.validate(visibility_field="project_id")
2262
conn = pecan.request.storage_conn
2263
return [Sample.from_db_model(s)
2264
for s in conn.query_samples(query.filter_expr,
2269
class QueryAlarmHistoryController(rest.RestController):
2270
"""Provides complex query possibilites for alarm history
2272
@wsme_pecan.wsexpose([AlarmChange], body=ComplexQuery)
2273
def post(self, body):
2274
"""Define query for retrieving AlarmChange data.
2276
:param body: Query rules for the alarm history to be returned.
2278
query = ValidatedComplexQuery(body,
2279
storage.models.AlarmChange,
2280
["user", "project"])
2281
query.validate(visibility_field="on_behalf_of")
2282
conn = pecan.request.storage_conn
2283
return [AlarmChange.from_db_model(s)
2284
for s in conn.query_alarm_history(query.filter_expr,
2289
class QueryAlarmsController(rest.RestController):
2290
"""Provides complex query possibilities for alarms
2292
history = QueryAlarmHistoryController()
2294
@wsme_pecan.wsexpose([Alarm], body=ComplexQuery)
2295
def post(self, body):
2296
"""Define query for retrieving Alarm data.
2298
:param body: Query rules for the alarms to be returned.
2300
query = ValidatedComplexQuery(body,
2301
storage.models.Alarm,
2302
["user", "project"])
2303
query.validate(visibility_field="project_id")
2304
conn = pecan.request.storage_conn
2305
return [Alarm.from_db_model(s)
2306
for s in conn.query_alarms(query.filter_expr,
2311
class QueryController(rest.RestController):
2313
samples = QuerySamplesController()
2314
alarms = QueryAlarmsController()
2317
def _flatten_capabilities(capabilities):
2318
return dict((k, v) for k, v in utils.recursive_keypairs(capabilities))
2321
class Capabilities(_Base):
2322
"""A representation of the API capabilities, usually constrained
2323
by restrictions imposed by the storage driver.
2326
api = {wtypes.text: bool}
2327
"A flattened dictionary of API capabilities"
2332
api=_flatten_capabilities({
2333
'meters': {'pagination': True,
2334
'query': {'simple': True,
2337
'resources': {'pagination': False,
2338
'query': {'simple': True,
2341
'samples': {'pagination': True,
2343
'query': {'simple': True,
2346
'statistics': {'pagination': True,
2348
'query': {'simple': True,
2351
'aggregation': {'standard': True,
2359
'cardinality': True,
2360
'quartile': False}}},
2361
'alarms': {'query': {'simple': True,
2363
'history': {'query': {'simple': True,
2365
'events': {'query': {'simple': True}},
2370
class CapabilitiesController(rest.RestController):
2371
"""Manages capabilities queries.
2374
@wsme_pecan.wsexpose(Capabilities)
2376
# variation in API capabilities is effectively determined by
2377
# the lack of strict feature parity across storage drivers
2378
driver_capabilities = pecan.request.storage_conn.get_capabilities()
2379
return Capabilities(api=_flatten_capabilities(driver_capabilities))
1844
2382
class V2Controller(object):
1845
2383
"""Version 2 API controller root."""