diff --git a/doc/api_samples/servers/v2.19/server-action-rebuild-resp.json b/doc/api_samples/servers/v2.19/server-action-rebuild-resp.json new file mode 100644 index 000000000000..bf182c5fa13e --- /dev/null +++ b/doc/api_samples/servers/v2.19/server-action-rebuild-resp.json @@ -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" + } +} diff --git a/doc/api_samples/servers/v2.19/server-action-rebuild.json b/doc/api_samples/servers/v2.19/server-action-rebuild.json new file mode 100644 index 000000000000..76dbb0f6f5e4 --- /dev/null +++ b/doc/api_samples/servers/v2.19/server-action-rebuild.json @@ -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" + } + } +} diff --git a/doc/api_samples/servers/v2.19/server-get-resp.json b/doc/api_samples/servers/v2.19/server-get-resp.json new file mode 100644 index 000000000000..8acab5936688 --- /dev/null +++ b/doc/api_samples/servers/v2.19/server-get-resp.json @@ -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" + } +} diff --git a/doc/api_samples/servers/v2.19/server-post-req.json b/doc/api_samples/servers/v2.19/server-post-req.json new file mode 100644 index 000000000000..24cdb9c2e5c5 --- /dev/null +++ b/doc/api_samples/servers/v2.19/server-post-req.json @@ -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" + } + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.19/server-post-resp.json b/doc/api_samples/servers/v2.19/server-post-resp.json new file mode 100644 index 000000000000..5994b55f6c55 --- /dev/null +++ b/doc/api_samples/servers/v2.19/server-post-resp.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.19/server-put-req.json b/doc/api_samples/servers/v2.19/server-put-req.json new file mode 100644 index 000000000000..50bd66ae5923 --- /dev/null +++ b/doc/api_samples/servers/v2.19/server-put-req.json @@ -0,0 +1,6 @@ +{ + "server" : { + "name" : "updated-server-test", + "description" : "updated-server-description", + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.19/server-put-resp.json b/doc/api_samples/servers/v2.19/server-put-resp.json new file mode 100644 index 000000000000..79c57982fa1e --- /dev/null +++ b/doc/api_samples/servers/v2.19/server-put-resp.json @@ -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" + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.19/servers-details-resp.json b/doc/api_samples/servers/v2.19/servers-details-resp.json new file mode 100644 index 000000000000..3f1a38c9190a --- /dev/null +++ b/doc/api_samples/servers/v2.19/servers-details-resp.json @@ -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" + } + ] +} diff --git a/doc/api_samples/servers/v2.19/servers-list-resp.json b/doc/api_samples/servers/v2.19/servers-list-resp.json new file mode 100644 index 000000000000..ffaccb8a7c94 --- /dev/null +++ b/doc/api_samples/servers/v2.19/servers-list-resp.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 6688800f12ed..0178080e07fb 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.18", + "version": "2.19", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index 655cb9666961..34646aa170bc 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.18", + "version": "2.19", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 97188c12edee..2f83e4763aa3 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -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 diff --git a/nova/api/openstack/compute/schemas/servers.py b/nova/api/openstack/compute/schemas/servers.py index 0779f825f4ee..054ec259dd57 100644 --- a/nova/api/openstack/compute/schemas/servers.py +++ b/nova/api/openstack/compute/schemas/servers.py @@ -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', diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 577ca4c7fb9b..2269c295dc86 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -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', } diff --git a/nova/api/openstack/compute/views/servers.py b/nova/api/openstack/compute/views/servers.py index 7d72cbc7ae56..14947f517345 100644 --- a/nova/api/openstack/compute/views/servers.py +++ b/nova/api/openstack/compute/views/servers.py @@ -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 diff --git a/nova/api/openstack/rest_api_version_history.rst b/nova/api/openstack/rest_api_version_history.rst index c66e90b3f439..8af876e4573a 100644 --- a/nova/api/openstack/rest_api_version_history.rst +++ b/nova/api/openstack/rest_api_version_history.rst @@ -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. diff --git a/nova/api/validation/parameter_types.py b/nova/api/validation/parameter_types.py index 1e55df42a6a5..1add33489e47 100644 --- a/nova/api/validation/parameter_types.py +++ b/nova/api/validation/parameter_types.py @@ -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, diff --git a/nova/compute/api.py b/nova/compute/api.py index 4bc7d9644659..67377df8238a 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -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, diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-action-rebuild-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-action-rebuild-resp.json.tpl new file mode 100644 index 000000000000..e863676e7b20 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-action-rebuild-resp.json.tpl @@ -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" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-action-rebuild.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-action-rebuild.json.tpl new file mode 100644 index 000000000000..8e6a86aa2b73 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-action-rebuild.json.tpl @@ -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" + } + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-get-resp.json.tpl new file mode 100644 index 000000000000..e9830f614288 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-get-resp.json.tpl @@ -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 + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-post-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-post-req.json.tpl new file mode 100644 index 000000000000..b1306fea57db --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-post-req.json.tpl @@ -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" + } + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-post-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-post-resp.json.tpl new file mode 100644 index 000000000000..5358868400cd --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-post-resp.json.tpl @@ -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" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-put-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-put-req.json.tpl new file mode 100644 index 000000000000..cf6ceef7b581 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-put-req.json.tpl @@ -0,0 +1,6 @@ +{ + "server" : { + "name" : "updated-server-test", + "description" : "updated-server-description" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-put-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-put-resp.json.tpl new file mode 100644 index 000000000000..cd3aaed4b466 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/server-put-resp.json.tpl @@ -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 + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/servers-details-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/servers-details-resp.json.tpl new file mode 100644 index 000000000000..0594f7c81e37 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/servers-details-resp.json.tpl @@ -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 + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/servers-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/servers-list-resp.json.tpl new file mode 100644 index 000000000000..f78d963d5d02 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.19/servers-list-resp.json.tpl @@ -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" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/versions/v21-version-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/versions/v21-version-get-resp.json.tpl index fcd4fa32fbaa..09aeb0facdda 100644 --- a/nova/tests/functional/api_sample_tests/api_samples/versions/v21-version-get-resp.json.tpl +++ b/nova/tests/functional/api_sample_tests/api_samples/versions/v21-version-get-resp.json.tpl @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.18", + "version": "2.19", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/tests/functional/api_sample_tests/api_samples/versions/versions-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/versions/versions-get-resp.json.tpl index b1667a76fb80..b6d70f4ce810 100644 --- a/nova/tests/functional/api_sample_tests/api_samples/versions/versions-get-resp.json.tpl +++ b/nova/tests/functional/api_sample_tests/api_samples/versions/versions-get-resp.json.tpl @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.18", + "version": "2.19", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/tests/functional/api_sample_tests/test_servers.py b/nova/tests/functional/api_sample_tests/test_servers.py index 6d8221cf948f..8a550c156406 100644 --- a/nova/tests/functional/api_sample_tests/test_servers.py +++ b/nova/tests/functional/api_sample_tests/test_servers.py @@ -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 diff --git a/nova/tests/functional/test_servers.py b/nova/tests/functional/test_servers.py index 9780c0651cca..4f55da6c9739 100644 --- a/nova/tests/functional/test_servers.py +++ b/nova/tests/functional/test_servers.py @@ -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) diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index dc8a571e920a..6e2412d36480 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -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 {} diff --git a/nova/tests/unit/api/openstack/compute/test_versions.py b/nova/tests/unit/api/openstack/compute/test_versions.py index 92a4c638d9c8..44cd8de5d191 100644 --- a/nova/tests/unit/api/openstack/compute/test_versions.py +++ b/nova/tests/unit/api/openstack/compute/test_versions.py @@ -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": [ diff --git a/nova/tests/unit/api/openstack/fakes.py b/nova/tests/unit/api/openstack/fakes.py index 549edb8f9b30..3b4e33928da7 100644 --- a/nova/tests/unit/api/openstack/fakes.py +++ b/nova/tests/unit/api/openstack/fakes.py @@ -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, diff --git a/releasenotes/notes/user-settable-server-description-89dcfc75677e31bc.yaml b/releasenotes/notes/user-settable-server-description-89dcfc75677e31bc.yaml new file mode 100644 index 000000000000..17c3d8152978 --- /dev/null +++ b/releasenotes/notes/user-settable-server-description-89dcfc75677e31bc.yaml @@ -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. diff --git a/tests-py3.txt b/tests-py3.txt index 3d63f3043ca1..d35b8dd2de51 100644 --- a/tests-py3.txt +++ b/tests-py3.txt @@ -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 @@ -211,4 +212,4 @@ nova.tests.unit.virt.test_virt_drivers.AbstractDriverTestCase nova.tests.unit.virt.vmwareapi.test_configdrive.ConfigDriveTestCase nova.tests.unit.virt.vmwareapi.test_driver_api.VMwareAPIVMTestCase nova.tests.unit.virt.xenapi.test_vmops.BootableTestCase -nova.tests.unit.virt.xenapi.test_vmops.SpawnTestCase \ No newline at end of file +nova.tests.unit.virt.xenapi.test_vmops.SpawnTestCase