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:
Chris St. Pierre
2014-09-05 15:12:51 -04:00
parent 5821bcf531
commit f40b6a1d5c
7 changed files with 101 additions and 30 deletions

View File

@@ -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,

View File

@@ -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,
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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):

View File

@@ -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):

View File

@@ -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