REST API changes for user settable server description

This patches adds changes to the Nova REST API to allow
users to create a server with a description, rebuild
a server with a description, update the description,
and get the description in the server details.

Note: Future commits will be done to support the server
description in python-novaclient and openstack-client.

APIImpact

Implements blueprint: user-settable-server-description

Change-Id: I74b1a340c5ab98fdea2186e87dd13f42ce7c7661
changes/50/254950/15
Chuck Carmack 7 years ago
parent 54f7925576
commit 4841cab03e

@ -0,0 +1,57 @@
{
"server": {
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"addr": "192.168.0.3",
"version": 4
}
]
},
"adminPass": "seekr3t",
"created": "2013-11-14T06:29:00Z",
"flavor": {
"id": "1",
"links": [
{
"href": "http://openstack.example.com/openstack/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "28d8d56f0e3a77e20891f455721cbb68032e017045e20aa5dfc6cb66",
"id": "a0a80a94-3d81-4a10-822a-daa0cf9e870b",
"image": {
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [
{
"href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
"rel": "bookmark"
}
]
},
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/a0a80a94-3d81-4a10-822a-daa0cf9e870b",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/a0a80a94-3d81-4a10-822a-daa0cf9e870b",
"rel": "bookmark"
}
],
"locked": false,
"metadata": {
"meta_var": "meta_val"
},
"name": "foobar",
"description" : "description of foobar",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "2013-11-14T06:29:02Z",
"user_id": "fake"
}
}

@ -0,0 +1,13 @@
{
"rebuild" : {
"accessIPv4" : "1.2.3.4",
"accessIPv6" : "80fe::",
"imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b",
"name" : "foobar",
"description" : "description of foobar",
"adminPass" : "seekr3t",
"metadata" : {
"meta_var" : "meta_val"
}
}
}

@ -0,0 +1,59 @@
{
"server": {
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff",
"OS-EXT-IPS:type": "fixed",
"addr": "192.168.0.3",
"version": 4
}
]
},
"created": "2015-12-07T17:24:14Z",
"description": "new-server-description",
"flavor": {
"id": "1",
"links": [
{
"href": "http://openstack.example.com/openstack/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "c656e68b04b483cfc87cdbaa2346557b174ec1cb6be6afbd2a0133a0",
"id": "ddb205dc-717e-496e-8e96-88a3b31b075d",
"image": {
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [
{
"href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
"rel": "bookmark"
}
]
},
"key_name": null,
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/ddb205dc-717e-496e-8e96-88a3b31b075d",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/ddb205dc-717e-496e-8e96-88a3b31b075d",
"rel": "bookmark"
}
],
"locked": false,
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "2015-12-07T17:24:15Z",
"user_id": "fake"
}
}

@ -0,0 +1,13 @@
{
"server" : {
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"name" : "new-server-test",
"description" : "new-server-description",
"imageRef" : "http://glance.openstack.example.com/images/70a599e0-31e7-49b7-b260-868f441e862b",
"flavorRef" : "http://openstack.example.com/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
}
}
}

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "rySfUy7xL4C5",
"id": "19923676-e78b-46fb-af62-a5942aece2ac",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/19923676-e78b-46fb-af62-a5942aece2ac",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/19923676-e78b-46fb-af62-a5942aece2ac",
"rel": "bookmark"
}
]
}
}

@ -0,0 +1,6 @@
{
"server" : {
"name" : "updated-server-test",
"description" : "updated-server-description",
}
}

@ -0,0 +1,56 @@
{
"server": {
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"addr": "192.168.0.3",
"version": 4
}
]
},
"created": "2015-12-07T19:19:36Z",
"description": "updated-server-description",
"flavor": {
"id": "1",
"links": [
{
"href": "http://openstack.example.com/openstack/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "4e17a358ca9bbc8ac6e215837b6410c0baa21b2463fefe3e8f712b31",
"id": "c509708e-f0c6-461f-b2b3-507547959eb2",
"image": {
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [
{
"href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
"rel": "bookmark"
}
]
},
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/c509708e-f0c6-461f-b2b3-507547959eb2",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/c509708e-f0c6-461f-b2b3-507547959eb2",
"rel": "bookmark"
}
],
"locked": false,
"metadata": {
"My Server Name": "Apache1"
},
"name": "updated-server-test",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "2015-12-07T19:19:36Z",
"user_id": "fake"
}
}

