Files
monasca-api/monasca_api/v2/reference/helpers.py
Ben Motz 7f09cd3acc Fix handling of mutiple dimensions in query
The Monasca API spec supports multiple comma separated dimensions in the
query string, however this was handled incorrectly (the Falcon library
parses this into a list, while the existing code was always expecting a
string.)

Additionally, the code was not handling the case of multiple instances
of the Dimensions query parameter.

Change-Id: Iebed15fed234b6052df899cbe84264e849f9895c
2015-08-27 18:12:21 +01:00

557 lines
18 KiB
Python

# Copyright 2014 Hewlett-Packard
# Copyright 2015 Cray Ltd.
#
# 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 datetime
import json
import urllib
import urlparse
import falcon
from oslo_log import log
import simplejson
from monasca_api.common.repositories import constants
from monasca_api.v2.common.schemas import dimensions_schema
from monasca_api.v2.common.schemas import exceptions as schemas_exceptions
from monasca_api.v2.common.schemas import metric_name_schema
LOG = log.getLogger(__name__)
def read_json_msg_body(req):
"""Read the json_msg from the http request body and return them as JSON.
:param req: HTTP request object.
:return: Returns the metrics as a JSON object.
:raises falcon.HTTPBadRequest:
"""
try:
msg = req.stream.read()
json_msg = json.loads(msg)
return json_msg
except ValueError as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request',
'Request body is not valid JSON')
def validate_json_content_type(req):
if req.content_type not in ['application/json']:
raise falcon.HTTPBadRequest('Bad request', 'Bad content type. Must be '
'application/json')
def is_in_role(req, authorized_roles):
"""Is one or more of the X-ROLES in the supplied authorized_roles.
:param req: HTTP request object. Must contain "X-ROLES" in the HTTP
request header.
:param authorized_roles: List of authorized roles to check against.
:return: Returns True if in the list of authorized roles, otherwise False.
"""
str_roles = req.get_header('X-ROLES')
if str_roles is None:
return False
roles = str_roles.lower().split(',')
for role in roles:
if role in authorized_roles:
return True
return False
def validate_authorization(req, authorized_roles):
"""Validates whether one or more X-ROLES in the HTTP header is authorized.
:param req: HTTP request object. Must contain "X-ROLES" in the HTTP
request header.
:param authorized_roles: List of authorized roles to check against.
:raises falcon.HTTPUnauthorized
"""
str_roles = req.get_header('X-ROLES')
if str_roles is None:
raise falcon.HTTPUnauthorized('Forbidden',
'Tenant does not have any roles')
roles = str_roles.lower().split(',')
authorized_roles_lower = [r.lower() for r in authorized_roles]
for role in roles:
if role in authorized_roles_lower:
return
raise falcon.HTTPUnauthorized('Forbidden',
'Tenant ID is missing a required role to '
'access this service')
def get_tenant_id(req):
"""Returns the tenant ID in the HTTP request header.
:param req: HTTP request object.
"""
return req.get_header('X-TENANT-ID')
def get_x_tenant_or_tenant_id(req, delegate_authorized_roles):
"""Evaluates whether the tenant ID or cross tenant ID should be returned.
:param req: HTTP request object.
:param delegate_authorized_roles: List of authorized roles that have
delegate privileges.
:returns: Returns the cross tenant or tenant ID.
"""
if is_in_role(req, delegate_authorized_roles):
params = falcon.uri.parse_query_string(req.query_string)
if 'tenant_id' in params:
tenant_id = params['tenant_id']
return tenant_id
return get_tenant_id(req)
def get_query_param(req, param_name, required=False, default_val=None):
try:
params = falcon.uri.parse_query_string(req.query_string)
if param_name in params:
param_val = params[param_name].decode('utf8')
return param_val
else:
if required:
raise Exception("Missing " + param_name)
else:
return default_val
except Exception as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request', ex.message)
def get_query_name(req, name_required=False):
"""Returns the query param "name" if supplied.
:param req: HTTP request object.
"""
try:
params = falcon.uri.parse_query_string(req.query_string)
if 'name' in params:
name = params['name']
return name
else:
if name_required:
raise Exception("Missing name")
else:
return ''
except Exception as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request', ex.message)
def get_query_dimensions(req):
"""Gets and parses the query param dimensions.
:param req: HTTP request object.
:return: Returns the dimensions as a JSON object
:raises falcon.HTTPBadRequest: If dimensions are malformed.
"""
try:
params = falcon.uri.parse_query_string(req.query_string)
dimensions = {}
if 'dimensions' in params:
dimensions_param = params['dimensions']
if isinstance(dimensions_param, basestring):
dimensions_str_array = [dimensions_param, ]
else:
dimensions_str_array = [
s for sublist in dimensions_param
for s in sublist.split(",")]
for dimension in dimensions_str_array:
dimension_name_value = dimension.split(':')
if len(dimension_name_value) == 2:
dimensions[dimension_name_value[0]] = dimension_name_value[
1]
else:
raise Exception('Dimensions are malformed')
return dimensions
except Exception as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request', ex.message)
def get_query_starttime_timestamp(req, required=True):
try:
params = falcon.uri.parse_query_string(req.query_string)
if 'start_time' in params:
return _convert_time_string(params['start_time'])
else:
if required:
raise Exception("Missing start time")
else:
return None
except Exception as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request', ex.message)
def get_query_endtime_timestamp(req, required=True):
try:
params = falcon.uri.parse_query_string(req.query_string)
if 'end_time' in params:
return _convert_time_string(params['end_time'])
else:
if required:
raise Exception("Missing end time")
else:
return None
except Exception as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request', ex.message)
def _convert_time_string(date_time_string):
dt = datetime.datetime.strptime(date_time_string, "%Y-%m-%dT%H:%M:%SZ")
timestamp = (dt - datetime.datetime(1970, 1, 1)).total_seconds()
return timestamp
def get_query_statistics(req):
try:
params = falcon.uri.parse_query_string(req.query_string)
if 'statistics' in params:
statistics = []
# falcon may return this as a list or as a string
if isinstance(params['statistics'], list):
statistics.extend(params['statistics'])
else:
statistics.append(params['statistics'])
statistics = [statistic.lower() for statistic in statistics]
if not all(statistic in ['avg', 'min', 'max', 'count', 'sum'] for
statistic in statistics):
raise Exception("Invalid statistic")
return statistics
else:
raise Exception("Missing statistics")
except Exception as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request', ex.message)
def get_query_period(req):
try:
params = falcon.uri.parse_query_string(req.query_string)
if 'period' in params:
return params['period']
else:
return None
except Exception as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request', ex.message)
def validate_query_name(name):
"""Validates the query param name.
:param name: Query param name.
:raises falcon.HTTPBadRequest: If name is not valid.
"""
try:
metric_name_schema.validate(name)
except schemas_exceptions.ValidationException as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request', ex.message)
def validate_query_dimensions(dimensions):
"""Validates the query param dimensions.
:param dimensions: Query param dimensions.
:raises falcon.HTTPBadRequest: If dimensions are not valid.
"""
try:
dimensions_schema.validate(dimensions)
except schemas_exceptions.ValidationException as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest('Bad request', ex.message)
def paginate(resource, uri, limit):
parsed_uri = urlparse.urlparse(uri)
self_link = build_base_uri(parsed_uri)
old_query_params = _get_old_query_params(parsed_uri)
if old_query_params:
self_link += '?' + '&'.join(old_query_params)
if resource and len(resource) > limit:
if 'timestamp' in resource[limit - 1]:
new_offset = resource[limit - 1]['timestamp']
if 'id' in resource[limit - 1]:
new_offset = resource[limit - 1]['id']
next_link = build_base_uri(parsed_uri)
new_query_params = [u'offset' + '=' + urllib.quote(
new_offset.encode('utf8'), safe='')]
_get_old_query_params_except_offset(new_query_params, parsed_uri)
if new_query_params:
next_link += '?' + '&'.join(new_query_params)
resource = {u'links': ([{u'rel': u'self',
u'href': self_link.decode('utf8')},
{u'rel': u'next',
u'href': next_link.decode('utf8')}]),
u'elements': resource[:limit]}
else:
resource = {u'links': ([{u'rel': u'self',
u'href': self_link.decode('utf8')}]),
u'elements': resource}
return resource
def paginate_measurement(measurement, uri, limit):
parsed_uri = urlparse.urlparse(uri)
self_link = build_base_uri(parsed_uri)
old_query_params = _get_old_query_params(parsed_uri)
if old_query_params:
self_link += '?' + '&'.join(old_query_params)
if (measurement
and measurement[0]
and measurement[0]['measurements']
and len(measurement[0]['measurements']) > limit):
new_offset = measurement[0]['measurements'][limit - 1][0]
next_link = build_base_uri(parsed_uri)
new_query_params = [u'offset' + '=' + urllib.quote(
new_offset.encode('utf8'), safe='')]
_get_old_query_params_except_offset(new_query_params, parsed_uri)
if new_query_params:
next_link += '?' + '&'.join(new_query_params)
truncated_measurement = {u'dimensions': measurement[0]['dimensions'],
u'measurements': (measurement[0]
['measurements'][:limit]),
u'name': measurement[0]['name'],
u'columns': measurement[0]['columns'],
u'id': new_offset}
resource = {u'links': ([{u'rel': u'self',
u'href': self_link.decode('utf8')},
{u'rel': u'next',
u'href': next_link.decode('utf8')}]),
u'elements': truncated_measurement}
else:
resource = {u'links': ([{u'rel': u'self',
u'href': self_link.decode('utf8')}]),
u'elements': measurement}
return resource
def _get_old_query_params(parsed_uri):
old_query_params = []
if parsed_uri.query:
for query_param in parsed_uri.query.split('&'):
query_param_name, query_param_val = query_param.split('=')
old_query_params.append(urllib.quote(
query_param_name.encode('utf8'), safe='')
+ "="
+ urllib.quote(query_param_val.encode('utf8'), safe=''))
return old_query_params
def _get_old_query_params_except_offset(new_query_params, parsed_uri):
if parsed_uri.query:
for query_param in parsed_uri.query.split('&'):
query_param_name, query_param_val = query_param.split('=')
if query_param_name.lower() != 'offset':
new_query_params.append(urllib.quote(
query_param_name.encode(
'utf8'), safe='') + "=" + urllib.quote(
query_param_val.encode(
'utf8'), safe=''))
def paginate_statistics(statistic, uri, limit):
parsed_uri = urlparse.urlparse(uri)
self_link = build_base_uri(parsed_uri)
old_query_params = _get_old_query_params(parsed_uri)
if old_query_params:
self_link += '?' + '&'.join(old_query_params)
if (statistic
and statistic[0]
and statistic[0]['statistics']
and len(statistic[0]['statistics']) > limit):
new_offset = (
statistic[0]['statistics'][limit - 1][0])
next_link = build_base_uri(parsed_uri)
new_query_params = [u'offset' + '=' + urllib.quote(
new_offset.encode('utf8'), safe='')]
_get_old_query_params_except_offset(new_query_params, parsed_uri)
if new_query_params:
next_link += '?' + '&'.join(new_query_params)
truncated_statistic = {u'dimensions': statistic[0]['dimensions'],
u'statistics': (statistic[0]['statistics'][
:limit]),
u'name': statistic[0]['name'],
u'columns': statistic[0]['columns'],
u'id': new_offset}
resource = {u'links': ([{u'rel': u'self',
u'href': self_link.decode('utf8')},
{u'rel': u'next',
u'href': next_link.decode('utf8')}]),
u'elements': truncated_statistic}
else:
resource = {u'links': ([{u'rel': u'self',
u'href': self_link.decode('utf8')}]),
u'elements': statistic}
return resource
def build_base_uri(parsed_uri):
return parsed_uri.scheme + '://' + parsed_uri.netloc + parsed_uri.path
def get_link(uri, resource_id, rel='self'):
"""Returns a link dictionary containing href, and rel.
:param uri: the http request.uri.
:param resource_id: the id of the resource
"""
parsed_uri = urlparse.urlparse(uri)
href = build_base_uri(parsed_uri)
href += '/' + resource_id
if rel:
link_dict = dict(href=href, rel=rel)
else:
link_dict = dict(href=href)
return link_dict
def add_links_to_resource(resource, uri, rel='self'):
"""Adds links to the given resource dictionary.
:param resource: the resource dictionary you wish to add links.
:param uri: the http request.uri.
"""
resource['links'] = [get_link(uri, resource['id'], rel)]
return resource
def add_links_to_resource_list(resourcelist, uri):
"""Adds links to the given resource dictionary list.
:param resourcelist: the list of resources you wish to add links.
:param uri: the http request.uri.
"""
for resource in resourcelist:
add_links_to_resource(resource, uri)
return resourcelist
def read_http_resource(req):
"""Read from http request and return json.
:param req: the http request.
"""
try:
msg = req.stream.read()
json_msg = simplejson.loads(msg)
return json_msg
except ValueError as ex:
LOG.debug(ex)
raise falcon.HTTPBadRequest(
'Bad request',
'Request body is not valid JSON')
def raise_not_found_exception(resource_name, resource_id, tenant_id):
"""Provides exception for not found requests (update, delete, list).
:param resource_name: the name of the resource.
:param resource_id: id of the resource.
:param tenant_id: id of the tenant
"""
msg = 'No %s method exists for tenant_id = %s id = %s' % (
resource_name, tenant_id, resource_id)
raise falcon.HTTPError(
status='404 Not Found',
title='Not Found',
description=msg,
code=404)
def dumpit_utf8(thingy):
return json.dumps(thingy, ensure_ascii=False).encode('utf8')
def str_2_bool(s):
return s.lower() in ("true")
def get_limit(req):
limit = get_query_param(req, 'limit')
if limit:
if limit.isdigit():
limit = int(limit)
if limit > constants.PAGE_LIMIT:
return constants.PAGE_LIMIT
else:
return limit
else:
raise falcon.HTTPBadRequest("Invalid limit",
"Limit "
"parameter must "
"be "
"an integer")
else:
return constants.PAGE_LIMIT