Skip to content

Commit

Permalink
Expand valid resource name character set
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Chris St. Pierre committed Dec 18, 2014
1 parent 5821bcf commit f40b6a1
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 30 deletions.
6 changes: 3 additions & 3 deletions nova/api/openstack/compute/schemas/v3/servers.py
Expand Up @@ -21,7 +21,7 @@
'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,
Expand Down Expand Up @@ -67,7 +67,7 @@
'server': {
'type': 'object',
'properties': {
'name': parameter_types.name,
'name': parameter_types.hostname,
},
# TODO(oomichi): ditto, enable here after all extension schema
# patches are merged.
Expand All @@ -84,7 +84,7 @@
'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,
Expand Down
46 changes: 36 additions & 10 deletions nova/api/validation/parameter_types.py
Expand Up @@ -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 = {
Expand Down Expand Up @@ -64,15 +98,7 @@
# 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,
}


Expand Down
8 changes: 5 additions & 3 deletions nova/compute/flavors.py
Expand Up @@ -25,6 +25,7 @@
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
Expand All @@ -50,7 +51,8 @@
# 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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions nova/tests/unit/api/openstack/compute/contrib/test_cells.py
Expand Up @@ -268,8 +268,8 @@ def test_cell_create_name_empty_string_raises(self):
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',
Expand Down Expand Up @@ -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)

Expand Down
Expand Up @@ -218,7 +218,7 @@ def _create_flavor_bad_request_case(self, body):
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):
Expand Down
44 changes: 37 additions & 7 deletions nova/tests/unit/test_api_validation.py
Expand Up @@ -562,34 +562,64 @@ def test_validate_name(self):
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):

Expand Down
9 changes: 5 additions & 4 deletions nova/tests/unit/test_flavors.py
Expand Up @@ -381,14 +381,15 @@ def test_create_with_valid_name(self):
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
Expand Down

0 comments on commit f40b6a1

Please sign in to comment.