diff --git a/nova/api/validation/parameter_types.py b/nova/api/validation/parameter_types.py index 9362aaf4ce4a..e17443fd2f58 100644 --- a/nova/api/validation/parameter_types.py +++ b/nova/api/validation/parameter_types.py @@ -21,6 +21,14 @@ import unicodedata import six +from nova.i18n import _ + + +class ValidationRegex(object): + def __init__(self, regex, reason): + self.regex = regex + self.reason = reason + def _is_printable(char): """determine if a unicode code point is printable. @@ -110,10 +118,12 @@ def _build_regex_range(ws=True, invert=False, exclude=None): valid_name_regex_base = '^(?![%s])[%s]*(? 0: + if isinstance(ex.cause, exception.InvalidName): + detail = ex.cause.format_message() + elif len(ex.path) > 0: + # NOTE: For whole OpenStack message consistency, this error + # message has been written as the similar format of WSME. detail = _("Invalid input for field/attribute %(path)s." " Value: %(value)s. %(message)s") % { 'path': ex.path.pop(), 'value': ex.instance, diff --git a/nova/exception.py b/nova/exception.py index 837c2a5ecc11..b8d5956b8916 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -420,6 +420,11 @@ class InvalidStrTime(Invalid): msg_fmt = _("Invalid datetime string: %(reason)s") +class InvalidName(Invalid): + msg_fmt = _("An invalid 'name' value was provided. " + "The name must be: %(reason)s") + + class InstanceInvalidState(Invalid): msg_fmt = _("Instance %(instance_uuid)s in %(attr)s %(state)s. Cannot " "%(method)s while the instance is in this state.") diff --git a/nova/tests/unit/test_api_validation.py b/nova/tests/unit/test_api_validation.py index 69c89bea44d5..f9a34cff0195 100644 --- a/nova/tests/unit/test_api_validation.py +++ b/nova/tests/unit/test_api_validation.py @@ -21,6 +21,7 @@ import six from nova.api.openstack import api_version_request as api_version from nova.api import validation from nova.api.validation import parameter_types +from nova.api.validation import validators from nova import exception from nova import test @@ -36,7 +37,7 @@ class FakeRequest(object): class ValidationRegex(test.NoDBTestCase): def test_cell_names(self): - cellre = re.compile(parameter_types.valid_cell_name_regex) + cellre = re.compile(parameter_types.valid_cell_name_regex.regex) self.assertTrue(cellre.search('foo')) self.assertFalse(cellre.search('foo.bar')) self.assertFalse(cellre.search('foo@bar')) @@ -108,6 +109,18 @@ class APIValidationTestCase(test.NoDBTestCase): self.fail('Any exception does not happen.') +class FormatCheckerTestCase(test.NoDBTestCase): + + def test_format_checker_failed(self): + format_checker = validators.FormatChecker() + exc = self.assertRaises(exception.InvalidName, + format_checker.check, " ", "name") + self.assertEqual("An invalid 'name' value was provided. The name must " + "be: printable characters. " + "Can not start or end with whitespace.", + exc.format_message()) + + class MicroversionsSchemaTestCase(APIValidationTestCase): def setUp(self): @@ -668,6 +681,122 @@ class HostnameIPaddressTestCase(APIValidationTestCase): expected_detail=detail) +class CellNameTestCase(APIValidationTestCase): + + def setUp(self): + super(CellNameTestCase, self).setUp() + schema = { + 'type': 'object', + 'properties': { + 'foo': parameter_types.cell_name, + }, + } + + @validation.schema(request_body_schema=schema) + def post(req, body): + return 'Validation succeeded.' + + self.post = post + + def test_validate_name(self): + self.assertEqual('Validation succeeded.', + self.post(body={'foo': 'abc'}, + req=FakeRequest())) + self.assertEqual('Validation succeeded.', + self.post(body={'foo': 'my server'}, + req=FakeRequest())) + self.assertEqual('Validation succeeded.', + self.post(body={'foo': u'\u0434'}, req=FakeRequest())) + self.assertEqual('Validation succeeded.', + self.post(body={'foo': u'\u0434\u2006\ufffd'}, + req=FakeRequest())) + + def test_validate_name_fails(self): + error = ("An invalid 'name' value was provided. The name must be: " + "printable characters except !, ., @. " + "Can not start or end with whitespace.") + + should_fail = (' ', + ' server', + 'server ', + u'a\xa0', # trailing unicode space + u'\uffff', # non-printable unicode + 'abc!def', + 'abc.def', + 'abc@def') + + for item in should_fail: + self.check_validation_error(self.post, body={'foo': item}, + expected_detail=error) + + # four-byte unicode, if supported by this python build + try: + self.check_validation_error(self.post, body={'foo': u'\U00010000'}, + expected_detail=error) + except ValueError: + pass + + +class CellNameLeadingTrailingSpacesTestCase(APIValidationTestCase): + + def setUp(self): + super(CellNameLeadingTrailingSpacesTestCase, self).setUp() + schema = { + 'type': 'object', + 'properties': { + 'foo': parameter_types.cell_name_leading_trailing_spaces, + }, + } + + @validation.schema(request_body_schema=schema) + def post(req, body): + return 'Validation succeeded.' + + self.post = post + + def test_validate_name(self): + self.assertEqual('Validation succeeded.', + self.post(body={'foo': 'abc'}, + req=FakeRequest())) + self.assertEqual('Validation succeeded.', + self.post(body={'foo': 'my server'}, + req=FakeRequest())) + self.assertEqual('Validation succeeded.', + self.post(body={'foo': u'\u0434'}, req=FakeRequest())) + self.assertEqual('Validation succeeded.', + self.post(body={'foo': u'\u0434\u2006\ufffd'}, + req=FakeRequest())) + self.assertEqual('Validation succeeded.', + self.post(body={'foo': ' my server'}, + req=FakeRequest())) + self.assertEqual('Validation succeeded.', + self.post(body={'foo': 'my server '}, + req=FakeRequest())) + + def test_validate_name_fails(self): + error = ("An invalid 'name' value was provided. The name must be: " + "printable characters except !, ., @, " + "with at least one non space character") + + should_fail = ( + ' ', + u'\uffff', # non-printable unicode + 'abc!def', + 'abc.def', + 'abc@def') + + for item in should_fail: + self.check_validation_error(self.post, body={'foo': item}, + expected_detail=error) + + # four-byte unicode, if supported by this python build + try: + self.check_validation_error(self.post, body={'foo': u'\U00010000'}, + expected_detail=error) + except ValueError: + pass + + class NameTestCase(APIValidationTestCase): def setUp(self): @@ -701,54 +830,25 @@ class NameTestCase(APIValidationTestCase): req=FakeRequest())) def test_validate_name_fails(self): - detail = (u"Invalid input for field/attribute foo. Value: ." - " ' ' does not match .*") - self.check_validation_error(self.post, body={'foo': ' '}, - expected_detail=detail) + error = ("An invalid 'name' value was provided. The name must be: " + "printable characters. " + "Can not start or end with whitespace.") - detail = ("Invalid input for field/attribute foo. Value: server." - " ' server' does not match .*") - self.check_validation_error(self.post, body={'foo': ' server'}, - expected_detail=detail) + should_fail = (' ', + ' server', + 'server ', + u'a\xa0', # trailing unicode space + u'\uffff', # non-printable unicode + ) - detail = ("Invalid input for field/attribute foo. Value: server ." - " 'server ' does not match .*") - self.check_validation_error(self.post, body={'foo': 'server '}, - expected_detail=detail) - - detail = ("Invalid input for field/attribute foo. Value: a." - " ' a' does not match .*") - self.check_validation_error(self.post, body={'foo': ' a'}, - expected_detail=detail) - - detail = ("Invalid input for field/attribute foo. Value: a ." - " 'a ' does not match .*") - self.check_validation_error(self.post, body={'foo': 'a '}, - expected_detail=detail) - - # NOTE(stpierre): Quoting for the unicode values in the error - # messages below gets *really* messy, so we just wildcard it - # out. (e.g., '.* does not match'). In practice, we don't - # particularly care about that part of the error message. - - # trailing unicode space - detail = (u"Invalid input for field/attribute foo. Value: a\xa0." - u' .* does not match .*') - self.check_validation_error(self.post, body={'foo': u'a\xa0'}, - expected_detail=detail) - - # non-printable unicode - detail = (u"Invalid input for field/attribute foo. Value: \uffff." - u" .* does not match .*") - self.check_validation_error(self.post, body={'foo': u'\uffff'}, - expected_detail=detail) + for item in should_fail: + self.check_validation_error(self.post, body={'foo': item}, + expected_detail=error) # four-byte unicode, if supported by this python build try: - detail = (u"Invalid input for field/attribute foo. Value: " - u"\U00010000. .* does not match .*") self.check_validation_error(self.post, body={'foo': u'\U00010000'}, - expected_detail=detail) + expected_detail=error) except ValueError: pass @@ -799,34 +899,23 @@ class NameWithLeadingTrailingSpacesTestCase(APIValidationTestCase): req=FakeRequest())) def test_validate_name_fails(self): - detail = (u"Invalid input for field/attribute foo. Value: ." - u" ' ' does not match .*") - self.check_validation_error(self.post, body={'foo': ' '}, - expected_detail=detail) + error = ("An invalid 'name' value was provided. The name must be: " + "printable characters with at least one non space character") - # NOTE(stpierre): Quoting for the unicode values in the error - # messages below gets *really* messy, so we just wildcard it - # out. (e.g., '.* does not match'). In practice, we don't - # particularly care about that part of the error message. + should_fail = ( + ' ', + u'\xa0', # unicode space + u'\uffff', # non-printable unicode + ) - # unicode space - detail = (u"Invalid input for field/attribute foo. Value: \xa0." - u' .* does not match .*') - self.check_validation_error(self.post, body={'foo': u'\xa0'}, - expected_detail=detail) - - # non-printable unicode - detail = (u"Invalid input for field/attribute foo. Value: \uffff." - u" .* does not match .*") - self.check_validation_error(self.post, body={'foo': u'\uffff'}, - expected_detail=detail) + for item in should_fail: + self.check_validation_error(self.post, body={'foo': item}, + expected_detail=error) # four-byte unicode, if supported by this python build try: - detail = (u"Invalid input for field/attribute foo. Value: " - u"\U00010000. .* does not match .*") self.check_validation_error(self.post, body={'foo': u'\U00010000'}, - expected_detail=detail) + expected_detail=error) except ValueError: pass @@ -1193,7 +1282,7 @@ class Ipv6TestCase(APIValidationTestCase): detail = ("Invalid input for field/attribute foo. Value: localhost." " 'localhost' is not a 'ipv6'") self.check_validation_error(self.post, body={'foo': 'localhost'}, - expected_detail=detail) + expected_detail=detail) detail = ("Invalid input for field/attribute foo." " Value: 192.168.0.100. '192.168.0.100' is not a 'ipv6'")