Merge "Filter leading/trailing spaces for name field in v2.1 compat mode"

changes/83/226783/1
Jenkins 8 years ago committed by Gerrit Code Review
commit 10ac88f2b4

@ -512,6 +512,17 @@ def get_instance(compute_api, context, instance_id, expected_attrs=None):
raise exc.HTTPNotFound(explanation=e.format_message())
def normalize_name(name):
# NOTE(alex_xu): This method is used by v2.1 legacy v2 compat mode.
# In the legacy v2 API, some of APIs strip the spaces and some of APIs not.
# The v2.1 disallow leading/trailing, for compatible v2 API and consistent,
# we enable leading/trailing spaces and strip spaces in legacy v2 compat
# mode. Althrough in legacy v2 API there are some APIs didn't strip spaces,
# but actually leading/trailing spaces(that means user depend on leading/
# trailing spaces distinguish different instance) is pointless usecase.
return name.strip()
def raise_feature_not_supported(msg=None):
if msg is None:
msg = _("The requested functionality is not supported.")

@ -98,7 +98,7 @@ class AccessIPs(extensions.V21APIExtensionBase):
server_update = server_create
server_rebuild = server_create
def get_server_create_schema(self):
def get_server_create_schema(self, version):
return access_ips.server_create
get_server_update_schema = get_server_create_schema

@ -19,6 +19,7 @@ import datetime
from webob import exc
from nova.api.openstack import common
from nova.api.openstack.compute.schemas import aggregates
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
@ -52,7 +53,8 @@ class AggregateController(wsgi.Controller):
# NOTE(gmann): Returns 200 for backwards compatibility but should be 201
# as this operation complete the creation of aggregates resource.
@extensions.expected_errors((400, 409))
@validation.schema(aggregates.create)
@validation.schema(aggregates.create_v20, '2.0', '2.0')
@validation.schema(aggregates.create, '2.1')
def create(self, req, body):
"""Creates an aggregate, given its name and
optional availability zone.
@ -60,8 +62,10 @@ class AggregateController(wsgi.Controller):
context = _get_context(req)
authorize(context, action='create')
host_aggregate = body["aggregate"]
name = host_aggregate["name"]
name = common.normalize_name(host_aggregate["name"])
avail_zone = host_aggregate.get("availability_zone")
if avail_zone:
avail_zone = common.normalize_name(avail_zone)
try:
aggregate = self.api.create_aggregate(context, name, avail_zone)
@ -91,12 +95,15 @@ class AggregateController(wsgi.Controller):
return self._marshall_aggregate(aggregate)
@extensions.expected_errors((400, 404, 409))
@validation.schema(aggregates.update)
@validation.schema(aggregates.update_v20, '2.0', '2.0')
@validation.schema(aggregates.update, '2.1')
def update(self, req, id, body):
"""Updates the name and/or availability_zone of given aggregate."""
context = _get_context(req)
authorize(context, action='update')
updates = body["aggregate"]
if 'name' in updates:
updates['name'] = common.normalize_name(updates['name'])
try:
aggregate = self.api.update_aggregate(context, id, updates)

@ -139,7 +139,13 @@ class AvailabilityZone(extensions.V21APIExtensionBase):
# NOTE(gmann): This function is not supposed to use 'body_deprecated_param'
# parameter as this is placed to handle scheduler_hint extension for V2.1.
def server_create(self, server_dict, create_kwargs, body_deprecated_param):
# NOTE(alex_xu): For v2.1 compat mode, we strip the spaces when create
# availability_zone. But we don't strip at here for backward-compatible
# with some users already created availability_zone with
# leading/trailing spaces with legacy v2 API.
create_kwargs['availability_zone'] = server_dict.get(ATTRIBUTE_NAME)
def get_server_create_schema(self):
def get_server_create_schema(self, version):
if version == "2.0":
return schema.server_create_v20
return schema.server_create

@ -73,5 +73,5 @@ class BlockDeviceMapping(extensions.V21APIExtensionBase):
# Unset the legacy_bdm flag if we got a block device mapping.
create_kwargs['legacy_bdm'] = False
def get_server_create_schema(self):
def get_server_create_schema(self, version):
return schema_block_device_mapping.server_create

@ -64,5 +64,5 @@ class BlockDeviceMappingV1(extensions.V21APIExtensionBase):
# Sets the legacy_bdm flag if we got a legacy block device mapping.
create_kwargs['legacy_bdm'] = True
def get_server_create_schema(self):
def get_server_create_schema(self, version):
return schema_block_device_mapping.server_create

@ -194,6 +194,9 @@ class CellsController(wsgi.Controller):
* Merging existing transport URL with transport information.
"""
if 'name' in cell:
cell['name'] = common.normalize_name(cell['name'])
# Start with the cell type conversion
if 'type' in cell:
cell['is_parent'] = cell['type'] == 'parent'
@ -236,7 +239,8 @@ class CellsController(wsgi.Controller):
# returning a response.
@extensions.expected_errors((400, 403, 501))
@common.check_cells_enabled
@validation.schema(cells.create)
@validation.schema(cells.create_v20, '2.0', '2.0')
@validation.schema(cells.create, '2.1')
def create(self, req, body):
"""Create a child cell entry."""
context = req.environ['nova.context']
@ -253,7 +257,8 @@ class CellsController(wsgi.Controller):
@extensions.expected_errors((400, 403, 404, 501))
@common.check_cells_enabled
@validation.schema(cells.update)
@validation.schema(cells.update_v20, '2.0', '2.0')
@validation.schema(cells.update, '2.1')
def update(self, req, id, body):
"""Update a child cell entry. 'id' is the cell name to update."""
context = req.environ['nova.context']

