Expand valid resource name character set
This allows all printable unicode characters and horizontal whitespace characters in resource names (flavors, aggregates, cells, etc.), as opposed to the rather limited set that was previously available. This follows the principle of not creating unnecessary restrictions. Implements: blueprint relax-resource-name-restrictions Closes-Bug: 1366778 Change-Id: I35104852797dcba4594af4361bf9226e16bfb114
This commit is contained in:
@@ -21,7 +21,7 @@ base_create = {
|
||||
'server': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': parameter_types.name,
|
||||
'name': parameter_types.hostname,
|
||||
'imageRef': parameter_types.image_ref,
|
||||
'flavorRef': parameter_types.flavor_ref,
|
||||
'adminPass': parameter_types.admin_password,
|
||||
@@ -67,7 +67,7 @@ base_update = {
|
||||
'server': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': parameter_types.name,
|
||||
'name': parameter_types.hostname,
|
||||
},
|
||||
# TODO(oomichi): ditto, enable here after all extension schema
|
||||
# patches are merged.
|
||||
@@ -84,7 +84,7 @@ base_rebuild = {
|
||||
'rebuild': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'name': parameter_types.name,
|
||||
'name': parameter_types.hostname,
|
||||
'imageRef': parameter_types.image_ref,
|
||||
'adminPass': parameter_types.admin_password,
|
||||
'metadata': parameter_types.metadata,
|
||||
|
||||
@@ -15,8 +15,42 @@
|
||||
Common parameter types for validating request Body.
|
||||
|
||||
"""
|
||||
|
||||
import copy
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
|
||||
def _is_printable(char):
|
||||
"""determine if a unicode code point is printable.
|
||||
|
||||
This checks if the character is either "other" (mostly control
|
||||
codes), or a non-horizontal space. All characters that don't match
|
||||
those criteria are considered printable; that is: letters;
|
||||
combining marks; numbers; punctuation; symbols; (horizontal) space
|
||||
separators.
|
||||
"""
|
||||
category = unicodedata.category(char)
|
||||
return (not category.startswith("C") and
|
||||
(not category.startswith("Z") or category == "Zs"))
|
||||
|
||||
|
||||
def _get_all_chars():
|
||||
for i in range(0xFFFF):
|
||||
yield unichr(i)
|
||||
|
||||
# build a regex that matches all printable characters. This allows
|
||||
# spaces in the middle of the name. Also note that the regexp below
|
||||
# deliberately allows the empty string. This is so only the constraint
|
||||
# which enforces a minimum length for the name is triggered when an
|
||||
# empty string is tested. Otherwise it is not deterministic which
|
||||
# constraint fails and this causes issues for some unittests when
|
||||
# PYTHONHASHSEED is set randomly.
|
||||
_printable = ''.join(c for c in _get_all_chars() if _is_printable(c))
|
||||
_printable_ws = ''.join(c for c in _get_all_chars()
|
||||
if unicodedata.category(c) == "Zs")
|
||||
|
||||
valid_name_regex = '^(?![%s])[%s]*(?<![%s])$' % (
|
||||
re.escape(_printable_ws), re.escape(_printable), re.escape(_printable_ws))
|
||||
|
||||
|
||||
boolean = {
|
||||
@@ -64,15 +98,7 @@ name = {
|
||||
# stored in the DB and Nova specific parameters.
|
||||
# This definition is used for all their parameters.
|
||||
'type': 'string', 'minLength': 1, 'maxLength': 255,
|
||||
|
||||
# NOTE: Allow to some spaces in middle of name.
|
||||
# Also note that the regexp below deliberately allows and
|
||||
# empty string. This is so only the constraint above
|
||||
# which enforces a minimum length for the name is triggered
|
||||
# when an empty string is tested. Otherwise it is not
|
||||
# deterministic which constraint fails and this causes issues
|
||||
# for some unittests when PYTHONHASHSEED is set randomly.
|
||||
'pattern': '^(?! )[a-zA-Z0-9. _-]*(?<! )$',
|
||||
'pattern': valid_name_regex,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ from oslo.config import cfg
|
||||
from oslo.utils import strutils
|
||||
import six
|
||||
|
||||
from nova.api.validation import parameter_types
|
||||
from nova import context
|
||||
from nova import db
|
||||
from nova import exception
|
||||
@@ -50,7 +51,8 @@ LOG = logging.getLogger(__name__)
|
||||
# create flavor names in locales that use them, however flavor IDs are limited
|
||||
# to ascii characters.
|
||||
VALID_ID_REGEX = re.compile("^[\w\.\- ]*$")
|
||||
VALID_NAME_REGEX = re.compile("^[\w\.\- ]*$", re.UNICODE)
|
||||
VALID_NAME_REGEX = re.compile(parameter_types.valid_name_regex, re.UNICODE)
|
||||
|
||||
# NOTE(dosaboy): This is supposed to represent the maximum value that we can
|
||||
# place into a SQL single precision float so that we can check whether values
|
||||
# are oversize. Postgres and MySQL both define this as their max whereas Sqlite
|
||||
@@ -111,8 +113,8 @@ def create(name, memory, vcpus, root_gb, ephemeral_gb=0, flavorid=None,
|
||||
# ensure name does not contain any special characters
|
||||
valid_name = VALID_NAME_REGEX.search(name)
|
||||
if not valid_name:
|
||||
msg = _("Flavor names can only contain alphanumeric characters, "
|
||||
"periods, dashes, underscores and spaces.")
|
||||
msg = _("Flavor names can only contain printable characters "
|
||||
"and horizontal spaces.")
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
# NOTE(vish): Internally, flavorid is stored as a string but it comes
|
||||
|
||||
@@ -268,8 +268,8 @@ class CellsTestV21(BaseCellsTest):
|
||||
self.assertRaises(self.bad_request,
|
||||
self.controller.create, req, body=body)
|
||||
|
||||
def test_cell_create_name_with_bang_raises(self):
|
||||
body = {'cell': {'name': 'moo!cow',
|
||||
def test_cell_create_name_with_invalid_character_raises(self):
|
||||
body = {'cell': {'name': 'moo\x00cow',
|
||||
'username': 'fred',
|
||||
'password': 'secret',
|
||||
'rpc_host': 'r3.example.org',
|
||||
@@ -684,6 +684,18 @@ class CellsTestV2(CellsTestV21):
|
||||
cell_extension = 'compute_extension:cells'
|
||||
bad_request = exc.HTTPBadRequest
|
||||
|
||||
def test_cell_create_name_with_invalid_character_raises(self):
|
||||
body = {'cell': {'name': 'moo!cow',
|
||||
'username': 'fred',
|
||||
'password': 'secret',
|
||||
'rpc_host': 'r3.example.org',
|
||||
'type': 'parent'}}
|
||||
|
||||
req = self._get_request("cells")
|
||||
req.environ['nova.context'] = self.context
|
||||
self.assertRaises(self.bad_request,
|
||||
self.controller.create, req, body=body)
|
||||
|
||||
def _get_cell_controller(self, ext_mgr):
|
||||
return cells_ext_v2.Controller(ext_mgr)
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ class FlavorManageTestV21(test.NoDBTestCase):
|
||||
self.assertEqual(res.status_code, 400)
|
||||
|
||||
def test_create_invalid_name(self):
|
||||
self.request_body['flavor']['name'] = 'bad !@#!$% name'
|
||||
self.request_body['flavor']['name'] = 'bad !@#!$%\x00 name'
|
||||
self._create_flavor_bad_request_case(self.request_body)
|
||||
|
||||
def test_create_flavor_name_is_whitespace(self):
|
||||
|
||||
@@ -562,34 +562,64 @@ class NameTestCase(APIValidationTestCase):
|
||||
req=FakeRequest()))
|
||||
self.assertEqual('Validation succeeded.',
|
||||
self.post(body={'foo': 'a'}, 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):
|
||||
pattern = "'^(?! )[a-zA-Z0-9. _-]*(?<! )$'"
|
||||
detail = ("Invalid input for field/attribute foo. Value: ."
|
||||
" ' ' does not match %s") % pattern
|
||||
detail = (u"Invalid input for field/attribute foo. Value: ."
|
||||
" ' ' does not match .*")
|
||||
self.check_validation_error(self.post, body={'foo': ' '},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: server."
|
||||
" ' server' does not match %s") % pattern
|
||||
" ' server' does not match .*")
|
||||
self.check_validation_error(self.post, body={'foo': ' server'},
|
||||
expected_detail=detail)
|
||||
|
||||
detail = ("Invalid input for field/attribute foo. Value: server ."
|
||||
" 'server ' does not match %s") % pattern
|
||||
" '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 %s") % pattern
|
||||
" ' 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 %s") % pattern
|
||||
" '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)
|
||||
|
||||
# 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)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class TcpUdpPortTestCase(APIValidationTestCase):
|
||||
|
||||
|
||||
@@ -381,14 +381,15 @@ class CreateInstanceTypeTest(test.TestCase):
|
||||
flavors.create(u'm1.\u5DE8\u5927', 6400, 100, 12000)
|
||||
|
||||
def test_name_with_special_characters(self):
|
||||
# Names can contain alphanumeric and [_.- ]
|
||||
# Names can contain all printable characters
|
||||
flavors.create('_foo.bar-123', 64, 1, 120)
|
||||
|
||||
# Ensure instance types raises InvalidInput for invalid characters.
|
||||
self.assertInvalidInput('foobar#', 64, 1, 120)
|
||||
self.assertInvalidInput('foobar\x00', 64, 1, 120)
|
||||
|
||||
def test_non_ascii_name_with_special_characters(self):
|
||||
self.assertInvalidInput(u'm1.\u5DE8\u5927 #', 64, 1, 120)
|
||||
def test_name_with_non_printable_characters(self):
|
||||
# Names cannot contain printable characters
|
||||
self.assertInvalidInput(u'm1.\u0868 #', 64, 1, 120)
|
||||
|
||||
def test_name_length_checks(self):
|
||||
MAX_LEN = 255
|
||||
|
||||
Reference in New Issue
Block a user