@ -0,0 +1,61 @@
{
"servers": [
{
"accessIPv4": "1.2.3.4",
"accessIPv6": "80fe::",
"addresses": {
"private": [
{
"OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff",
"OS-EXT-IPS:type": "fixed",
"addr": "192.168.0.3",
"version": 4
}
]
},
"created": "2015-12-07T19:54:48Z",
"description": "new-server-description",
"flavor": {
"id": "1",
"links": [
{
"href": "http://openstack.example.com/openstack/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "a672ab12738567bfcb852c846d66a6ce5c3555b42d73db80bdc6f1a4",
"id": "91965362-fd86-4543-8ce1-c17074d2984d",
"image": {
"id": "70a599e0-31e7-49b7-b260-868f441e862b",
"links": [
{
"href": "http://openstack.example.com/openstack/images/70a599e0-31e7-49b7-b260-868f441e862b",
"rel": "bookmark"
}
]
},
"key_name": null,
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/91965362-fd86-4543-8ce1-c17074d2984d",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/91965362-fd86-4543-8ce1-c17074d2984d",
"rel": "bookmark"
}
],
"locked": false,
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "2015-12-07T19:54:49Z",
"user_id": "fake"
}
]
}

@ -0,0 +1,18 @@
{
"servers": [
{
"id": "78d95942-8805-4597-b1af-3d0e38330758",
"links": [
{
"href": "http://openstack.example.com/v2/openstack/servers/78d95942-8805-4597-b1af-3d0e38330758",
"rel": "self"
},
{
"href": "http://openstack.example.com/openstack/servers/78d95942-8805-4597-b1af-3d0e38330758",
"rel": "bookmark"
}
],
"name": "new-server-test"
}
]
}

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.18",
"version": "2.19",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.18",
"version": "2.19",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

@ -60,6 +60,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.16 - Exposes host_status for servers/detail and servers/{server_id}
* 2.17 - Add trigger_crash_dump to server actions
* 2.18 - Makes project_id optional in v2.1
* 2.19 - Allow user to set and get the server description
"""
# The minimum and maximum versions of the API supported
@ -68,7 +69,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.18"
_MAX_API_VERSION = "2.19"
DEFAULT_API_VERSION = _MIN_API_VERSION

@ -58,6 +58,11 @@ base_create_v20['properties']['server'][
'properties']['name'] = parameter_types.name_with_leading_trailing_spaces
base_create_v219 = copy.deepcopy(base_create)
base_create_v219['properties']['server'][
'properties']['description'] = parameter_types.description
base_update = {
'type': 'object',
'properties': {
@ -78,6 +83,9 @@ base_update_v20 = copy.deepcopy(base_update)
base_update_v20['properties']['server'][
'properties']['name'] = parameter_types.name_with_leading_trailing_spaces
base_update_v219 = copy.deepcopy(base_update)
base_update_v219['properties']['server'][
'properties']['description'] = parameter_types.description
base_rebuild = {
'type': 'object',
@ -104,6 +112,9 @@ base_rebuild_v20 = copy.deepcopy(base_rebuild)
base_rebuild_v20['properties']['rebuild'][
'properties']['name'] = parameter_types.name_with_leading_trailing_spaces
base_rebuild_v219 = copy.deepcopy(base_rebuild)
base_rebuild_v219['properties']['rebuild'][
'properties']['description'] = parameter_types.description
base_resize = {
'type': 'object',

@ -82,6 +82,10 @@ class ServersController(wsgi.Controller):
schema_server_update_v20 = schema_servers.base_update_v20
schema_server_rebuild_v20 = schema_servers.base_rebuild_v20
schema_server_create_v219 = schema_servers.base_create_v219
schema_server_update_v219 = schema_servers.base_update_v219
schema_server_rebuild_v219 = schema_servers.base_rebuild_v219
@staticmethod
def _add_location(robj):
# Just in case...
@ -206,6 +210,9 @@ class ServersController(wsgi.Controller):
invoke_kwds={"extension_info": self.extension_info},
propagate_map_exceptions=True)
if list(self.create_schema_manager):
self.create_schema_manager.map(self._create_extension_schema,
self.schema_server_create_v219,
'2.19')
self.create_schema_manager.map(self._create_extension_schema,
self.schema_server_create, '2.1')
self.create_schema_manager.map(self._create_extension_schema,
@ -223,6 +230,9 @@ class ServersController(wsgi.Controller):
invoke_kwds={"extension_info": self.extension_info},
propagate_map_exceptions=True)
if list(self.update_schema_manager):
self.update_schema_manager.map(self._update_extension_schema,
self.schema_server_update_v219,
'2.19')
self.update_schema_manager.map(self._update_extension_schema,
self.schema_server_update, '2.1')
self.update_schema_manager.map(self._update_extension_schema,
@ -240,6 +250,9 @@ class ServersController(wsgi.Controller):
invoke_kwds={"extension_info": self.extension_info},
propagate_map_exceptions=True)
if list(self.rebuild_schema_manager):
self.rebuild_schema_manager.map(self._rebuild_extension_schema,
self.schema_server_rebuild_v219,
'2.19')
self.rebuild_schema_manager.map(self._rebuild_extension_schema,
self.schema_server_rebuild, '2.1')
self.rebuild_schema_manager.map(self._rebuild_extension_schema,
@ -514,7 +527,8 @@ class ServersController(wsgi.Controller):
@wsgi.response(202)
@extensions.expected_errors((400, 403, 409, 413))
@validation.schema(schema_server_create_v20, '2.0', '2.0')
@validation.schema(schema_server_create, '2.1')
@validation.schema(schema_server_create, '2.1', '2.18')
@validation.schema(schema_server_create_v219, '2.19')
def create(self, req, body):
"""Creates a new server for a given user."""
@ -523,6 +537,16 @@ class ServersController(wsgi.Controller):
password = self._get_server_admin_password(server_dict)
name = common.normalize_name(server_dict['name'])
if api_version_request.is_supported(req, min_version='2.19'):
if 'description' in server_dict:
# This is allowed to be None
description = server_dict['description']
else:
# No default description
description = None
else:
description = name
# Arguments to be passed to instance create function
create_kwargs = {}
@ -596,7 +620,7 @@ class ServersController(wsgi.Controller):
inst_type,
image_uuid,
display_name=name,
display_description=name,
display_description=description,
availability_zone=availability_zone,
forced_host=host, forced_node=node,
metadata=server_dict.get('metadata', {}),
@ -767,7 +791,8 @@ class ServersController(wsgi.Controller):
@extensions.expected_errors((400, 404))
@validation.schema(schema_server_update_v20, '2.0', '2.0')
@validation.schema(schema_server_update, '2.1')
@validation.schema(schema_server_update, '2.1', '2.18')
@validation.schema(schema_server_update_v219, '2.19')
def update(self, req, id, body):
"""Update server then pass on to version-specific controller."""
@ -779,6 +804,10 @@ class ServersController(wsgi.Controller):
update_dict['display_name'] = common.normalize_name(
body['server']['name'])
if 'description' in body['server']:
# This is allowed to be None (remove description)
update_dict['display_description'] = body['server']['description']
if list(self.update_extension_manager):
self.update_extension_manager.map(self._update_extension_point,
body['server'], update_dict)
@ -972,7 +1001,8 @@ class ServersController(wsgi.Controller):
@extensions.expected_errors((400, 403, 404, 409, 413))
@wsgi.action('rebuild')
@validation.schema(schema_server_rebuild_v20, '2.0', '2.0')
@validation.schema(schema_server_rebuild, '2.1')
@validation.schema(schema_server_rebuild, '2.1', '2.18')
@validation.schema(schema_server_rebuild_v219, '2.19')
def _action_rebuild(self, req, id, body):
"""Rebuild an instance with the given attributes."""
rebuild_dict = body['rebuild']
@ -988,6 +1018,7 @@ class ServersController(wsgi.Controller):
attr_map = {
'name': 'display_name',
'description': 'display_description',
'metadata': 'metadata',
}

@ -309,4 +309,8 @@ class ViewBuilderV21(ViewBuilder):
server["server"]["locked"] = (True if instance["locked_by"]
else False)
if api_version_request.is_supported(request, min_version="2.19"):
server["server"]["description"] = instance.get(
"display_description")
return server

@ -167,3 +167,9 @@ user documentation.
2.18
----
Establishes a set of routes that makes project_id an optional construct in v2.1.
2.19
----
Allow the user to set and get the server description.
The user will be able to set the description when creating, rebuilding,
or updating a server, and get the description as part of the server details.

@ -103,6 +103,13 @@ valid_name_leading_trailing_spaces_regex = (
valid_name_regex_obj = re.compile(valid_name_regex, re.UNICODE)
valid_description_regex_base = '^[%s]*$'
valid_description_regex = valid_description_regex_base % (
re.escape(_get_printable()))
boolean = {
'type': ['boolean', 'string'],
'enum': [True, 'True', 'TRUE', 'true', '1', 'ON', 'On', 'on',
@ -175,6 +182,12 @@ name_with_leading_trailing_spaces = {
}
description = {
'type': ['string', 'null'], 'minLength': 0, 'maxLength': 255,
'pattern': valid_description_regex,
}
tcp_udp_port = {
'type': ['integer', 'string'], 'pattern': '^[0-9]*$',
'minimum': 0, 'maximum': 65535,

@ -879,7 +879,7 @@ class API(base.Base):
'root_gb': instance_type['root_gb'],
'ephemeral_gb': instance_type['ephemeral_gb'],
'display_name': display_name,
'display_description': display_description or '',
'display_description': display_description,
'user_data': user_data,
'key_name': key_name,
'key_data': key_data,

@ -0,0 +1,57 @@
{
"server": {
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"addresses": {
"private": [
{
"addr": "%(ip)s",
"version": 4
}
]
},
"adminPass": "%(password)s",
"created": "%(isotime)s",
"flavor": {
"id": "1",
"links": [
{
"href": "%(compute_endpoint)s/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "%(hostid)s",
"id": "%(uuid)s",
"image": {
"id": "%(uuid)s",
"links": [
{
"href": "%(compute_endpoint)s/images/%(uuid)s",
"rel": "bookmark"
}
]
},
"links": [
{
"href": "%(versioned_compute_endpoint)s/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/servers/%(uuid)s",
"rel": "bookmark"
}
],
"locked": false,
"metadata": {
"meta_var": "meta_val"
},
"name": "%(name)s",
"description": "%(description)s",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "%(isotime)s",
"user_id": "fake"
}
}

@ -0,0 +1,13 @@
{
"rebuild" : {
"accessIPv4" : "%(access_ip_v4)s",
"accessIPv6" : "%(access_ip_v6)s",
"imageRef" : "%(uuid)s",
"name" : "%(name)s",
"description" : "%(description)s",
"adminPass" : "%(pass)s",
"metadata" : {
"meta_var" : "meta_val"
}
}
}

@ -0,0 +1,59 @@
{
"server": {
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"addresses": {
"private": [
{
"addr": "%(ip)s",
"OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff",
"OS-EXT-IPS:type": "fixed",
"version": 4
}
]
},
"created": "%(isotime)s",
"flavor": {
"id": "1",
"links": [
{
"href": "%(compute_endpoint)s/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "%(hostid)s",
"id": "%(id)s",
"image": {
"id": "%(uuid)s",
"links": [
{
"href": "%(compute_endpoint)s/images/%(uuid)s",
"rel": "bookmark"
}
]
},
"key_name": null,
"links": [
{
"href": "%(versioned_compute_endpoint)s/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/servers/%(uuid)s",
"rel": "bookmark"
}
],
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"description": "new-server-description",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "%(isotime)s",
"user_id": "fake",
"locked": false
}
}

@ -0,0 +1,13 @@
{
"server" : {
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"name" : "new-server-test",
"description" : "new-server-description",
"imageRef" : "%(glance_host)s/images/%(image_id)s",
"flavorRef" : "%(host)s/flavors/1",
"metadata" : {
"My Server Name" : "Apache1"
}
}
}

@ -0,0 +1,16 @@
{
"server": {
"adminPass": "%(password)s",
"id": "%(id)s",
"links": [
{
"href": "%(versioned_compute_endpoint)s/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/servers/%(uuid)s",
"rel": "bookmark"
}
]
}
}

@ -0,0 +1,6 @@
{
"server" : {
"name" : "updated-server-test",
"description" : "updated-server-description"
}
}

@ -0,0 +1,56 @@
{
"server": {
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"addresses": {
"private": [
{
"addr": "%(ip)s",
"version": 4
}
]
},
"created": "%(isotime)s",
"flavor": {
"id": "1",
"links": [
{
"href": "%(compute_endpoint)s/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "%(hostid)s",
"id": "%(id)s",
"image": {
"id": "%(uuid)s",
"links": [
{
"href": "%(compute_endpoint)s/images/%(uuid)s",
"rel": "bookmark"
}
]
},
"links": [
{
"href": "%(versioned_compute_endpoint)s/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/servers/%(uuid)s",
"rel": "bookmark"
}
],
"metadata": {
"My Server Name": "Apache1"
},
"name": "updated-server-test",
"description": "updated-server-description",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "%(isotime)s",
"user_id": "fake",
"locked": false
}
}

@ -0,0 +1,61 @@
{
"servers": [
{
"accessIPv4": "%(access_ip_v4)s",
"accessIPv6": "%(access_ip_v6)s",
"addresses": {
"private": [
{
"addr": "%(ip)s",
"OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff",
"OS-EXT-IPS:type": "fixed",
"version": 4
}
]
},
"created": "%(isotime)s",
"flavor": {
"id": "1",
"links": [
{
"href": "%(compute_endpoint)s/flavors/1",
"rel": "bookmark"
}
]
},
"hostId": "%(hostid)s",
"id": "%(id)s",
"image": {
"id": "%(uuid)s",
"links": [
{
"href": "%(compute_endpoint)s/images/%(uuid)s",
"rel": "bookmark"
}
]
},
"key_name": null,
"links": [
{
"href": "%(versioned_compute_endpoint)s/servers/%(uuid)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/servers/%(id)s",
"rel": "bookmark"
}
],
"metadata": {
"My Server Name": "Apache1"
},
"name": "new-server-test",
"description": "new-server-description",
"progress": 0,
"status": "ACTIVE",
"tenant_id": "openstack",
"updated": "%(isotime)s",
"user_id": "fake",
"locked": false
}
]
}

@ -0,0 +1,18 @@
{
"servers": [
{
"id": "%(id)s",
"links": [
{
"href": "%(versioned_compute_endpoint)s/servers/%(id)s",
"rel": "self"
},
{
"href": "%(compute_endpoint)s/servers/%(id)s",
"rel": "bookmark"
}
],
"name": "new-server-test"
}
]
}

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.18",
"version": "2.19",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.18",
"version": "2.19",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

@ -96,7 +96,7 @@ class ServersSampleJsonTest(ServersSampleBase):
self._verify_response('servers-list-resp', subs, response, 200)
def test_servers_details(self):
uuid = self._post_server()
uuid = self.test_servers_post()
response = self._do_get('servers/detail',
api_version=self.microversion)
subs = {}
@ -117,6 +117,27 @@ class ServersSampleJson29Test(ServersSampleJsonTest):
scenarios = [('v2_9', {'api_major_version': 'v2.1'})]
class ServersSampleJson219Test(ServersSampleJsonTest):
microversion = '2.19'
sample_dir = 'servers'
scenarios = [('v2_19', {'api_major_version': 'v2.1'})]
def test_servers_post(self):
return self._post_server(False)
def test_servers_put(self):
uuid = self.test_servers_post()
response = self._do_put('servers/%s' % uuid, 'server-put-req', {})
subs = {
'image_id': fake.get_valid_image_id(),
'hostid': '[a-f0-9]+',
'glance_host': self._get_glance_host(),
'access_ip_v4': '1.2.3.4',
'access_ip_v6': '80fe::'
}
self._verify_response('server-put-resp', subs, response, 200)
class ServerSortKeysJsonTests(ServersSampleBase):
sample_dir = 'servers-sort'
@ -173,9 +194,6 @@ class ServersActionsJsonTest(ServersSampleBase):
uuid = self._post_server()
image = fake.get_valid_image_id()
params = {
'host': self._get_host(),
'compute_endpoint': self._get_compute_endpoint(),
'versioned_compute_endpoint': self._get_vers_compute_endpoint(),
'uuid': image,
'name': 'foobar',
'pass': 'seekr3t',
@ -217,6 +235,31 @@ class ServersActionsJsonTest(ServersSampleBase):
{'name': 'foo-image'})
class ServersActionsJson219Test(ServersSampleBase):
microversion = '2.19'
sample_dir = 'servers'
scenarios = [('v2_19', {'api_major_version': 'v2.1'})]
def test_server_rebuild(self):
uuid = self._post_server()
image = fake.get_valid_image_id()
params = {
'uuid': image,
'name': 'foobar',
'description': 'description of foobar',
'pass': 'seekr3t',
'hostid': '[a-f0-9]+',
'access_ip_v4': '1.2.3.4',
'access_ip_v6': '80fe::',
}
resp = self._do_post('servers/%s/action' % uuid,
'server-action-rebuild', params)
subs = params.copy()
del subs['uuid']
self._verify_response('server-action-rebuild-resp', subs, resp, 202)
class ServersActionsAllJsonTest(ServersActionsJsonTest):
all_extensions = True
sample_dir = None

@ -510,3 +510,231 @@ class ServersTest(ServersTestBase):
class ServersTestV21(ServersTest):
api_major_version = 'v2.1'
class ServersTestV219(ServersTestBase):
api_major_version = 'v2.1'
def _create_server(self, set_desc = True, desc = None):
server = self._build_minimal_create_server_request()
if set_desc:
server['description'] = desc
post = {'server': server}
response = self.api.api_post('/servers', post,
headers=self._headers).body
return (server, response['server'])
def _update_server(self, server_id, set_desc = True, desc = None):
new_name = integrated_helpers.generate_random_alphanumeric(8)
server = {'server': {'name': new_name}}
if set_desc:
server['server']['description'] = desc
self.api.api_put('/servers/%s' % server_id, server,
headers=self._headers)
def _rebuild_server(self, server_id, set_desc = True, desc = None):
new_name = integrated_helpers.generate_random_alphanumeric(8)
post = {}
post['rebuild'] = {
"name": new_name,
self._image_ref_parameter: "76fa36fc-c930-4bf3-8c8a-ea2a2420deb6",
self._access_ipv4_parameter: "172.19.0.2",
self._access_ipv6_parameter: "fe80::2",
"metadata": {'some': 'thing'},
}
post['rebuild'].update(self._get_access_ips_params())
if set_desc:
post['rebuild']['description'] = desc
self.api.api_post('/servers/%s/action' % server_id, post,
headers=self._headers)
def _create_server_and_verify(self, set_desc = True, expected_desc = None):
# Creates a server with a description and verifies it is
# in the GET responses.
created_server_id = self._create_server(set_desc,
expected_desc)[1]['id']
self._verify_server_description(created_server_id, expected_desc)
self._delete_server(created_server_id)
def _update_server_and_verify(self, server_id, set_desc = True,
expected_desc = None):
# Updates a server with a description and verifies it is
# in the GET responses.
self._update_server(server_id, set_desc, expected_desc)
self._verify_server_description(server_id, expected_desc)
def _rebuild_server_and_verify(self, server_id, set_desc = True,
expected_desc = None):
# Rebuilds a server with a description and verifies it is
# in the GET responses.
self._rebuild_server(server_id, set_desc, expected_desc)
self._verify_server_description(server_id, expected_desc)
def _verify_server_description(self, server_id, expected_desc = None,
desc_in_resp = True):
# Calls GET on the servers and verifies that the description
# is set as expected in the response, or not set at all.
response = self.api.api_get('/servers/%s' % server_id,
headers=self._headers)
found_server = response.body['server']
self.assertEqual(server_id, found_server['id'])
if desc_in_resp:
# Verify the description is set as expected (can be None)
self.assertEqual(expected_desc, found_server.get('description'))
else:
# Verify the description is not included in the response.
self.assertNotIn('description', found_server)
servers = self.api.api_get('/servers/detail',
headers=self._headers).body['servers']
server_map = {server['id']: server for server in servers}
found_server = server_map.get(server_id)
self.assertTrue(found_server)
if desc_in_resp:
# Verify the description is set as expected (can be None)
self.assertEqual(expected_desc, found_server.get('description'))
else:
# Verify the description is not included in the response.
self.assertNotIn('description', found_server)
def _create_assertRaisesRegex(self, desc):
# Verifies that a 400 error is thrown on create server
with self.assertRaisesRegex(client.OpenStackApiException,
".*Unexpected status code.*") as cm:
self._create_server(True, desc)
self.assertEqual(400, cm.exception.response.status_code)
def _update_assertRaisesRegex(self, server_id, desc):
# Verifies that a 400 error is thrown on update server
with self.assertRaisesRegex(client.OpenStackApiException,
".*Unexpected status code.*") as cm:
self._update_server(server_id, True, desc)
self.assertEqual(400, cm.exception.response.status_code)
def _rebuild_assertRaisesRegex(self, server_id, desc):
# Verifies that a 400 error is thrown on rebuild server
with self.assertRaisesRegex(client.OpenStackApiException,
".*Unexpected status code.*") as cm:
self._rebuild_server(server_id, True, desc)
self.assertEqual(400, cm.exception.response.status_code)
def test_create_server_with_description(self):
fake_network.set_stub_network_methods(self)
self._headers = {}
self._headers['X-OpenStack-Nova-API-Version'] = '2.19'
# Create and get a server with a description
self._create_server_and_verify(True, 'test description')
# Create and get a server with an empty description
self._create_server_and_verify(True, '')
# Create and get a server with description set to None
self._create_server_and_verify()
# Create and get a server without setting the description
self._create_server_and_verify(False)
def test_update_server_with_description(self):
fake_network.set_stub_network_methods(self)
self._headers = {}
self._headers['X-OpenStack-Nova-API-Version'] = '2.19'
# Create a server with an initial description
server_id = self._create_server(True, 'test desc 1')[1]['id']
# Update and get the server with a description
self._update_server_and_verify(server_id, True, 'updated desc')
# Update and get the server name without changing the description
self._update_server_and_verify(server_id, False, 'updated desc')
# Update and get the server with an empty description
self._update_server_and_verify(server_id, True, '')
# Update and get the server by removing the description (set to None)
self._update_server_and_verify(server_id)
# Update and get the server with a 2nd new description
self._update_server_and_verify(server_id, True, 'updated desc2')
# Cleanup
self._delete_server(server_id)
def test_rebuild_server_with_description(self):
fake_network.set_stub_network_methods(self)
self._headers = {}
self._headers['X-OpenStack-Nova-API-Version'] = '2.19'
# Create a server with an initial description
server = self._create_server(True, 'test desc 1')[1]
server_id = server['id']
self._wait_for_state_change(server, 'BUILD')
# Rebuild and get the server with a description
self._rebuild_server_and_verify(server_id, True, 'updated desc')
# Rebuild and get the server name without changing the description
self._rebuild_server_and_verify(server_id, False, 'updated desc')
# Rebuild and get the server with an empty description
self._rebuild_server_and_verify(server_id, True, '')
# Rebuild and get the server by removing the description (set to None)
self._rebuild_server_and_verify(server_id)
# Rebuild and get the server with a 2nd new description
self._rebuild_server_and_verify(server_id, True, 'updated desc2')
# Cleanup
self._delete_server(server_id)
def test_version_compatibility(self):
fake_network.set_stub_network_methods(self)
# Create a server with microversion v2.19 and a description.
self._headers = {}
self._headers['X-OpenStack-Nova-API-Version'] = '2.19'
server_id = self._create_server(True, 'test desc 1')[1]['id']
# Verify that the description is not included on V2.18 GETs
self._headers['X-OpenStack-Nova-API-Version'] = '2.18'
self._verify_server_description(server_id, desc_in_resp = False)
# Verify that updating the server with description on V2.18
# results in a 400 error
self._update_assertRaisesRegex(server_id, 'test update 2.18')
# Verify that rebuilding the server with description on V2.18
# results in a 400 error
self._rebuild_assertRaisesRegex(server_id, 'test rebuild 2.18')
# Cleanup
self._delete_server(server_id)
# Create a server on V2.18 and verify that the description
# defaults to the name on a V2.19 GET
self._headers['X-OpenStack-Nova-API-Version'] = '2.18'
server_req, response = self._create_server(False)
server_id = response['id']
self._headers['X-OpenStack-Nova-API-Version'] = '2.19'
self._verify_server_description(server_id, server_req['name'])
# Cleanup
self._delete_server(server_id)
# Verify that creating a server with description on V2.18
# results in a 400 error
self._headers['X-OpenStack-Nova-API-Version'] = '2.18'
self._create_assertRaisesRegex('test create 2.18')
def test_description_errors(self):
fake_network.set_stub_network_methods(self)
self._headers = {}
self._headers['X-OpenStack-Nova-API-Version'] = '2.19'
# Create servers with invalid descriptions. These throw 400.
# Invalid unicode with non-printable control char
self._create_assertRaisesRegex(u'invalid\0dstring')
# Description is longer than 255 chars
self._create_assertRaisesRegex('x' * 256)
# Update and rebuild servers with invalid descriptions.
# These throw 400.
server_id = self._create_server(True, "desc")[1]['id']
# Invalid unicode with non-printable control char
self._update_assertRaisesRegex(server_id, u'invalid\u0604string')
self._rebuild_assertRaisesRegex(server_id, u'invalid\u0604string')
# Description is longer than 255 chars
self._update_assertRaisesRegex(server_id, 'x' * 256)
self._rebuild_assertRaisesRegex(server_id, 'x' * 256)

@ -132,6 +132,18 @@ def fake_instance_get_all_with_locked(context, list_locked, **kwargs):
return objects.InstanceList(objects=obj_list)
def fake_instance_get_all_with_description(context, list_desc, **kwargs):
obj_list = []
s_id = 0
for desc in list_desc:
uuid = fakes.get_fake_uuid(desc)
s_id = s_id + 1
kwargs['display_description'] = desc
server = fakes.stub_instance_obj(context, id=s_id, uuid=uuid, **kwargs)
obj_list.append(server)
return objects.InstanceList(objects=obj_list)
class MockSetAdminPassword(object):
def __init__(self):
self.instance_id = None
@ -1409,6 +1421,66 @@ class ServersControllerTestV29(ServersControllerTest):
self.assertNotIn(key, search_opts)
class ServersControllerTestV219(ServersControllerTest):
wsgi_api_version = '2.19'
def _get_server_data_dict(self, uuid, image_bookmark, flavor_bookmark,
status="ACTIVE", progress=100, description=None):
server_dict = super(ServersControllerTestV219,
self)._get_server_data_dict(uuid,
image_bookmark,
flavor_bookmark,
status,
progress)
server_dict['server']['locked'] = False
server_dict['server']['description'] = description
return server_dict
@mock.patch.object(compute_api.API, 'get')
def _test_get_server_with_description(self, description, get_mock):
image_bookmark = "http://localhost/fake/images/10"
flavor_bookmark = "http://localhost/fake/flavors/2"
uuid = FAKE_UUID
get_mock.side_effect = fakes.fake_compute_get(id=2,
display_description=description,
uuid=uuid)
req = self.req('/fake/servers/%s' % uuid)
res_dict = self.controller.show(req, uuid)
expected_server = self._get_server_data_dict(uuid,
image_bookmark,
flavor_bookmark,
status="BUILD",
progress=0,
description=description)
self.assertThat(res_dict, matchers.DictMatches(expected_server))
return res_dict
@mock.patch.object(compute_api.API, 'get_all')
def _test_list_server_detail_with_descriptions(self,
s1_desc,
s2_desc,
get_all_mock):
get_all_mock.return_value = fake_instance_get_all_with_description(
context, [s1_desc, s2_desc])
req = self.req('/fake/servers/detail')
servers_list = self.controller.detail(req)
# Check that each returned server has the same 'description' value
# and 'id' as they were created.
for desc in [s1_desc, s2_desc]:
server = next(server for server in servers_list['servers']
if (server['id'] == fakes.get_fake_uuid(desc)))
expected = desc
self.assertEqual(expected, server['description'])
def test_get_server_with_description(self):
self._test_get_server_with_description('test desc')
def test_list_server_detail_with_descriptions(self):
self._test_list_server_detail_with_descriptions('desc1', 'desc2')
class ServersControllerDeleteTest(ControllerTest):
def setUp(self):
@ -1806,6 +1878,55 @@ class ServersControllerRebuildInstanceTest(ControllerTest):
self.controller._stop_server, req, 'test_inst', body)
class ServersControllerRebuildTestV219(ServersControllerRebuildInstanceTest):
def setUp(self):
super(ServersControllerRebuildTestV219, self).setUp()
self.req.api_version_request = \
api_version_request.APIVersionRequest('2.19')
def _rebuild_server(self, set_desc, desc):
fake_get = fakes.fake_compute_get(vm_state=vm_states.ACTIVE,
display_description=desc)
self.stubs.Set(compute_api.API, 'get',
lambda api, *a, **k: fake_get(*a, **k))
if set_desc:
self.body['rebuild']['description'] = desc
self.req.body = jsonutils.dump_as_bytes(self.body)
server = self.controller._action_rebuild(self.req, FAKE_UUID,
body=self.body).obj['server']
self.assertEqual(server['id'], FAKE_UUID)
self.assertEqual(server['description'], desc)
def test_rebuild_server_with_description(self):
self._rebuild_server(True, 'server desc')
def test_rebuild_server_empty_description(self):
self._rebuild_server(True, '')
def test_rebuild_server_without_description(self):
self._rebuild_server(False, '')
def test_rebuild_server_remove_description(self):
self._rebuild_server(True, None)
def test_rebuild_server_description_too_long(self):
self.body['rebuild']['description'] = 'x' * 256
self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError,
self.controller._action_rebuild,
self.req, FAKE_UUID, body=self.body)
def test_rebuild_server_description_invalid(self):
# Invalid non-printable control char in the desc.
self.body['rebuild']['description'] = "123\0d456"
self.req.body = jsonutils.dump_as_bytes(self.body)
self.assertRaises(exception.ValidationError,
self.controller._action_rebuild,
self.req, FAKE_UUID, body=self.body)
class ServersControllerUpdateTest(ControllerTest):
def _get_request(self, body=None, options=None):
@ -2022,6 +2143,68 @@ class ServersControllerTriggerCrashDumpTest(ControllerTest):
self.req, FAKE_UUID, body=self.body)
class ServersControllerUpdateTestV219(ServersControllerUpdateTest):
def _get_request(self, body=None, options=None):
req = super(ServersControllerUpdateTestV219, self)._get_request(
body=body,
options=options)
req.api_version_request = api_version_request.APIVersionRequest('2.19')
return req
def _update_server_desc(self, set_desc, desc=None):
body = {'server': {}}
if set_desc:
body['server']['description'] = desc
req = self._get_request()
res_dict = self.controller.update(req, FAKE_UUID, body=body)
return res_dict
def test_update_server_description(self):
res_dict = self._update_server_desc(True, 'server_desc')
self.assertEqual(res_dict['server']['id'], FAKE_UUID)