@ -73,5 +73,5 @@ class ConfigDrive(extensions.V21APIExtensionBase):
def server_create(self, server_dict, create_kwargs, body_deprecated_param):
create_kwargs['config_drive'] = server_dict.get(ATTRIBUTE_NAME)
def get_server_create_schema(self):
def get_server_create_schema(self, version):
return schema_config_drive.server_create

@ -36,7 +36,8 @@ class CreateBackupController(wsgi.Controller):
@extensions.expected_errors((400, 403, 404, 409))
@wsgi.action('createBackup')
@validation.schema(create_backup.create_backup)
@validation.schema(create_backup.create_backup_v20, '2.0', '2.0')
@validation.schema(create_backup.create_backup, '2.1')
def _create_backup(self, req, id, body):
"""Backup a server instance.
@ -52,7 +53,7 @@ class CreateBackupController(wsgi.Controller):
authorize(context)
entity = body["createBackup"]
image_name = entity["name"]
image_name = common.normalize_name(entity["name"])
backup_type = entity["backup_type"]
rotation = int(entity["rotation"])

@ -146,7 +146,7 @@ class DiskConfig(extensions.V21APIExtensionBase):
server_rebuild = server_create
server_resize = server_create
def get_server_create_schema(self):
def get_server_create_schema(self, version):
return disk_config.server_create
get_server_update_schema = get_server_create_schema

@ -54,7 +54,8 @@ class FlavorManageController(wsgi.Controller):
# as this operation complete the creation of flavor resource.
@wsgi.action("create")
@extensions.expected_errors((400, 409, 500))
@validation.schema(flavor_manage.create)
@validation.schema(flavor_manage.create_v20, '2.0', '2.0')
@validation.schema(flavor_manage.create, '2.1')
def _create(self, req, body):
context = req.environ['nova.context']
authorize(context)

@ -18,6 +18,7 @@
import webob
import webob.exc
from nova.api.openstack import common
from nova.api.openstack.compute.schemas import keypairs
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
@ -93,7 +94,8 @@ class KeypairController(wsgi.Controller):
@wsgi.Controller.api_version("2.1", "2.1") # noqa
@extensions.expected_errors((400, 403, 409))
@validation.schema(keypairs.create)
@validation.schema(keypairs.create_v20, "2.0", "2.0")
@validation.schema(keypairs.create, "2.1", "2.1")
def create(self, req, body):
"""Create or import keypair.
@ -111,7 +113,7 @@ class KeypairController(wsgi.Controller):
def _create(self, req, body, user_id=None, **keypair_filters):
context = req.environ['nova.context']
params = body['keypair']
name = params['name']
name = common.normalize_name(params['name'])
key_type = params.get('type', keypair_obj.KEYPAIR_TYPE_SSH)
user_id = user_id or context.user_id
authorize(context, action='create',
@ -304,7 +306,14 @@ class Keypairs(extensions.V21APIExtensionBase):
# NOTE(gmann): This function is not supposed to use 'body_deprecated_param'
# parameter as this is placed to handle scheduler_hint extension for V2.1.
def server_create(self, server_dict, create_kwargs, body_deprecated_param):
# NOTE(alex_xu): The v2.1 API compat mode, we strip the spaces for
# keypair create. But we didn't strip spaces at here for
# backward-compatible some users already created keypair and name with
# leading/trailing spaces by legacy v2 API.
create_kwargs['key_name'] = server_dict.get('key_name')
def get_server_create_schema(self):
return keypairs.server_create
def get_server_create_schema(self, version):
if version == '2.0':
return keypairs.server_create_v20
else:
return keypairs.server_create

@ -60,5 +60,5 @@ class MultipleCreate(extensions.V21APIExtensionBase):
create_kwargs['max_count'] = max_count
create_kwargs['return_reservation_id'] = return_id
def get_server_create_schema(self):
def get_server_create_schema(self, version):
return schema_multiple_create.server_create

@ -59,7 +59,7 @@ class Personality(extensions.V21APIExtensionBase):
create_kwargs['files_to_inject'] = self._get_injected_files(
server_dict['personality'])
def get_server_create_schema(self):
def get_server_create_schema(self, version):
return personality.server_create
get_server_rebuild_schema = get_server_create_schema

@ -40,5 +40,5 @@ class PreserveEphemeralRebuild(extensions.V21APIExtensionBase):
rebuild_kwargs['preserve_ephemeral'] = strutils.bool_from_string(
rebuild_dict['preserve_ephemeral'], strict=True)
def get_server_rebuild_schema(self):
def get_server_rebuild_schema(self, version):
return preserve_ephemeral_rebuild.server_rebuild

@ -44,5 +44,5 @@ class SchedulerHints(extensions.V21APIExtensionBase):
create_kwargs['scheduler_hints'] = scheduler_hints
def get_server_create_schema(self):
def get_server_create_schema(self, version):
return schema.server_create

@ -18,6 +18,10 @@ from nova.api.validation import parameter_types
availability_zone = copy.deepcopy(parameter_types.name)
availability_zone['type'] = ['string', 'null']
availability_zone_with_leading_trailing_spaces = copy.deepcopy(parameter_types.
name_with_leading_trailing_spaces)
availability_zone_with_leading_trailing_spaces['type'] = ['string', 'null']
create = {
'type': 'object',
@ -37,6 +41,14 @@ create = {
'additionalProperties': False,
}
create_v20 = copy.deepcopy(create)
create_v20['properties']['aggregate']['properties']['name'] = (parameter_types.
name_with_leading_trailing_spaces)
create_v20['properties']['aggregate']['properties']['availability_zone'] = (
availability_zone_with_leading_trailing_spaces)
update = {
'type': 'object',
'properties': {
@ -44,7 +56,7 @@ update = {
'aggregate': {
'type': 'object',
'properties': {
'name': parameter_types.name,
'name': parameter_types.name_with_leading_trailing_spaces,
'availability_zone': availability_zone
},
'additionalProperties': False,
@ -58,6 +70,14 @@ update = {
'additionalProperties': False,
}
update_v20 = copy.deepcopy(update)
update_v20['properties']['aggregate']['properties']['name'] = (parameter_types.
name_with_leading_trailing_spaces)
update_v20['properties']['aggregate']['properties']['availability_zone'] = (
availability_zone_with_leading_trailing_spaces)
add_host = {
'type': 'object',
'properties': {

@ -17,3 +17,7 @@ from nova.api.validation import parameter_types
server_create = {
'availability_zone': parameter_types.name,
}
server_create_v20 = {
'availability_zone': parameter_types.name_with_leading_trailing_spaces,
}

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
@ -55,6 +57,11 @@ create = {
}
create_v20 = copy.deepcopy(create)
create_v20['properties']['cell']['properties']['name'] = (parameter_types.
cell_name_leading_trailing_spaces)
update = {
'type': 'object',
'properties': {
@ -85,6 +92,11 @@ update = {
}
update_v20 = copy.deepcopy(create)
update_v20['properties']['cell']['properties']['name'] = (parameter_types.
cell_name_leading_trailing_spaces)
sync_instances = {
'type': 'object',
'properties': {

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
@ -37,3 +39,9 @@ create_backup = {
'required': ['createBackup'],
'additionalProperties': False,
}
create_backup_v20 = copy.deepcopy(create_backup)
create_backup_v20['properties'][
'createBackup']['properties']['name'] = (parameter_types.
name_with_leading_trailing_spaces)

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
create = {
@ -56,3 +58,8 @@ create = {
'required': ['flavor'],
'additionalProperties': False,
}
create_v20 = copy.deepcopy(create)
create_v20['properties']['flavor']['properties']['name'] = (parameter_types.
name_with_leading_trailing_spaces)

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
@ -32,6 +34,12 @@ create = {
'additionalProperties': False,
}
create_v20 = copy.deepcopy(create)
create_v20['properties']['keypair']['properties']['name'] = (parameter_types.
name_with_leading_trailing_spaces)
create_v22 = {
'type': 'object',
'properties': {
@ -78,3 +86,7 @@ create_v210 = {
server_create = {
'key_name': parameter_types.name,
}
server_create_v20 = {
'key_name': parameter_types.name_with_leading_trailing_spaces,
}

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
server_create = {
@ -29,3 +31,8 @@ server_create = {
}
},
}
server_create_v20 = copy.deepcopy(server_create)
server_create_v20['security_groups']['items']['properties']['name'] = (
parameter_types.name_with_leading_trailing_spaces)

@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
from nova.api.validation import parameter_types
@ -50,6 +52,12 @@ base_create = {
'additionalProperties': False,
}
base_create_v20 = copy.deepcopy(base_create)
base_create_v20['properties']['server'][
'properties']['name'] = parameter_types.name_with_leading_trailing_spaces
base_update = {
'type': 'object',
'properties': {
@ -65,6 +73,12 @@ base_update = {
'additionalProperties': False,
}
base_update_v20 = copy.deepcopy(base_update)
base_update_v20['properties']['server'][
'properties']['name'] = parameter_types.name_with_leading_trailing_spaces
base_rebuild = {
'type': 'object',
'properties': {
@ -85,6 +99,12 @@ base_rebuild = {
'additionalProperties': False,
}
base_rebuild_v20 = copy.deepcopy(base_rebuild)
base_rebuild_v20['properties']['rebuild'][
'properties']['name'] = parameter_types.name_with_leading_trailing_spaces
base_resize = {
'type': 'object',
'properties': {
@ -118,6 +138,12 @@ create_image = {
'additionalProperties': False
}
create_image_v20 = copy.deepcopy(create_image)
create_image_v20['properties']['createImage'][
'properties']['name'] = parameter_types.name_with_leading_trailing_spaces
reboot = {
'type': 'object',
'properties': {

@ -527,5 +527,7 @@ class SecurityGroups(extensions.V21APIExtensionBase):
create_kwargs['security_group'] = list(
set(create_kwargs['security_group']))
def get_server_create_schema(self):
def get_server_create_schema(self, version):
if version == '2.0':
return schema_security_groups.server_create_v20
return schema_security_groups.server_create

@ -82,6 +82,10 @@ class ServersController(wsgi.Controller):
schema_server_rebuild = schema_servers.base_rebuild
schema_server_resize = schema_servers.base_resize
schema_server_create_v20 = schema_servers.base_create_v20
schema_server_update_v20 = schema_servers.base_update_v20
schema_server_rebuild_v20 = schema_servers.base_rebuild_v20
@staticmethod
def _add_location(robj):
# Just in case...
@ -201,7 +205,10 @@ class ServersController(wsgi.Controller):
propagate_map_exceptions=True)
if list(self.create_schema_manager):
self.create_schema_manager.map(self._create_extension_schema,
self.schema_server_create)
self.schema_server_create, '2.1')
self.create_schema_manager.map(self._create_extension_schema,
self.schema_server_create_v20,
'2.0')
else:
LOG.debug("Did not find any server create schemas")
@ -215,7 +222,10 @@ class ServersController(wsgi.Controller):
propagate_map_exceptions=True)
if list(self.update_schema_manager):
self.update_schema_manager.map(self._update_extension_schema,
self.schema_server_update)
self.schema_server_update, '2.1')
self.update_schema_manager.map(self._update_extension_schema,
self.schema_server_update_v20,
'2.0')
else:
LOG.debug("Did not find any server update schemas")
@ -229,7 +239,10 @@ class ServersController(wsgi.Controller):
propagate_map_exceptions=True)
if list(self.rebuild_schema_manager):
self.rebuild_schema_manager.map(self._rebuild_extension_schema,
self.schema_server_rebuild)
self.schema_server_rebuild, '2.1')
self.rebuild_schema_manager.map(self._rebuild_extension_schema,
self.schema_server_rebuild_v20,
'2.0')
else:
LOG.debug("Did not find any server rebuild schemas")
@ -243,7 +256,7 @@ class ServersController(wsgi.Controller):
propagate_map_exceptions=True)
if list(self.resize_schema_manager):
self.resize_schema_manager.map(self._resize_extension_schema,
self.schema_server_resize)
self.schema_server_resize, '2.1')
else:
LOG.debug("Did not find any server resize schemas")
@ -506,14 +519,15 @@ class ServersController(wsgi.Controller):
@wsgi.response(202)
@extensions.expected_errors((400, 403, 409, 413))
@validation.schema(schema_server_create)
@validation.schema(schema_server_create_v20, '2.0', '2.0')
@validation.schema(schema_server_create, '2.1')
def create(self, req, body):
"""Creates a new server for a given user."""
context = req.environ['nova.context']
server_dict = body['server']
password = self._get_server_admin_password(server_dict)
name = server_dict['name']
name = common.normalize_name(server_dict['name'])
# Arguments to be passed to instance create function
create_kwargs = {}
@ -704,11 +718,11 @@ class ServersController(wsgi.Controller):
LOG.debug("Running _update_extension_point for %s", ext.obj)
handler.server_update(update_dict, update_kwargs)
def _create_extension_schema(self, ext, create_schema):
def _create_extension_schema(self, ext, create_schema, version):
handler = ext.obj
LOG.debug("Running _create_extension_schema for %s", ext.obj)
schema = handler.get_server_create_schema()
schema = handler.get_server_create_schema(version)
if ext.obj.name == 'SchedulerHints':
# NOTE(oomichi): The request parameter position of scheduler-hint
# extension is different from the other extensions, so here handles
@ -717,25 +731,25 @@ class ServersController(wsgi.Controller):
else:
create_schema['properties']['server']['properties'].update(schema)
def _update_extension_schema(self, ext, update_schema):
def _update_extension_schema(self, ext, update_schema, version):
handler = ext.obj
LOG.debug("Running _update_extension_schema for %s", ext.obj)
schema = handler.get_server_update_schema()
schema = handler.get_server_update_schema(version)
update_schema['properties']['server']['properties'].update(schema)
def _rebuild_extension_schema(self, ext, rebuild_schema):
def _rebuild_extension_schema(self, ext, rebuild_schema, version):
handler = ext.obj
LOG.debug("Running _rebuild_extension_schema for %s", ext.obj)
schema = handler.get_server_rebuild_schema()
schema = handler.get_server_rebuild_schema(version)
rebuild_schema['properties']['rebuild']['properties'].update(schema)
def _resize_extension_schema(self, ext, resize_schema):
def _resize_extension_schema(self, ext, resize_schema, version):
handler = ext.obj
LOG.debug("Running _resize_extension_schema for %s", ext.obj)
schema = handler.get_server_resize_schema()
schema = handler.get_server_resize_schema(version)
resize_schema['properties']['resize']['properties'].update(schema)
def _delete(self, context, req, instance_uuid):
@ -753,7 +767,8 @@ class ServersController(wsgi.Controller):
self.compute_api.delete(context, instance)
@extensions.expected_errors((400, 404))
@validation.schema(schema_server_update)
@validation.schema(schema_server_update_v20, '2.0', '2.0')
@validation.schema(schema_server_update, '2.1')
def update(self, req, id, body):
"""Update server then pass on to version-specific controller."""
@ -762,7 +777,8 @@ class ServersController(wsgi.Controller):
authorize(ctxt, action='update')
if 'name' in body['server']:
update_dict['display_name'] = body['server']['name']
update_dict['display_name'] = common.normalize_name(
body['server']['name'])
if list(self.update_extension_manager):
self.update_extension_manager.map(self._update_extension_point,
@ -957,7 +973,8 @@ class ServersController(wsgi.Controller):
@wsgi.response(202)
@extensions.expected_errors((400, 403, 404, 409, 413))
@wsgi.action('rebuild')
@validation.schema(schema_server_rebuild)
@validation.schema(schema_server_rebuild_v20, '2.0', '2.0')
@validation.schema(schema_server_rebuild, '2.1')
def _action_rebuild(self, req, id, body):
"""Rebuild an instance with the given attributes."""
rebuild_dict = body['rebuild']
@ -984,8 +1001,12 @@ class ServersController(wsgi.Controller):
for request_attribute, instance_attribute in attr_map.items():
try:
rebuild_kwargs[instance_attribute] = rebuild_dict[
request_attribute]
if request_attribute == 'name':
rebuild_kwargs[instance_attribute] = common.normalize_name(
rebuild_dict[request_attribute])
else:
rebuild_kwargs[instance_attribute] = rebuild_dict[
request_attribute]
except (KeyError, TypeError):
pass
@ -1033,14 +1054,15 @@ class ServersController(wsgi.Controller):
@extensions.expected_errors((400, 403, 404, 409))
@wsgi.action('createImage')
@common.check_snapshots_enabled
@validation.schema(schema_servers.create_image)
@validation.schema(schema_servers.create_image, '2.0', '2.0')
@validation.schema(schema_servers.create_image, '2.1')
def _action_create_image(self, req, id, body):
"""Snapshot a server instance."""
context = req.environ['nova.context']
authorize(context, action='create_image')
entity = body["createImage"]
image_name = entity["name"]
image_name = common.normalize_name(entity["name"])
metadata = entity.get('metadata', {})
common.check_img_metadata_properties_quota(context, metadata)

@ -38,5 +38,5 @@ class UserData(extensions.V21APIExtensionBase):
def server_create(self, server_dict, create_kwargs, body_deprecated_param):
create_kwargs['user_data'] = server_dict.get(ATTRIBUTE_NAME)
def get_server_create_schema(self):
def get_server_create_schema(self, version):
return schema_user_data.server_create

@ -58,16 +58,48 @@ def _get_printable(exclude=None):
_printable_ws = ''.join(c for c in _get_all_chars()
if unicodedata.category(c) == "Zs")
valid_name_regex = '^(?![%s])[%s]*(?<![%s])$' % (
def _get_printable_no_ws(exclude=None):
if exclude is None:
exclude = []
return ''.join(c for c in _get_all_chars()
if _is_printable(c) and
unicodedata.category(c) != "Zs" and
c not in exclude)
valid_name_regex_base = '^(?![%s])[%s]*(?<![%s])$'
valid_name_regex = valid_name_regex_base % (
re.escape(_printable_ws), re.escape(_get_printable()),
re.escape(_printable_ws))
# cell's name disallow '!', '.' and '@'.
valid_cell_name_regex = '^(?![%s])[%s]*(?<![%s])$' % (
# This regex allows leading/trailing whitespace
valid_name_leading_trailing_spaces_regex_base = (
"^[%(ws)s]*[%(no_ws)s]+[%(ws)s]*$|"
"^[%(ws)s]*[%(no_ws)s][%(no_ws)s%(ws)s]+[%(no_ws)s][%(ws)s]*$")
valid_cell_name_regex = valid_name_regex_base % (
re.escape(_printable_ws),
re.escape(_get_printable(exclude=['!', '.', '@'])),
re.escape(_printable_ws))
# cell's name disallow '!', '.' and '@'.
valid_cell_name_leading_trailing_spaces_regex = (
valid_name_leading_trailing_spaces_regex_base % {
'ws': re.escape(_printable_ws),
'no_ws': re.escape(_get_printable_no_ws(exclude=['!', '.', '@']))})
valid_name_leading_trailing_spaces_regex = (
valid_name_leading_trailing_spaces_regex_base % {
'ws': re.escape(_printable_ws),
'no_ws': re.escape(_get_printable_no_ws())})
boolean = {
'type': ['boolean', 'string'],
'enum': [True, 'True', 'TRUE', 'true', '1', 'ON', 'On', 'on',
@ -128,6 +160,18 @@ cell_name = {
}
cell_name_leading_trailing_spaces = {
'type': 'string', 'minLength': 1, 'maxLength': 255,
'pattern': valid_cell_name_leading_trailing_spaces_regex,
}
name_with_leading_trailing_spaces = {
'type': 'string', 'minLength': 1, 'maxLength': 255,
'pattern': valid_name_leading_trailing_spaces_regex,
}
tcp_udp_port = {
'type': ['integer', 'string'], 'pattern': '^[0-9]*$',
'minimum': 0, 'maximum': 65535,

@ -36,6 +36,7 @@ from nova.api.openstack.compute.legacy_v2 import ips
from nova.api.openstack.compute.legacy_v2 import servers
from nova.api.openstack.compute import views
from nova.api.openstack import extensions
from nova import availability_zones
from nova.compute import api as compute_api
from nova.compute import flavors
from nova.compute import task_states
@ -1398,6 +1399,14 @@ class ServersControllerUpdateTest(ControllerTest):
self.assertEqual(FAKE_UUID, res_dict['server']['id'])
self.assertEqual('server_test', res_dict['server']['name'])
def test_update_server_name_with_leading_trailing_spaces(self):
body = {'server': {'name': ' abc def '}}
req = self._get_request(body, {'name': 'server_test'})
res_dict = self.controller.update(req, FAKE_UUID, body)
self.assertEqual(FAKE_UUID, res_dict['server']['id'])
self.assertEqual('abc def', res_dict['server']['name'])
def test_update_server_name_too_long(self):
body = {'server': {'name': 'x' * 256}}
req = self._get_request(body, {'name': 'server_test'})
@ -1644,6 +1653,19 @@ class ServersControllerRebuildInstanceTest(ControllerTest):
self.controller._action_rebuild,
self.req, FAKE_UUID, self.body)
def test_rebuild_instance_name_with_leading_trailing_spaces(self):
self.body['rebuild']['name'] = ' abc def '
self.req.body = jsonutils.dumps(self.body)
def fake_rebuild(*args, **kwargs):
# NOTE(alex_xu): V2 API rebuild didn't strip the leading/trailing
# spaces.
self.assertEqual(' abc def ', kwargs['display_name'])
with mock.patch.object(compute_api.API, 'rebuild') as mock_rebuild:
mock_rebuild.side_effect = fake_rebuild
self.controller._action_rebuild(self.req, FAKE_UUID, self.body)
def test_rebuild_instance_name_with_spaces_in_middle(self):
self.body['rebuild']['name'] = 'abc def'
self.req.body = jsonutils.dumps(self.body)
@ -1977,6 +1999,7 @@ class ServersControllerCreateTest(test.TestCase):
server = self.controller.create(self.req, self.body).obj['server']
self._check_admin_pass_len(server)
self.assertEqual(FAKE_UUID, server['id'])
return server
def test_create_instance_private_flavor(self):
values = {
@ -2176,6 +2199,22 @@ class ServersControllerCreateTest(test.TestCase):
self.assertRaises(webob.exc.HTTPConflict,
self._test_create_extra, params)
def test_create_instance_sg_with_leading_trailing_spaces(self):
self.flags(network_api_class='nova.network.neutronv2.api.API')
network = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
requested_networks = [{'uuid': network}]
params = {'networks': requested_networks,
'security_groups': [{'name': ' sg '}]}
def fake_create(*args, **kwargs):
self.assertEqual([' sg '], kwargs['security_group'])
return (objects.InstanceList(objects=[fakes.stub_instance_obj(
self.req.environ['nova.context'])]), None)
self.stubs.Set(compute_api.API, 'create', fake_create)
self.ext_mgr.extensions = {'os-security-groups'}
self._test_create_extra(params)
def test_create_instance_with_port_with_no_fixed_ips(self):
self.flags(network_api_class='nova.network.neutronv2.api.API')
port_id = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
@ -2245,6 +2284,21 @@ class ServersControllerCreateTest(test.TestCase):
self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create,
self.req, self.body)
def test_create_instance_name_with_leading_trailing_spaces(self):
self.body['server']['name'] = ' abc def '
self.req.body = jsonutils.dumps(self.body)
server = self._test_create_instance()
self.assertEqual(
self.body['server']['name'].strip(),
self.instance_cache_by_uuid[server['id']]['display_name'])
def test_create_instance_az_with_leading_trailing_spaces(self):
self.body['server']['availability_zone'] = ' zone1 '
self.req.body = jsonutils.dumps(self.body)
with mock.patch.object(availability_zones, 'get_availability_zones',
return_value=[' zone1 ']):
self._test_create_instance()
def test_create_instance(self):
self.req.body = jsonutils.dumps(self.body)
res = self.controller.create(self.req, self.body).obj

@ -21,6 +21,7 @@ from webob import exc
from nova.api.openstack.compute import aggregates as aggregates_v21
from nova.api.openstack.compute.legacy_v2.contrib import aggregates \
as aggregates_v2
from nova.compute import api as compute_api
from nova import context
from nova import exception
from nova import test
@ -142,6 +143,27 @@ class AggregateTestCaseV21(test.NoDBTestCase):
{"foo": "test",
"availability_zone": "nova1"}})
def test_create_name_with_leading_trailing_spaces(self):
self.assertRaises(self.bad_request, self.controller.create,
self.req, body={"aggregate":
{"name": " test ",
"availability_zone": "nova1"}})
def test_create_name_with_leading_trailing_spaces_compat_mode(self):
def fake_mock_aggs(context, name, az):
self.assertEqual('test', name)
return AGGREGATE
with mock.patch.object(compute_api.AggregateAPI,
'create_aggregate') as mock_aggs:
mock_aggs.side_effect = fake_mock_aggs
self.req.set_legacy_v2()
self.controller.create(self.req,
body={"aggregate":
{"name": " test ",
"availability_zone": "nova1"}})
def test_create_with_no_availability_zone(self):
def stub_create_aggregate(context, name, availability_zone):
self.assertEqual(context, self.context, "context")
@ -173,6 +195,28 @@ class AggregateTestCaseV21(test.NoDBTestCase):
{"name": "test",
"availability_zone": "x" * 256}})
def test_create_availability_zone_with_leading_trailing_spaces(self):
self.assertRaises(self.bad_request, self.controller.create,
self.req, body={"aggregate":
{"name": "test",
"availability_zone": " nova1 "}})
def test_create_availabiltiy_zone_with_leading_trailing_spaces_compat_mode(
self):
def fake_mock_aggs(context, name, az):
self.assertEqual('nova1', az)
return AGGREGATE
with mock.patch.object(compute_api.AggregateAPI,
'create_aggregate') as mock_aggs:
mock_aggs.side_effect = fake_mock_aggs
self.req.set_legacy_v2()
self.controller.create(self.req,
body={"aggregate":
{"name": "test",
"availability_zone": " nova1 "}})
def test_create_with_null_availability_zone(self):
aggregate = {"name": "aggregate1",
"id": "1",
@ -682,3 +726,40 @@ class AggregateTestCaseV2(AggregateTestCaseV21):
self.assertRaises(exception.AdminRequired,
self.controller._remove_host, self.user_req,
'1', {'host': 'fake_host'})
def test_create_name_with_leading_trailing_spaces(self):
def fake_mock_aggs(context, name, az):
# NOTE(alex_xu): legacy v2 api didn't strip the spaces.
self.assertEqual(' test ', name)
return AGGREGATE
with mock.patch.object(compute_api.AggregateAPI,
'create_aggregate') as mock_aggs:
mock_aggs.side_effect = fake_mock_aggs
self.controller.create(self.req,
body={"aggregate":
{"name": " test ",
"availability_zone": "nova1"}})
def test_create_name_with_leading_trailing_spaces_compat_mode(self):
pass
def test_create_availability_zone_with_leading_trailing_spaces(self):
def fake_mock_aggs(context, name, az):
# NOTE(alex_xu): legacy v2 api didn't strip the spaces.
self.assertEqual(' nova1 ', az)
return AGGREGATE
with mock.patch.object(compute_api.AggregateAPI,
'create_aggregate') as mock_aggs:
mock_aggs.side_effect = fake_mock_aggs
self.controller.create(self.req,
body={"aggregate":
{"name": " test ",
"availability_zone": " nova1 "}})
def test_create_availabiltiy_zone_with_leading_trailing_spaces_compat_mode(
self):
pass

@ -313,6 +313,31 @@ class CellsTestV21(BaseCellsTest):
self.assertRaises(self.bad_request, self.controller.create,
req, body=body)
def test_cell_create_name_with_leading_trailing_spaces(self):
body = {'cell': {'name': ' moocow ',
'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 test_cell_create_name_with_leading_trailing_spaces_compat_mode(self):
body = {'cell': {'name': ' moocow ',
'username': 'fred',
'password': 'secret',
'rpc_host': 'r3.example.org',
'type': 'parent'}}
req = self._get_request("cells")
req.environ['nova.context'] = self.context
req.set_legacy_v2()
resp = self.controller.create(req, body=body)
self.assertEqual('moocow', resp['cell']['name'])
def test_cell_create_name_with_invalid_type_raises(self):
body = {'cell': {'name': 'moocow',
'username': 'fred',
@ -733,3 +758,19 @@ class CellsTestV2(CellsTestV21):
req = self._get_request("cells")
req.environ['nova.context'] = self.context
self.controller.update(req, 'cell1', body=body)
def test_cell_create_name_with_leading_trailing_spaces_compat_mode(self):
pass
def test_cell_create_name_with_leading_trailing_spaces(self):
body = {'cell': {'name': ' moocow ',
'username': 'fred',
'password': 'secret',
'rpc_host': 'r3.example.org',
'type': 'parent'}}
req = self._get_request("cells")
req.environ['nova.context'] = self.context
resp = self.controller.create(req, body=body)
# NOTE(alex_xu): legacy v2 didn't strip the spaces.
self.assertEqual(' moocow ', resp['cell']['name'])

@ -85,6 +85,39 @@ class CreateBackupTestsV21(admin_only_action_common.CommonMixin,
self.controller._create_backup,
self.req, fakes.FAKE_UUID, body=body)
def test_create_backup_name_with_leading_trailing_spaces(self):
body = {
'createBackup': {
'name': ' test ',
'backup_type': 'daily',
'rotation': 1,
},
}
self.assertRaises(self.validation_error,
self.controller._create_backup,
self.req, fakes.FAKE_UUID, body=body)
def test_create_backup_name_with_leading_trailing_spaces_compat_mode(
self):
body = {
'createBackup': {
'name': ' test ',
'backup_type': 'daily',
'rotation': 1,
},
}
image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
properties={})
common.check_img_metadata_properties_quota(self.context, {})
instance = self._stub_instance_get()
self.compute_api.backup(self.context, instance, 'test',
'daily', 1,
extra_properties={}).AndReturn(image)
self.mox.ReplayAll()
self.req.set_legacy_v2()
self.controller._create_backup(self.req, instance.uuid,
body=body)
def test_create_backup_no_rotation(self):
# Rotation is required for backup requests.
body = {
@ -317,6 +350,29 @@ class CreateBackupTestsV2(CreateBackupTestsV21):
def test_create_backup_non_dict_metadata(self):
pass
def test_create_backup_name_with_leading_trailing_spaces(self):
body = {
'createBackup': {
'name': ' test ',
'backup_type': 'daily',
'rotation': 1,
},
}
image = dict(id='fake-image-id', status='ACTIVE', name='Backup 1',
properties={})
common.check_img_metadata_properties_quota(self.context, {})
instance = self._stub_instance_get()
self.compute_api.backup(self.context, instance, ' test ',
'daily', 1,
extra_properties={}).AndReturn(image)
self.mox.ReplayAll()
self.controller._create_backup(self.req, instance.uuid,
body=body)
def test_create_backup_name_with_leading_trailing_spaces_compat_mode(
self):
pass
class CreateBackupPolicyEnforcementv21(test.NoDBTestCase):

@ -180,8 +180,8 @@ class FlavorManageTestV21(test.NoDBTestCase):
def test_create_missing_disk(self):
self._test_create_missing_parameter('disk')
def _create_flavor_success_case(self, body):
req = self._get_http_request(url=self.base_url)
def _create_flavor_success_case(self, body, req=None):
req = req if req else self._get_http_request(url=self.base_url)
req.headers['Content-Type'] = 'application/json'
req.method = 'POST'
req.body = jsonutils.dumps(body)
@ -227,6 +227,17 @@ class FlavorManageTestV21(test.NoDBTestCase):
self.request_body['flavor']['name'] = 'a' * 256
self._create_flavor_bad_request_case(self.request_body)
def test_create_with_name_leading_trailing_spaces(self):
self.request_body['flavor']['name'] = ' test '
self._create_flavor_bad_request_case(self.request_body)
def test_create_with_name_leading_trailing_spaces_compat_mode(self):
req = self._get_http_request(url=self.base_url)
req.set_legacy_v2()
self.request_body['flavor']['name'] = ' test '
body = self._create_flavor_success_case(self.request_body, req)
self.assertEqual('test', body['flavor']['name'])
def test_create_without_flavorname(self):
del self.request_body['flavor']['name']
self._create_flavor_bad_request_case(self.request_body)
@ -444,6 +455,15 @@ class FlavorManageTestV2(FlavorManageTestV21):
def _get_http_request(self, url=''):
return fakes.HTTPRequest.blank(url, use_admin_context=False)
def test_create_with_name_leading_trailing_spaces(self):
req = self._get_http_request(url=self.base_url)
self.request_body['flavor']['name'] = ' test '
body = self._create_flavor_success_case(self.request_body, req)
self.assertEqual('test', body['flavor']['name'])
def test_create_with_name_leading_trailing_spaces_compat_mode(self):
pass