Merge "Fix dimension validation of alarms"

This commit is contained in:
Jenkins 2016-04-15 12:44:30 +00:00 committed by Gerrit Code Review
commit 37e275c4bd
10 changed files with 175 additions and 60 deletions

View File

@ -108,7 +108,7 @@ public final class DimensionValidation {
/**
* Validates that the given {@code dimensions} are valid.
*
*
* @throws WebApplicationException if validation fails
*/
public static void validate(Map<String, String> dimensions) {
@ -118,27 +118,8 @@ public final class DimensionValidation {
String value = dimension.getValue();
// General validations
if (Strings.isNullOrEmpty(name))
throw Exceptions.unprocessableEntity("Dimension name cannot be empty");
if (Strings.isNullOrEmpty(value))
throw Exceptions.unprocessableEntity("Dimension %s cannot have an empty value", name);
if (name.length() > 255)
throw Exceptions.unprocessableEntity("Dimension name %s must be 255 characters or less",
name);
if (value.length() > 255)
throw Exceptions.unprocessableEntity("Dimension value %s must be 255 characters or less",
value);
// Dimension names that start with underscores are reserved for internal use only.
if (name.startsWith("_")) {
throw Exceptions.unprocessableEntity("Dimension name cannot start with underscore (_)",
name);
}
if (!VALID_DIMENSION_NAME.matcher(name).matches())
throw Exceptions.unprocessableEntity(
"Dimension name %s may not contain: %s", name, INVALID_CHAR_STRING);
if (!VALID_DIMENSION_NAME.matcher(value).matches())
throw Exceptions.unprocessableEntity(
"Dimension value %s may not contain: %s", value, INVALID_CHAR_STRING);
validateDimensionName(name);
validateDimensionValue(value, name, false);
}
}
@ -146,26 +127,77 @@ public final class DimensionValidation {
* Validates a list of dimension names
* @param names
*/
public static void validateNames(List<String> names) {
if(names != null) {
if (names != null) {
for (String name : names) {
if (Strings.isNullOrEmpty(name)) {
throw Exceptions.unprocessableEntity("Dimension name cannot be empty");
}
if (name.length() > 255) {
throw Exceptions.unprocessableEntity("Dimension name '%s' must be 255 characters or less",
name);
}
// Dimension names that start with underscores are reserved for internal use only.
if (name.startsWith("_")) {
throw Exceptions.unprocessableEntity("Dimension name '%s' cannot start with underscore (_)",
name);
}
if (!VALID_DIMENSION_NAME.matcher(name).matches())
throw Exceptions.unprocessableEntity(
"Dimension name '%s' may not contain: %s", name, INVALID_CHAR_STRING);
validateDimensionName(name);
}
}
}
/**
* Validates a dimension name
* @param name Dimension name
*/
public static void validateName(String name) {
validateDimensionName(name);
}
/**
* Validates a dimension value
* @param value Dimension value
* @param name Dimension name of the value
*/
public static void validateValue(String value, String name) {
validateDimensionValue(value, name, true);
}
/**
* Validates a dimension name
* @param name Dimension name
*/
public static void validateDimensionName(String name) {
if (Strings.isNullOrEmpty(name)) {
throw Exceptions.unprocessableEntity("Dimension name cannot be empty");
}
if (name.length() > 255) {
throw Exceptions.unprocessableEntity("Dimension name '%s' must be 255 characters or less",
name);
}
// Dimension name that start with underscores are reserved for internal use only.
if (name.startsWith("_")) {
throw Exceptions.unprocessableEntity("Dimension name '%s' cannot start with underscore (_)",
name);
}
if (!VALID_DIMENSION_NAME.matcher(name).matches()) {
throw Exceptions.unprocessableEntity(
"Dimension name '%s' may not contain: %s", name, INVALID_CHAR_STRING);
}
}
/**
* Validates a dimension value
* @param value Dimension value
* @param name Dimension name of the value
* @param nullValueOk whether or not a null value is valid
*/
public static void validateDimensionValue(String value, String name, boolean nullValueOk) {
if (value == null && nullValueOk) {
return;
}
if (Strings.isNullOrEmpty(value)) {
throw Exceptions.unprocessableEntity("Dimension '%s' cannot have an empty value", name);
}
if (value.length() > 255) {
throw Exceptions.unprocessableEntity("Dimension '%s' value '%s' must be 255 characters or less",
name, value);
}
if (!VALID_DIMENSION_NAME.matcher(value).matches()) {
throw Exceptions.unprocessableEntity(
"Dimension '%s' value '%s' may not contain: %s", name, value,
INVALID_CHAR_STRING);
}
}
}

View File

@ -42,6 +42,7 @@ public final class Validation {
private static final Splitter COMMA_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
private static final Splitter COLON_SPLITTER = Splitter.on(':').omitEmptyStrings().trimResults().limit(2);
private static final Splitter SPACE_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults();
private static final Splitter VERTICAL_BAR_SPLITTER = Splitter.on('|').omitEmptyStrings().trimResults();
private static final Joiner SPACE_JOINER = Joiner.on(' ');
private static final DateTimeFormatter ISO_8601_FORMATTER = ISODateTimeFormat
.dateOptionalTimeParser().withZoneUTC();
@ -95,10 +96,21 @@ public final class Validation {
Map<String, String> dimensions = new HashMap<String, String>();
for (String dimensionStr : COMMA_SPLITTER.split(dimensionsStr)) {
String[] dimensionArr = Iterables.toArray(COLON_SPLITTER.split(dimensionStr), String.class);
if (dimensionArr.length == 2)
dimensions.put(dimensionArr[0], dimensionArr[1]);
if (dimensionArr.length == 1)
if (dimensionArr.length == 1) {
DimensionValidation.validateName(dimensionArr[0]);
dimensions.put(dimensionArr[0], "");
} else if (dimensionArr.length > 1) {
DimensionValidation.validateName(dimensionArr[0]);
if (dimensionArr[1].contains("|")) {
List<String> dimensionValueArr = VERTICAL_BAR_SPLITTER.splitToList(dimensionArr[1]);
for (String dimensionValue : dimensionValueArr) {
DimensionValidation.validateValue(dimensionValue, dimensionArr[0]);
}
} else {
DimensionValidation.validateValue(dimensionArr[1], dimensionArr[0]);
}
dimensions.put(dimensionArr[0], dimensionArr[1]);
}
}
//DimensionValidation.validate(dimensions);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
* Copyright (c) 2014,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 a copy of the License at
@ -21,6 +21,7 @@ import java.util.Map;
import javax.ws.rs.WebApplicationException;
import org.apache.commons.lang3.StringUtils;
import org.testng.annotations.Test;
@Test
@ -52,4 +53,49 @@ public class DimensionsTest {
}
});
}
public void shouldValidateKey() {
DimensionValidation.validateName("this.is_a.valid-key");
}
@Test(expectedExceptions = WebApplicationException.class)
public void shouldErrorOnValidateKeyWithEmptyKey() {
DimensionValidation.validateName("");
}
@Test(expectedExceptions = WebApplicationException.class)
public void shouldErrorOnValidateKeyWithLongKey() {
String key = StringUtils.repeat("A", 256);
DimensionValidation.validateName(key);
}
@Test(expectedExceptions = WebApplicationException.class)
public void shouldErrorOnValidateKeyWithStartingUnderscore() {
DimensionValidation.validateName("_key");
}
@Test(expectedExceptions = WebApplicationException.class)
public void shouldErrorOnValidateKeyWithInvalidCharKey() {
DimensionValidation.validateName("this{}that");
}
public void shouldValidateValue() {
DimensionValidation.validateValue("this.is_a.valid-value", "valid_name");
}
@Test(expectedExceptions = WebApplicationException.class)
public void shouldErrorOnValidateValueWithEmptyValue() {
DimensionValidation.validateValue("", "valid_name");
}
@Test(expectedExceptions = WebApplicationException.class)
public void shouldErrorOnValidateValueWithLongValue() {
String value = StringUtils.repeat("A", 256);
DimensionValidation.validateValue(value, "valid_name");
}
@Test(expectedExceptions = WebApplicationException.class)
public void shouldErrorOnValidateValueWithInvalidCharValue() {
DimensionValidation.validateValue("this{}that", "valid_name");
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
* Copyright (c) 2014,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 a copy of the License at
@ -221,7 +221,7 @@ public class MetricResourceTest extends AbstractMonApiResourceTest {
valueMeta));
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
"Dimension blah cannot have an empty value");
"Dimension 'blah' cannot have an empty value");
}
public void shouldErrorOnCreateWithMissingDimensionValue() {
@ -235,7 +235,7 @@ public class MetricResourceTest extends AbstractMonApiResourceTest {
valueMeta));
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
"Dimension flavor_id cannot have an empty value");
"Dimension 'flavor_id' cannot have an empty value");
}
public void shouldErrorOnCreateWithTooLongDimensionName() {
@ -257,7 +257,7 @@ public class MetricResourceTest extends AbstractMonApiResourceTest {
.matches(
"unprocessable_entity",
422,
"Dimension name 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789abc must be 255 characters or less");
"Dimension name '012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789abc' must be 255 characters or less");
}
public void shouldErrorOnCreateWithTooLongDimensionValue() {
@ -279,7 +279,7 @@ public class MetricResourceTest extends AbstractMonApiResourceTest {
.matches(
"unprocessable_entity",
422,
"Dimension value 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789abc must be 255 characters or less");
"Dimension 'abc' value '012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789abc' must be 255 characters or less");
}
public void shouldErrorOnCreateWithHighTimestamp() {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
* Copyright (c) 2014,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 a copy of the License at
@ -52,7 +52,8 @@ public final class ErrorMessages {
@Nullable String detailsPrefix) {
assertEquals(rootKey, faultType);
assertEquals(message.code, code);
assertTrue(message.message.startsWith(messagePrefix), message.message);
assertTrue(message.message.startsWith(messagePrefix),
String.format("String '%s' does not start with '%s'", message.message, messagePrefix));
if (detailsPrefix != null)
assertTrue(message.details.startsWith(detailsPrefix), message.details);
}

View File

@ -330,9 +330,9 @@ class AlarmsRepository(sql_repository.SQLRepository,
if '|' in parsed_dimension[1]:
values = parsed_dimension[1].encode('utf8').split('|')
sub_values_cond = []
for j, value in values:
for j, value in enumerate(values):
sub_md_value = "b_md_value_{}_{}".format(i, j)
sub_values_cond.append = (md.c.value == bindparam(sub_md_value))
sub_values_cond.append(md.c.value == bindparam(sub_md_value))
parms[sub_md_value] = value
values_cond = or_(*sub_values_cond)
values_cond_flag = True

View File

@ -68,6 +68,10 @@ class TestDimensionValidation(unittest.TestCase):
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz")
self.assertRaises(AssertionError, validation.dimension_key, dim_key)
def test_key_starts_with_underscore(self):
dim_key = '_key'
self.assertRaises(AssertionError, validation.dimension_key, dim_key)
def test_invalid_chars_key(self):
for c in invalid_chars:
dim_key = "this{}that".format(c)

View File

@ -35,6 +35,7 @@ def dimension_key(dkey):
assert isinstance(dkey, (str, unicode)), "Dimension key must be a string"
assert len(dkey) <= 255, "Dimension key must be 255 characters or less"
assert len(dkey) >= 1, "Dimension key cannot be empty"
assert dkey[0] != '_', "Dimension key cannot start with underscore (_)"
assert not restricted_chars.search(dkey), "Invalid characters in dimension name " + dkey

View File

@ -165,12 +165,15 @@ class Alarms(alarms_api_v2.AlarmsV2API,
try:
assert isinstance(dimensions, list)
for dimension in dimensions:
name_value = dimension.split('=')
name_value = dimension.split(':')
validation.dimension_key(name_value[0])
if len(name_value) > 1 and '|' in name_value[1]:
values = name_value[1].split('|')
for value in values:
validation.dimension_value(value)
if len(name_value) > 1:
if '|' in name_value[1]:
values = name_value[1].split('|')
for value in values:
validation.dimension_value(value)
else:
validation.dimension_value(name_value[1])
except Exception as e:
raise HTTPUnprocessableEntityError("Unprocessable Entity", e.message)

View File

@ -80,8 +80,8 @@ class TestAlarms(base.BaseMonascaTest):
alarm_definition_ids, expected_metric \
= self._create_alarms_for_test_alarms(num=1)
for key in expected_metric['dimensions']:
value = expected_metric['dimensions'][key]
query_parms = '?metric_dimensions=' + key + ':' + value
value = expected_metric['dimensions'][key]
query_parms = '?metric_dimensions=' + key + ':' + value
resp, response_body = self.monasca_client.list_alarms(query_parms)
self._verify_list_alarms_elements(resp, response_body,
expect_num_elements=1)
@ -91,6 +91,22 @@ class TestAlarms(base.BaseMonascaTest):
self.assertEqual(alarm_definition_ids[0],
element['alarm_definition']['id'])
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_list_alarms_by_metric_dimensions_key_exceeds_max_length(self):
key = 'x' * (constants.MAX_ALARM_METRIC_DIMENSIONS_KEY_LENGTH + 1)
query_parms = '?metric_dimensions=' + key
self.assertRaises(exceptions.UnprocessableEntity,
self.monasca_client.list_alarms, query_parms)
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_list_alarms_by_metric_dimensions_value_exceeds_max_length(self):
value = 'x' * (constants.MAX_ALARM_METRIC_DIMENSIONS_VALUE_LENGTH + 1)
query_parms = '?metric_dimensions=key:' + value
self.assertRaises(exceptions.UnprocessableEntity,
self.monasca_client.list_alarms, query_parms)
@test.attr(type="gate")
def test_list_alarms_by_multiple_metric_dimensions(self):
metric = helpers.create_metric(
@ -196,7 +212,7 @@ class TestAlarms(base.BaseMonascaTest):
resp, response_body = self.monasca_client.list_alarms('?metric_name=' + metric_name)
self.assertEqual(200, resp.status)
if len(response_body['elements']) >= 3:
return
break
time.sleep(constants.RETRY_WAIT_SECS)
if i >= constants.MAX_RETRIES - 1:
self.fail("Timeout creating alarms, required 3 but found {}".format(