From 02bf7a9858b88bcf1e0c8a7bccec6628f2d658f1 Mon Sep 17 00:00:00 2001 From: Kaiyan Sheng Date: Wed, 16 Mar 2016 15:18:00 -0600 Subject: [PATCH] Validate value meta key and value in python api Add validation for value meta in python api Add unit test and tempest tests on value meta validation Change the documentation Change-Id: I4633785211e83d22f016ecfbdcbca3896043cf00 --- docs/monasca-api-spec.md | 4 +- monasca_api/tests/test_validation.py | 41 ++++++++++++++++++- monasca_api/v2/common/validation.py | 27 ++++++++++++ monasca_api/v2/reference/metrics.py | 7 ++-- monasca_tempest_tests/tests/api/constants.py | 5 ++- .../tests/api/test_metrics.py | 26 ++++++++++++ 6 files changed, 102 insertions(+), 8 deletions(-) diff --git a/docs/monasca-api-spec.md b/docs/monasca-api-spec.md index 070e66d36..2e017cc9a 100644 --- a/docs/monasca-api-spec.md +++ b/docs/monasca-api-spec.md @@ -361,7 +361,7 @@ Optionally, a measurement may also contain extra data about the value which is k For an example of how value meta is used, imagine this metric: http_status{url: http://localhost:8080/healthcheck, hostname=devstack, service=object-storage}. The measurements for this metric have a value of either 1 or 0 depending if the status check succeeded. If the check fails, it would be helpful to have the actual http status code and error message if possible. So instead of just a value, the measurement will be something like: {Timestamp=now(), value=1, value_meta{http_rc=500, error=“Error accessing MySQL”}} -Up to 16 separate key/value pairs of value meta are allowed per measurement. The keys are required and are trimmed of leading and trailing whitespace and have a maximum length of 255 characters. The value is a string and has a maximum length of 2048 characters. The value can be an empty string. Whitespace is not trimmed from the values. +Up to 16 separate key/value pairs of value meta are allowed per measurement. The keys are required and are trimmed of leading and trailing whitespace and have a maximum length of 255 characters. The value is a string and value meta (with key, value and '{"":""}' combined) has a maximum length of 2048 characters. The value can be an empty string. Whitespace is not trimmed from the values. ## Alarm Definitions and Alarms @@ -881,7 +881,7 @@ Consists of a single metric object or an array of metric objects. A metric has t * dimensions ({string(255): string(255)}, optional) - A dictionary consisting of (key, value) pairs used to uniquely identify a metric. * timestamp (string, required) - The timestamp in milliseconds from the Epoch. * value (float, required) - Value of the metric. Values with base-10 exponents greater than 126 or less than -130 are truncated. -* value_meta ({string(255): string(2048)}, optional) - A dictionary consisting of (key, value) pairs used to add information about the value. +* value_meta ({string(255): string}(2048), optional) - A dictionary consisting of (key, value) pairs used to add information about the value. Value_meta key value combinations must be 2048 characters or less including '{"":""}' 7 characters total from every json string. The name and dimensions are used to uniquely identify a metric. diff --git a/monasca_api/tests/test_validation.py b/monasca_api/tests/test_validation.py index 899f90bdf..99a31243a 100644 --- a/monasca_api/tests/test_validation.py +++ b/monasca_api/tests/test_validation.py @@ -1,6 +1,5 @@ -# Copyright 2015 Hewlett-Packard +# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development Company LP # Copyright 2015 Cray Inc. All Rights Reserved. -# Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. # # 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 @@ -95,6 +94,44 @@ class TestDimensionValidation(unittest.TestCase): self.assertRaises(AssertionError, validation.dimension_value, dim_value) +class TestValueMetaValidation(unittest.TestCase): + def test_valid_name(self): + value_meta_name = "this.is_a.valid-name" + value_meta = {value_meta_name: 'value_meta_value'} + validation.validate_value_meta(value_meta) + self.assertTrue(True) + + def test_nonstring_name(self): + value_meta_name = 123456 + value_meta = {value_meta_name: 'value_meta_value'} + self.assertRaises(AssertionError, validation.validate_value_meta, + value_meta) + + def test_long_name(self): + value_meta_name = "x" * 256 + value_meta = {value_meta_name: 'value_meta_value'} + self.assertRaises(AssertionError, validation.validate_value_meta, + value_meta) + + def test_valid_value(self): + value_meta_value = "this.is_a.valid-value" + value_meta = {'value_meta_name': value_meta_value} + validation.validate_value_meta(value_meta) + self.assertTrue(True) + + def test_nonstring_value(self): + value_meta_value = 123456 + value_meta = {'value_meta_name': value_meta_value} + self.assertRaises(AssertionError, validation.validate_value_meta, + value_meta) + + def test_long_value_meta(self): + value_meta_value = "x" * 2048 + value_meta = {'value_meta_name': value_meta_value} + self.assertRaises(AssertionError, validation.validate_value_meta, + value_meta) + + class TestRoleValidation(unittest.TestCase): def test_role_valid(self): diff --git a/monasca_api/v2/common/validation.py b/monasca_api/v2/common/validation.py index ab92acd4d..87e8b76f8 100644 --- a/monasca_api/v2/common/validation.py +++ b/monasca_api/v2/common/validation.py @@ -14,6 +14,7 @@ from monasca_api.v2.common.exceptions import HTTPUnprocessableEntityError +import json import re invalid_chars = "<>={}(),\"\\\\|;&" @@ -23,6 +24,12 @@ VALID_ALARM_STATES = ["ALARM", "OK", "UNDETERMINED"] VALID_ALARM_DEFINITION_SEVERITIES = ["LOW", "MEDIUM", "HIGH", "CRITICAL"] +VALUE_META_MAX_NUMBER = 16 + +VALUE_META_MAX_LENGTH = 2048 + +VALUE_META_NAME_MAX_LENGTH = 255 + def metric_name(name): assert isinstance(name, (str, unicode)), "Metric name must be a string" @@ -74,3 +81,23 @@ def validate_sort_by(sort_by_list, allowed_sort_by): raise HTTPUnprocessableEntityError("Unprocessable Entity", "sort_by value {} must be 'asc' or 'desc'".format( sort_by_values[1])) + + +def validate_value_meta(value_meta): + value_meta_string = json.dumps(value_meta) + # entries + assert len(value_meta) <= VALUE_META_MAX_NUMBER, "ValueMeta entries must be {} or less".format( + VALUE_META_MAX_NUMBER) + # total length + assert len(value_meta_string) <= VALUE_META_MAX_LENGTH, \ + "ValueMeta name value combinations must be {} characters or less".format( + VALUE_META_MAX_LENGTH) + for name in value_meta: + # name + assert isinstance(name, (str, unicode)), "ValueMeta name must be a string" + assert len(name) <= VALUE_META_NAME_MAX_LENGTH, "ValueMeta name must be {} characters or less".format( + VALUE_META_NAME_MAX_LENGTH) + assert len(name) >= 1, "ValueMeta name cannot be empty" + # value + assert isinstance(value_meta[name], (str, unicode)), "ValueMeta value must be a string" + assert len(value_meta[name]) >= 1, "ValueMeta value cannot be empty" diff --git a/monasca_api/v2/reference/metrics.py b/monasca_api/v2/reference/metrics.py index 446355576..c4d788bb2 100644 --- a/monasca_api/v2/reference/metrics.py +++ b/monasca_api/v2/reference/metrics.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# (C) Copyright 2014, 2016 Hewlett Packard Enterprise Development Company LP # # 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 @@ -78,7 +78,7 @@ class Metrics(metrics_api_v2.MetricsV2API): else: self._validate_single_metric(metrics) except Exception as ex: - LOG.debug(ex) + LOG.exception(ex) raise HTTPUnprocessableEntityError('Unprocessable Entity', ex.message) def _validate_single_metric(self, metric): @@ -89,9 +89,10 @@ class Metrics(metrics_api_v2.MetricsV2API): for dimension_key in metric['dimensions']: validation.dimension_key(dimension_key) validation.dimension_value(metric['dimensions'][dimension_key]) + if "value_meta" in metric: + validation.validate_value_meta(metric['value_meta']) def _send_metrics(self, metrics): - try: self._message_queue.send_message_batch(metrics) except message_queue_exceptions.MessageQueueException as ex: diff --git a/monasca_tempest_tests/tests/api/constants.py b/monasca_tempest_tests/tests/api/constants.py index baa582ec1..1ccc504b3 100644 --- a/monasca_tempest_tests/tests/api/constants.py +++ b/monasca_tempest_tests/tests/api/constants.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development Company LP # # 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 @@ -39,3 +39,6 @@ MAX_ALARM_METRIC_NAME_LENGTH = 255 MAX_ALARM_METRIC_DIMENSIONS_KEY_LENGTH = 255 MAX_ALARM_METRIC_DIMENSIONS_VALUE_LENGTH = 255 MAX_ALARM_LINK_LENGTH = 512 + +MAX_VALUE_META_NAME_LENGTH = 255 +MAX_VALUE_META_TOTAL_LENGTH = 2048 diff --git a/monasca_tempest_tests/tests/api/test_metrics.py b/monasca_tempest_tests/tests/api/test_metrics.py index a4c6abcef..455548075 100644 --- a/monasca_tempest_tests/tests/api/test_metrics.py +++ b/monasca_tempest_tests/tests/api/test_metrics.py @@ -282,6 +282,32 @@ class TestMetrics(base.BaseMonascaTest): self.monasca_client.create_metrics, metric) + @test.attr(type='gate') + @test.attr(type=['negative']) + def test_create_metric_with_value_meta_name_exceeds_max_length(self): + long_value_meta_name = "x" * (constants.MAX_VALUE_META_NAME_LENGTH + 1) + metric = helpers.create_metric(name='name', + value_meta= + {long_value_meta_name: + "value_meta_value"} + ) + self.assertRaises(exceptions.UnprocessableEntity, + self.monasca_client.create_metrics, + metric) + + @test.attr(type='gate') + @test.attr(type=['negative']) + def test_create_metric_with_value_meta_exceeds_max_length(self): + value_meta_name = "x" + long_value_meta_value = "y" * constants.MAX_VALUE_META_TOTAL_LENGTH + metric = helpers.create_metric(name='name', + value_meta= + {value_meta_name: long_value_meta_value} + ) + self.assertRaises(exceptions.UnprocessableEntity, + self.monasca_client.create_metrics, + metric) + @test.attr(type='gate') def test_list_metrics(self): resp, response_body = self.monasca_client.list_metrics()