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
This commit is contained in:
Chuck Carmack 2015-12-08 20:39:08 +00:00
parent 54f7925576
commit 4841cab03e
36 changed files with 1198 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
}
]
}
}

View File

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

View File

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

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"
}
]
}
}

View File

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

View File

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

View File

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

View File

@ -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"
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
self.assertEqual(res_dict['server']['description'], 'server_desc')
def test_update_server_empty_description(self):
res_dict = self._update_server_desc(True, '')
self.assertEqual(res_dict['server']['id'], FAKE_UUID)
self.assertEqual(res_dict['server']['description'], '')
def test_update_server_without_description(self):
res_dict = self._update_server_desc(False)
self.assertEqual(res_dict['server']['id'], FAKE_UUID)
self.assertIsNone(res_dict['server']['description'])
def test_update_server_remove_description(self):
res_dict = self._update_server_desc(True)
self.assertEqual(res_dict['server']['id'], FAKE_UUID)
self.assertIsNone(res_dict['server']['description'])
def test_update_server_all_attributes(self):
body = {'server': {
'name': 'server_test',
'description': 'server_desc'
}}
req = self._get_request(body, {'name': 'server_test'})
res_dict = self.controller.update(req, FAKE_UUID, body=body)
self.assertEqual(res_dict['server']['id'], FAKE_UUID)
self.assertEqual(res_dict['server']['name'], 'server_test')
self.assertEqual(res_dict['server']['description'], 'server_desc')
def test_update_server_description_too_long(self):
body = {'server': {'description': 'x' * 256}}
req = self._get_request(body, {'name': 'server_test'})
self.assertRaises(exception.ValidationError, self.controller.update,
req, FAKE_UUID, body=body)
def test_update_server_description_invalid(self):
# Invalid non-printable control char in the desc.
body = {'server': {'description': "123\0d456"}}
req = self._get_request(body, {'name': 'server_test'})
self.assertRaises(exception.ValidationError, self.controller.update,
req, FAKE_UUID, body=body)
class ServerStatusTest(test.TestCase):
def setUp(self):
@ -2150,6 +2333,7 @@ class ServersControllerCreateTest(test.TestCase):
instance = fake_instance.fake_db_instance(**{
'id': self.instance_cache_num,
'display_name': inst['display_name'] or 'test',
'display_description': inst['display_description'] or '',
'uuid': FAKE_UUID,
'instance_type': inst_type,
'image_ref': inst.get('image_ref', def_image_ref),
@ -3112,6 +3296,48 @@ class ServersControllerCreateTest(test.TestCase):
self.req, body=self.body)
class ServersControllerCreateTestV219(ServersControllerCreateTest):
def _create_instance_req(self, set_desc, desc=None):
# proper local hrefs must start with 'http://localhost/v2/'
image_href = 'http://localhost/v2/images/%s' % self.image_uuid
self.body['server']['imageRef'] = image_href
if set_desc:
self.body['server']['description'] = desc
self.req.body = jsonutils.dump_as_bytes(self.body)
self.req.api_version_request = \
api_version_request.APIVersionRequest('2.19')
def test_create_instance_with_description(self):
self._create_instance_req(True, 'server_desc')
# The fact that the action doesn't raise is enough validation
self.controller.create(self.req, body=self.body).obj
def test_create_instance_with_none_description(self):
self._create_instance_req(True)
# The fact that the action doesn't raise is enough validation
self.controller.create(self.req, body=self.body).obj
def test_create_instance_with_empty_description(self):
self._create_instance_req(True, '')
# The fact that the action doesn't raise is enough validation
self.controller.create(self.req, body=self.body).obj
def test_create_instance_without_description(self):
self._create_instance_req(False)
# The fact that the action doesn't raise is enough validation
self.controller.create(self.req, body=self.body).obj
def test_create_instance_description_too_long(self):
self._create_instance_req(True, 'X' * 256)
self.assertRaises(exception.ValidationError, self.controller.create,
self.req, body=self.body)
def test_create_instance_description_invalid(self):
self._create_instance_req(True, "abc\0ddef")
self.assertRaises(exception.ValidationError, self.controller.create,
self.req, body=self.body)
class ServersControllerCreateTestWithMock(test.TestCase):
image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6'
flavor_ref = 'http://localhost/123/flavors/3'
@ -3789,7 +4015,7 @@ class FakeExt(extensions.V21APIExtensionBase):
pass
def fake_schema_extension_point(self, version):
if version == '2.1':
if version == '2.1' or version == '2.19':
return self.fake_schema
elif version == '2.0':
return {}

View File

@ -66,7 +66,7 @@ EXP_VERSIONS = {
"v2.1": {
"id": "v2.1",
"status": "CURRENT",
"version": "2.18",
"version": "2.19",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z",
"links": [
@ -128,7 +128,7 @@ class VersionsTestV20(test.NoDBTestCase):
{
"id": "v2.1",
"status": "CURRENT",
"version": "2.18",
"version": "2.19",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z",
"links": [

View File

@ -431,6 +431,7 @@ def stub_instance(id=1, user_id=None, project_id=None, host=None,
flavor_id="1", name=None, key_name='',
access_ipv4=None, access_ipv6=None, progress=0,
auto_disk_config=False, display_name=None,
display_description=None,
include_fake_metadata=True, config_drive=None,
power_state=None, nw_cache=None, metadata=None,
security_groups=None, root_device_name=None,
@ -522,7 +523,7 @@ def stub_instance(id=1, user_id=None, project_id=None, host=None,
"terminated_at": terminated_at,
"availability_zone": availability_zone,
"display_name": display_name or server_name,
"display_description": "",
"display_description": display_description,
"locked": locked_by is not None,
"locked_by": locked_by,
"metadata": metadata,

View File

@ -0,0 +1,16 @@
---
features:
- In Nova Compute API microversion 2.19, you can
specify a "description" attribute when creating, rebuilding, or updating
a server instance. This description can be retrieved by getting
server details, or list details for servers.
Refer to the Nova Compute API documentation for more
information.
Note that the description attribute existed in prior
Nova versions, but was set to the server name by Nova,
and was not visible to the user. So, servers you
created with microversions prior to 2.19 will return
the description equals the name on server details
microversion 2.19.

View File

@ -48,6 +48,7 @@ nova.tests.unit.api.openstack.compute.test_server_actions.ServerActionsControlle
nova.tests.unit.api.openstack.compute.test_serversV21.Base64ValidationTest
nova.tests.unit.api.openstack.compute.test_serversV21.ServersControllerCreateTest
nova.tests.unit.api.openstack.compute.test_serversV21.ServersControllerRebuildInstanceTest
nova.tests.unit.api.openstack.compute.test_serversV21.ServersControllerRebuildTestV219
nova.tests.unit.api.openstack.compute.test_serversV21.ServersControllerTest
nova.tests.unit.api.openstack.compute.test_serversV21.ServersControllerTestV29
nova.tests.unit.api.openstack.compute.test_simple_tenant_usage.SimpleTenantUsageTestV2