From b50b5a660efd5401490e9dbda853d7363d0568e3 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Thu, 1 Jun 2017 11:06:08 +0800 Subject: [PATCH] Support tag instances when boot(4/4) This is the 4th patch of the series, this patch adds a new microversion in API to support adding tags when booting instances. Implemetes: blueprint support-tag-instance-when-boot Change-Id: Ifcaaf285c8f98a1d0e8bbbc87b2f57fbce057346 --- api-ref/source/parameters.yaml | 22 +++++ api-ref/source/servers.inc | 1 + .../servers/v2.52/server-create-req.json | 31 ++++++ .../servers/v2.52/server-create-resp.json | 22 +++++ .../servers/v2.52/server-get-resp.json | 94 ++++++++++++++++++ .../servers/v2.52/servers-details-resp.json | 96 +++++++++++++++++++ .../servers/v2.52/servers-list-resp.json | 18 ++++ .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- nova/api/openstack/api_version_request.py | 3 +- .../compute/rest_api_version_history.rst | 17 ++++ nova/api/openstack/compute/schemas/servers.py | 12 ++- nova/api/openstack/compute/servers.py | 8 +- nova/compute/api.py | 56 ++++++++--- nova/conductor/api.py | 5 +- nova/objects/build_request.py | 34 ++++++- .../servers/v2.52/server-create-req.json.tpl | 31 ++++++ .../servers/v2.52/server-create-resp.json.tpl | 22 +++++ .../servers/v2.52/server-get-resp.json.tpl | 88 +++++++++++++++++ .../v2.52/servers-details-resp.json.tpl | 90 +++++++++++++++++ .../servers/v2.52/servers-list-resp.json.tpl | 18 ++++ .../api_sample_tests/test_servers.py | 6 ++ nova/tests/functional/wsgi/test_servers.py | 54 +++++++++++ .../api/openstack/compute/test_serversV21.py | 50 ++++++++++ nova/tests/unit/compute/test_compute_api.py | 19 ++-- ...upport-tag-when-boot-4dd124371e3ef446.yaml | 4 + 26 files changed, 774 insertions(+), 31 deletions(-) create mode 100644 doc/api_samples/servers/v2.52/server-create-req.json create mode 100644 doc/api_samples/servers/v2.52/server-create-resp.json create mode 100644 doc/api_samples/servers/v2.52/server-get-resp.json create mode 100644 doc/api_samples/servers/v2.52/servers-details-resp.json create mode 100644 doc/api_samples/servers/v2.52/servers-list-resp.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-create-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-create-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-get-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/servers-details-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/servers-list-resp.json.tpl create mode 100644 releasenotes/notes/support-tag-when-boot-4dd124371e3ef446.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 11c26e91cb1e..4d3ec59ddb44 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -4963,6 +4963,28 @@ server_status: in: body required: true type: string +server_tags_create: + description: | + A list of tags. Tags have the following restrictions: + + - Tag is a Unicode bytestring no longer than 60 characters. + + - Tag is a non-empty string. + + - Tags are case sensitive. + + - '/' is not allowed to be in a tag name + + - Comma is not allowed to be in a tag name in order to simplify + requests that specify lists of tags + + - All other characters are allowed to be in a tag name + + - Each server can have up to 50 tags. + in: body + required: false + type: array + min_version: 2.52 server_usages: description: | A list of the server usage objects. diff --git a/api-ref/source/servers.inc b/api-ref/source/servers.inc index 2279dfed7a49..3247d6f8ce21 100644 --- a/api-ref/source/servers.inc +++ b/api-ref/source/servers.inc @@ -333,6 +333,7 @@ Request - os:scheduler_hints: os:scheduler_hints - OS-DCF:diskConfig: OS-DCF:diskConfig - description: server_description + - tags: server_tags_create **Example Create Server** diff --git a/doc/api_samples/servers/v2.52/server-create-req.json b/doc/api_samples/servers/v2.52/server-create-req.json new file mode 100644 index 000000000000..36d2b4cf5ccc --- /dev/null +++ b/doc/api_samples/servers/v2.52/server-create-req.json @@ -0,0 +1,31 @@ +{ + "server" : { + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "name" : "new-server-test", + "imageRef" : "70a599e0-31e7-49b7-b260-868f441e862b", + "flavorRef" : "http://openstack.example.com/flavors/1", + "availability_zone": "nova", + "OS-DCF:diskConfig": "AUTO", + "metadata" : { + "My Server Name" : "Apache1" + }, + "personality": [ + { + "path": "/etc/banner.txt", + "contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ], + "security_groups": [ + { + "name": "default" + } + ], + "user_data" : "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==", + "networks": "auto", + "tags": ["tag1", "tag2"] + }, + "OS-SCH-HNT:scheduler_hints": { + "same_host": "48e6a9f6-30af-47e0-bc04-acaed113bb4e" + } +} diff --git a/doc/api_samples/servers/v2.52/server-create-resp.json b/doc/api_samples/servers/v2.52/server-create-resp.json new file mode 100644 index 000000000000..dd0bb9f2284e --- /dev/null +++ b/doc/api_samples/servers/v2.52/server-create-resp.json @@ -0,0 +1,22 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "adminPass": "S5wqy9sPYUvU", + "id": "97108291-2fd7-4dc2-a909-eaae0306a6a9", + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/97108291-2fd7-4dc2-a909-eaae0306a6a9", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/97108291-2fd7-4dc2-a909-eaae0306a6a9", + "rel": "bookmark" + } + ], + "security_groups": [ + { + "name": "default" + } + ] + } +} \ No newline at end of file diff --git a/doc/api_samples/servers/v2.52/server-get-resp.json b/doc/api_samples/servers/v2.52/server-get-resp.json new file mode 100644 index 000000000000..ff651f8547ae --- /dev/null +++ b/doc/api_samples/servers/v2.52/server-get-resp.json @@ -0,0 +1,94 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-AZ:availability_zone": "nova", + "OS-EXT-SRV-ATTR:host": "compute", + "OS-EXT-SRV-ATTR:hostname": "new-server-test", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "fake-mini", + "OS-EXT-SRV-ATTR:instance_name": "instance-00000001", + "OS-EXT-SRV-ATTR:kernel_id": "", + "OS-EXT-SRV-ATTR:launch_index": 0, + "OS-EXT-SRV-ATTR:ramdisk_id": "", + "OS-EXT-SRV-ATTR:reservation_id": "r-ov3q80zj", + "OS-EXT-SRV-ATTR:root_device_name": "/dev/sda", + "OS-EXT-SRV-ATTR:user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2017-02-14T19:23:59.895661", + "OS-SRV-USG:terminated_at": null, + "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 + } + ] + }, + "config_drive": "", + "created": "2017-02-14T19:23:58Z", + "description": null, + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": {}, + "original_name": "m1.tiny", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6", + "host_status": "UP", + "id": "9168b536-cd40-4630-b43f-b259807c6e87", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "key_name": null, + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/9168b536-cd40-4630-b43f-b259807c6e87", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/9168b536-cd40-4630-b43f-b259807c6e87", + "rel": "bookmark" + } + ], + "locked": false, + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "os-extended-volumes:volumes_attached": [ + { + "delete_on_termination": false, + "id": "volume_id1" + }, + { + "delete_on_termination": false, + "id": "volume_id2" + } + ], + "progress": 0, + "security_groups": [ + { + "name": "default" + } + ], + "status": "ACTIVE", + "tags": ["tag1", "tag2"], + "tenant_id": "6f70656e737461636b20342065766572", + "updated": "2017-02-14T19:24:00Z", + "user_id": "fake" + } +} diff --git a/doc/api_samples/servers/v2.52/servers-details-resp.json b/doc/api_samples/servers/v2.52/servers-details-resp.json new file mode 100644 index 000000000000..8beef359845c --- /dev/null +++ b/doc/api_samples/servers/v2.52/servers-details-resp.json @@ -0,0 +1,96 @@ +{ + "servers": [ + { + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-AZ:availability_zone": "nova", + "OS-EXT-SRV-ATTR:host": "compute", + "OS-EXT-SRV-ATTR:hostname": "new-server-test", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "fake-mini", + "OS-EXT-SRV-ATTR:instance_name": "instance-00000001", + "OS-EXT-SRV-ATTR:kernel_id": "", + "OS-EXT-SRV-ATTR:launch_index": 0, + "OS-EXT-SRV-ATTR:ramdisk_id": "", + "OS-EXT-SRV-ATTR:reservation_id": "r-iffothgx", + "OS-EXT-SRV-ATTR:root_device_name": "/dev/sda", + "OS-EXT-SRV-ATTR:user_data": "IyEvYmluL2Jhc2gKL2Jpbi9zdQplY2hvICJJIGFtIGluIHlvdSEiCg==", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "OS-SRV-USG:launched_at": "2017-02-14T19:24:43.891568", + "OS-SRV-USG:terminated_at": null, + "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 + } + ] + }, + "config_drive": "", + "created": "2017-02-14T19:24:42Z", + "description": null, + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": {}, + "original_name": "m1.tiny", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "hostId": "2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6", + "host_status": "UP", + "id": "764e369e-a874-4401-b7ce-43e4760888da", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "key_name": null, + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/764e369e-a874-4401-b7ce-43e4760888da", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/764e369e-a874-4401-b7ce-43e4760888da", + "rel": "bookmark" + } + ], + "locked": false, + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "os-extended-volumes:volumes_attached": [ + { + "delete_on_termination": false, + "id": "volume_id1" + }, + { + "delete_on_termination": false, + "id": "volume_id2" + } + ], + "progress": 0, + "security_groups": [ + { + "name": "default" + } + ], + "status": "ACTIVE", + "tags": ["tag1", "tag2"], + "tenant_id": "6f70656e737461636b20342065766572", + "updated": "2017-02-14T19:24:43Z", + "user_id": "fake" + } + ] +} diff --git a/doc/api_samples/servers/v2.52/servers-list-resp.json b/doc/api_samples/servers/v2.52/servers-list-resp.json new file mode 100644 index 000000000000..c197a33167f8 --- /dev/null +++ b/doc/api_samples/servers/v2.52/servers-list-resp.json @@ -0,0 +1,18 @@ +{ + "servers": [ + { + "id": "6e3a87e6-a133-452e-86e1-a31291c1b1c8", + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/6e3a87e6-a133-452e-86e1-a31291c1b1c8", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/6e3a87e6-a133-452e-86e1-a31291c1b1c8", + "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 2d9451e7e476..4e8809b178e8 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.51", + "version": "2.52", "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 44b0ef5e4214..e3cc545b5d7b 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.51", + "version": "2.52", "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 e14dcac3fb27..9a1702ee8421 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -123,6 +123,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 2.51 - Adds new event name to external-events (volume-extended). Also, non-admins can see instance action event details except for the traceback field. + * 2.52 - Adds support for applying tags when creating a server. """ # The minimum and maximum versions of the API supported @@ -131,7 +132,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.51" +_MAX_API_VERSION = "2.52" DEFAULT_API_VERSION = _MIN_API_VERSION # Almost all proxy APIs which related to network, images and baremetal diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index dece5f169903..aa8b7be0a41b 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -227,6 +227,17 @@ user documentation. A user can create, update, delete or check existence of simple string tags for servers by the os-server-tags plugin. + Tags have the following schema restrictions: + + * Tag is a Unicode bytestring no longer than 60 characters. + * Tag is a non-empty string. + * Tags are case sensitive. + * '/' is not allowed to be in a tag name + * Comma is not allowed to be in a tag name in order to simplify requests that + specify lists of tags + * All other characters are allowed to be in a tag name + * Each server can have up to 50 tags. + The resource point for these operations is /servers//tags A user can add a single tag to the server by sending PUT request to the @@ -610,3 +621,9 @@ user documentation. useful for API users to monitor when a volume extend operation completes for the given server instance. By default only users with the administrator role will be able to see event ``traceback`` details. + +2.52 +---- + + Adds support for applying tags when creating a server. The tag schema is + the same as in the `2.26`_ microversion. diff --git a/nova/api/openstack/compute/schemas/servers.py b/nova/api/openstack/compute/schemas/servers.py index 8fed84b77cd0..b6a2c54a8473 100644 --- a/nova/api/openstack/compute/schemas/servers.py +++ b/nova/api/openstack/compute/schemas/servers.py @@ -16,7 +16,7 @@ import copy from nova.api.validation import parameter_types from nova.api.validation.parameter_types import multi_params - +from nova.objects import instance base_create = { 'type': 'object', @@ -129,6 +129,16 @@ base_create_v242['properties']['server']['properties']['networks'] = { ]} +# 2.52 builds on 2.42 and makes the following changes: +# Allowing adding tags to instances when booting +base_create_v252 = copy.deepcopy(base_create_v242) +base_create_v252['properties']['server']['properties']['tags'] = { + "type": "array", + "items": parameter_types.tag, + "maxItems": instance.MAX_TAG_COUNT +} + + base_update = { 'type': 'object', 'properties': { diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index d4a4a497bd5b..48f0d64982b0 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -83,6 +83,7 @@ class ServersController(wsgi.Controller): schema_server_create_v232 = schema_servers.base_create_v232 schema_server_create_v237 = schema_servers.base_create_v237 schema_server_create_v242 = schema_servers.base_create_v242 + schema_server_create_v252 = schema_servers.base_create_v252 # NOTE(alex_xu): Please do not add more items into this list. This list # should be removed in the future. @@ -135,6 +136,7 @@ class ServersController(wsgi.Controller): # TODO(alex_xu): The final goal is that merging all of # extended json-schema into server main json-schema. + self._create_schema(self.schema_server_create_v252, '2.52') self._create_schema(self.schema_server_create_v242, '2.42') self._create_schema(self.schema_server_create_v237, '2.37') self._create_schema(self.schema_server_create_v232, '2.32') @@ -446,7 +448,8 @@ class ServersController(wsgi.Controller): @validation.schema(schema_server_create_v219, '2.19', '2.31') @validation.schema(schema_server_create_v232, '2.32', '2.36') @validation.schema(schema_server_create_v237, '2.37', '2.41') - @validation.schema(schema_server_create_v242, '2.42') + @validation.schema(schema_server_create_v242, '2.42', '2.51') + @validation.schema(schema_server_create_v252, '2.52') def create(self, req, body): """Creates a new server for a given user.""" context = req.environ['nova.context'] @@ -467,6 +470,9 @@ class ServersController(wsgi.Controller): availability_zone = create_kwargs.pop("availability_zone", None) + if api_version_request.is_supported(req, min_version='2.52'): + create_kwargs['tags'] = server_dict.get('tags') + helpers.translate_attributes(helpers.CREATE, server_dict, create_kwargs) diff --git a/nova/compute/api.py b/nova/compute/api.py index fdcb7d4d2f05..2ed1557e710d 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -986,7 +986,7 @@ class API(base.Base): max_count, base_options, boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, filter_properties, - key_pair): + key_pair, tags): # Reserve quotas num_instances, quotas = self._check_num_instances_quota( context, instance_type, min_count, max_count) @@ -1027,11 +1027,13 @@ class API(base.Base): block_device_mapping = ( self._bdm_validate_set_size_and_instance(context, instance, instance_type, block_device_mapping)) + instance_tags = self._transform_tags(tags, instance.uuid) build_request = objects.BuildRequest(context, instance=instance, instance_uuid=instance.uuid, project_id=instance.project_id, - block_device_mappings=block_device_mapping) + block_device_mappings=block_device_mapping, + tags=instance_tags) build_request.create() # Create an instance_mapping. The null cell_mapping indicates @@ -1179,7 +1181,7 @@ class API(base.Base): requested_networks, config_drive, block_device_mapping, auto_disk_config, filter_properties, reservation_id=None, legacy_bdm=True, shutdown_terminate=False, - check_server_group_quota=False): + check_server_group_quota=False, tags=None): """Verify all the input parameters regardless of the provisioning strategy being performed and schedule the instance(s) for creation. @@ -1192,6 +1194,7 @@ class API(base.Base): min_count = min_count or 1 max_count = max_count or min_count block_device_mapping = block_device_mapping or [] + tags = tags or [] if image_href: image_id, boot_meta = self._get_image(context, image_href) @@ -1237,11 +1240,13 @@ class API(base.Base): instance_group = self._get_requested_instance_group(context, filter_properties) - instances_to_build = self._provision_instances(context, instance_type, - min_count, max_count, base_options, boot_meta, security_groups, - block_device_mapping, shutdown_terminate, - instance_group, check_server_group_quota, filter_properties, - key_pair) + tags = self._create_tag_list_obj(context, tags) + + instances_to_build = self._provision_instances( + context, instance_type, min_count, max_count, base_options, + boot_meta, security_groups, block_device_mapping, + shutdown_terminate, instance_group, check_server_group_quota, + filter_properties, key_pair, tags) instances = [] request_specs = [] @@ -1276,7 +1281,8 @@ class API(base.Base): admin_password=admin_password, injected_files=injected_files, requested_networks=requested_networks, - block_device_mapping=block_device_mapping) + block_device_mapping=block_device_mapping, + tags=tags) return (instances, reservation_id) @@ -1583,6 +1589,33 @@ class API(base.Base): return instance + def _create_tag_list_obj(self, context, tags): + """Create TagList objects from simple string tags. + + :param context: security context. + :param tags: simple string tags from API request. + :returns: TagList object. + """ + tag_list = [objects.Tag(context=context, tag=t) for t in tags] + tag_list_obj = objects.TagList(objects=tag_list) + return tag_list_obj + + def _transform_tags(self, tags, resource_id): + """Change the resource_id of the tags according to the input param. + + Because this method can be called multiple times when more than one + instance is booted in a single request it makes a copy of the tags + list. + + :param tags: TagList object. + :param resource_id: string. + :returns: TagList object. + """ + instance_tags = tags.obj_clone() + for tag in instance_tags: + tag.resource_id = resource_id + return instance_tags + # This method remains because cellsv1 uses it in the scheduler def create_db_entry_for_new_instance(self, context, instance_type, image, instance, security_group, block_device_mapping, num_instances, @@ -1640,7 +1673,7 @@ class API(base.Base): access_ip_v4=None, access_ip_v6=None, requested_networks=None, config_drive=None, auto_disk_config=None, scheduler_hints=None, legacy_bdm=True, shutdown_terminate=False, - check_server_group_quota=False): + check_server_group_quota=False, tags=None): """Provision instances, sending instance information to the scheduler. The scheduler will determine where the instance(s) go and will handle creating the DB entries. @@ -1679,7 +1712,8 @@ class API(base.Base): filter_properties=filter_properties, legacy_bdm=legacy_bdm, shutdown_terminate=shutdown_terminate, - check_server_group_quota=check_server_group_quota) + check_server_group_quota=check_server_group_quota, + tags=tags) def _check_auto_disk_config(self, instance=None, image=None, **extra_instance_updates): diff --git a/nova/conductor/api.py b/nova/conductor/api.py index 080a3e8e6d83..ce6023454688 100644 --- a/nova/conductor/api.py +++ b/nova/conductor/api.py @@ -126,11 +126,12 @@ class ComputeTaskAPI(object): def schedule_and_build_instances(self, context, build_requests, request_spec, image, admin_password, injected_files, - requested_networks, block_device_mapping): + requested_networks, block_device_mapping, + tags=None): self.conductor_compute_rpcapi.schedule_and_build_instances( context, build_requests, request_spec, image, admin_password, injected_files, requested_networks, - block_device_mapping) + block_device_mapping, tags) def unshelve_instance(self, context, instance, request_spec=None): self.conductor_compute_rpcapi.unshelve_instance(context, diff --git a/nova/objects/build_request.py b/nova/objects/build_request.py index 82c49c1741dd..2f0dcd32c05c 100644 --- a/nova/objects/build_request.py +++ b/nova/objects/build_request.py @@ -295,6 +295,33 @@ class BuildRequestList(base.ObjectListBase, base.NovaObject): if (k not in instance.metadata or v != instance.metadata[k]): return False + elif filter_key in ( + 'tags', 'tags-any', 'not-tags', 'not-tags-any'): + # Get the list of simple string tags first. + tags = ([tag.tag for tag in instance.tags] + if instance.tags else []) + if filter_key == 'tags': + for item in filter_val: + if item not in tags: + return False + elif filter_key == 'tags-any': + found = [] + for item in filter_val: + if item in tags: + found.append(item) + if not found: + return False + elif filter_key == 'not-tags': + found = [] + for item in filter_val: + if item in tags: + found.append(item) + if len(found) == len(filter_val): + return False + elif filter_key == 'not-tags-any': + for item in filter_val: + if item in tags: + return False elif isinstance(filter_val, (list, tuple, set, frozenset)): if not filter_val: # Special value to indicate that nothing will match. @@ -364,10 +391,6 @@ class BuildRequestList(base.ObjectListBase, base.NovaObject): build_requests = cls.get_all(context) # Fortunately some filters do not apply here. - # 'tags' can not be applied at boot time so will not be set for an - # instance here. - # TODO(zhengzhenyu): Handle this when the API supports creating - # servers with tags. # 'changes-since' works off of the updated_at field which has not yet # been set at the point in the boot process where build_request still # exists. So it can be ignored. @@ -381,7 +404,8 @@ class BuildRequestList(base.ObjectListBase, base.NovaObject): exact_match_filter_names = ['project_id', 'user_id', 'image_ref', 'vm_state', 'instance_type_id', 'uuid', 'metadata', 'host', 'task_state', - 'system_metadata'] + 'system_metadata', 'tags', 'tags-any', + 'not-tags', 'not-tags-any'] exact_filters = {} regex_filters = {} for key, value in filters.items(): diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-create-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-create-req.json.tpl new file mode 100644 index 000000000000..220973ec37da --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-create-req.json.tpl @@ -0,0 +1,31 @@ +{ + "server" : { + "accessIPv4": "%(access_ip_v4)s", + "accessIPv6": "%(access_ip_v6)s", + "name" : "new-server-test", + "imageRef" : "%(image_id)s", + "flavorRef" : "http://openstack.example.com/flavors/1", + "availability_zone": "nova", + "OS-DCF:diskConfig": "AUTO", + "metadata" : { + "My Server Name" : "Apache1" + }, + "personality": [ + { + "path": "/etc/banner.txt", + "contents": "ICAgICAgDQoiQSBjbG91ZCBkb2VzIG5vdCBrbm93IHdoeSBp dCBtb3ZlcyBpbiBqdXN0IHN1Y2ggYSBkaXJlY3Rpb24gYW5k IGF0IHN1Y2ggYSBzcGVlZC4uLkl0IGZlZWxzIGFuIGltcHVs c2lvbi4uLnRoaXMgaXMgdGhlIHBsYWNlIHRvIGdvIG5vdy4g QnV0IHRoZSBza3kga25vd3MgdGhlIHJlYXNvbnMgYW5kIHRo ZSBwYXR0ZXJucyBiZWhpbmQgYWxsIGNsb3VkcywgYW5kIHlv dSB3aWxsIGtub3csIHRvbywgd2hlbiB5b3UgbGlmdCB5b3Vy c2VsZiBoaWdoIGVub3VnaCB0byBzZWUgYmV5b25kIGhvcml6 b25zLiINCg0KLVJpY2hhcmQgQmFjaA==" + } + ], + "security_groups": [ + { + "name": "default" + } + ], + "user_data" : "%(user_data)s", + "networks": "auto", + "tags": ["tag1", "tag2"] + }, + "OS-SCH-HNT:scheduler_hints": { + "same_host": "%(uuid)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-create-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-create-resp.json.tpl new file mode 100644 index 000000000000..4b30e0cfbdb8 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-create-resp.json.tpl @@ -0,0 +1,22 @@ +{ + "server": { + "OS-DCF:diskConfig": "AUTO", + "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" + } + ], + "security_groups": [ + { + "name": "default" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-get-resp.json.tpl new file mode 100644 index 000000000000..67caad0f2dd8 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/server-get-resp.json.tpl @@ -0,0 +1,88 @@ +{ + "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", + "description": null, + "host_status": "UP", + "locked": false, + "tags": ["tag1", "tag2"], + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": {}, + "original_name": "m1.tiny", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "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", + "config_drive": "%(cdrive)s", + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-AZ:availability_zone": "nova", + "OS-EXT-SRV-ATTR:host": "%(compute_host)s", + "OS-EXT-SRV-ATTR:hostname": "%(hostname)s", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "%(hypervisor_hostname)s", + "OS-EXT-SRV-ATTR:instance_name": "%(instance_name)s", + "OS-EXT-SRV-ATTR:kernel_id": "", + "OS-EXT-SRV-ATTR:launch_index": 0, + "OS-EXT-SRV-ATTR:ramdisk_id": "", + "OS-EXT-SRV-ATTR:reservation_id": "%(reservation_id)s", + "OS-EXT-SRV-ATTR:root_device_name": "/dev/sda", + "OS-EXT-SRV-ATTR:user_data": "%(user_data)s", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "os-extended-volumes:volumes_attached": [ + {"id": "volume_id1", "delete_on_termination": false}, + {"id": "volume_id2", "delete_on_termination": false} + ], + "OS-SRV-USG:launched_at": "%(strtime)s", + "OS-SRV-USG:terminated_at": null, + "progress": 0, + "security_groups": [ + { + "name": "default" + } + ], + "status": "ACTIVE", + "tenant_id": "6f70656e737461636b20342065766572", + "updated": "%(isotime)s", + "user_id": "fake" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/servers-details-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/servers-details-resp.json.tpl new file mode 100644 index 000000000000..6bbcde9d2145 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/servers-details-resp.json.tpl @@ -0,0 +1,90 @@ +{ + "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", + "description": null, + "host_status": "UP", + "locked": false, + "tags": ["tag1", "tag2"], + "flavor": { + "disk": 1, + "ephemeral": 0, + "extra_specs": {}, + "original_name": "m1.tiny", + "ram": 512, + "swap": 0, + "vcpus": 1 + }, + "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", + "config_drive": "%(cdrive)s", + "OS-DCF:diskConfig": "AUTO", + "OS-EXT-AZ:availability_zone": "nova", + "OS-EXT-SRV-ATTR:host": "%(compute_host)s", + "OS-EXT-SRV-ATTR:hostname": "%(hostname)s", + "OS-EXT-SRV-ATTR:hypervisor_hostname": "%(hypervisor_hostname)s", + "OS-EXT-SRV-ATTR:instance_name": "%(instance_name)s", + "OS-EXT-SRV-ATTR:kernel_id": "", + "OS-EXT-SRV-ATTR:launch_index": 0, + "OS-EXT-SRV-ATTR:ramdisk_id": "", + "OS-EXT-SRV-ATTR:reservation_id": "%(reservation_id)s", + "OS-EXT-SRV-ATTR:root_device_name": "/dev/sda", + "OS-EXT-SRV-ATTR:user_data": "%(user_data)s", + "OS-EXT-STS:power_state": 1, + "OS-EXT-STS:task_state": null, + "OS-EXT-STS:vm_state": "active", + "os-extended-volumes:volumes_attached": [ + {"id": "volume_id1", "delete_on_termination": false}, + {"id": "volume_id2", "delete_on_termination": false} + ], + "OS-SRV-USG:launched_at": "%(strtime)s", + "OS-SRV-USG:terminated_at": null, + "progress": 0, + "security_groups": [ + { + "name": "default" + } + ], + "status": "ACTIVE", + "tenant_id": "6f70656e737461636b20342065766572", + "updated": "%(isotime)s", + "user_id": "fake" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/servers-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/servers/v2.52/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.52/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/test_servers.py b/nova/tests/functional/api_sample_tests/test_servers.py index 0195564469eb..c859d665cf87 100644 --- a/nova/tests/functional/api_sample_tests/test_servers.py +++ b/nova/tests/functional/api_sample_tests/test_servers.py @@ -230,6 +230,12 @@ class ServersSampleJson247Test(ServersSampleJsonTest): self._verify_response('server-action-rebuild-resp', subs, resp, 202) +class ServersSampleJson252Test(ServersSampleJsonTest): + microversion = '2.52' + scenarios = [('v2_52', {'api_major_version': 'v2.1'})] + use_common_server_post = False + + class ServersUpdateSampleJsonTest(ServersSampleBase): def test_update_server(self): diff --git a/nova/tests/functional/wsgi/test_servers.py b/nova/tests/functional/wsgi/test_servers.py index c4a6da8bb1d7..8d643602ffde 100644 --- a/nova/tests/functional/wsgi/test_servers.py +++ b/nova/tests/functional/wsgi/test_servers.py @@ -183,3 +183,57 @@ class ServersPreSchedulingTestCase(test.TestCase): def test_instance_list_from_buildrequests_old_service(self): self._test_instance_list_from_buildrequests() + + def test_instance_list_from_buildrequests_with_tags(self): + """Creates two servers with two tags each, where the 2nd tag (tag2) + is the only intersection between the tags in both servers. This is + used to test the various tags filters working in the BuildRequestList. + """ + self.useFixture(nova_fixtures.AllServicesCurrent()) + image_ref = fake_image.get_valid_image_id() + body = { + 'server': { + 'name': 'foo', + 'imageRef': image_ref, + 'flavorRef': '1', + 'networks': 'none', + 'tags': ['tag1', 'tag2'] + } + } + inst1 = self.api.api_post('servers', body) + body['server']['name'] = 'bar' + body['server']['tags'] = ['tag2', 'tag3'] + inst2 = self.api.api_post('servers', body) + + # list servers using tags=tag1,tag2 + list_resp = self.api.api_get( + 'servers/detail?tags=tag1,tag2') + list_resp = list_resp.body['servers'] + self.assertEqual(1, len(list_resp)) + self.assertEqual(inst1.body['server']['id'], list_resp[0]['id']) + self.assertEqual('foo', list_resp[0]['name']) + + # list servers using tags-any=tag1,tag3 + list_resp = self.api.api_get( + 'servers/detail?tags-any=tag1,tag3') + list_resp = list_resp.body['servers'] + self.assertEqual(2, len(list_resp)) + # Default sort is created_at desc, so last created is first + self.assertEqual(inst2.body['server']['id'], list_resp[0]['id']) + self.assertEqual('bar', list_resp[0]['name']) + self.assertEqual(inst1.body['server']['id'], list_resp[1]['id']) + self.assertEqual('foo', list_resp[1]['name']) + + # list servers using not-tags=tag1,tag2 + list_resp = self.api.api_get( + 'servers/detail?not-tags=tag1,tag2') + list_resp = list_resp.body['servers'] + self.assertEqual(1, len(list_resp)) + self.assertEqual(inst2.body['server']['id'], list_resp[0]['id']) + self.assertEqual('bar', list_resp[0]['name']) + + # list servers using not-tags-any=tag1,tag3 + list_resp = self.api.api_get( + 'servers/detail?not-tags-any=tag1,tag3') + list_resp = list_resp.body['servers'] + self.assertEqual(0, len(list_resp)) diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index ec84f938d035..b6c6bfc6da0d 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -16,6 +16,7 @@ import collections import datetime +import ddt import uuid import fixtures @@ -56,6 +57,7 @@ from nova.image import glance from nova.network import manager from nova import objects from nova.objects import instance as instance_obj +from nova.objects import tag from nova.policies import servers as server_policies from nova import policy from nova import test @@ -3862,6 +3864,54 @@ class ServersControllerCreateTestV237(test.NoDBTestCase): [{'uuid': uuid}]) +@ddt.ddt +class ServersControllerCreateTestV252(test.NoDBTestCase): + def setUp(self): + super(ServersControllerCreateTestV252, self).setUp() + self.controller = servers.ServersController() + + self.body = { + 'server': { + 'name': 'device-tagging-server', + 'imageRef': '6b0edabb-8cde-4684-a3f4-978960a51378', + 'flavorRef': '2', + 'networks': [{ + 'uuid': 'ff608d40-75e9-48cb-b745-77bb55b5eaf2' + }] + } + } + + self.req = fakes.HTTPRequestV21.blank('/fake/servers', version='2.52') + self.req.method = 'POST' + self.req.headers['content-type'] = 'application/json' + + def _create_server(self, tags): + self.body['server']['tags'] = tags + self.req.body = jsonutils.dump_as_bytes(self.body) + return self.controller.create(self.req, body=self.body).obj['server'] + + def test_create_server_with_tags_pre_2_52_fails(self): + """Negative test to make sure you can't pass 'tags' before 2.52""" + self.req.api_version_request = \ + api_version_request.APIVersionRequest('2.51') + self.assertRaises( + exception.ValidationError, self._create_server, ['tag1']) + + @ddt.data([','], + ['/'], + ['a' * (tag.MAX_TAG_LENGTH + 1)], + ['a'] * (instance_obj.MAX_TAG_COUNT + 1), + [''], + [1, 2, 3], + {'tag': 'tag'}) + def test_create_server_with_tags_incorrect_tags(self, tags): + """Negative test to incorrect tags are not allowed""" + self.req.api_version_request = \ + api_version_request.APIVersionRequest('2.52') + self.assertRaises( + exception.ValidationError, self._create_server, tags) + + class ServersControllerCreateTestWithMock(test.TestCase): image_uuid = '76fa36fc-c930-4bf3-8c8a-ea2a2420deb6' flavor_ref = 'http://localhost/123/flavors/3' diff --git a/nova/tests/unit/compute/test_compute_api.py b/nova/tests/unit/compute/test_compute_api.py index f348ba9cac1b..f37e4ee3b756 100644 --- a/nova/tests/unit/compute/test_compute_api.py +++ b/nova/tests/unit/compute/test_compute_api.py @@ -3643,7 +3643,7 @@ class _ComputeAPIUnitTestMixIn(object): boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, filter_properties, - None) + None, objects.TagList()) do_test() @@ -3692,7 +3692,8 @@ class _ComputeAPIUnitTestMixIn(object): 1, 1, mock.MagicMock(), {}, None, None, None, None, {}, None, - fake_keypair) + fake_keypair, + objects.TagList()) self.assertEqual( 'test', mock_instance.return_value.keypairs.objects[0].name) @@ -3701,7 +3702,7 @@ class _ComputeAPIUnitTestMixIn(object): 1, 1, mock.MagicMock(), {}, None, None, None, None, {}, None, - None) + None, objects.TagList()) self.assertEqual( 0, len(mock_instance.return_value.keypairs.objects)) @@ -3764,6 +3765,7 @@ class _ComputeAPIUnitTestMixIn(object): 'device_name': 'vda', 'boot_index': 0, }))]) + instance_tags = objects.TagList(objects=[objects.Tag(tag='tag')]) shutdown_terminate = True instance_group = None check_server_group_quota = False @@ -3775,7 +3777,7 @@ class _ComputeAPIUnitTestMixIn(object): min_count, max_count, base_options, boot_meta, security_groups, block_device_mappings, shutdown_terminate, instance_group, check_server_group_quota, - filter_properties, None) + filter_properties, None, instance_tags) for rs, br, im in instances_to_build: self.assertIsInstance(br.instance, objects.Instance) @@ -3783,6 +3785,7 @@ class _ComputeAPIUnitTestMixIn(object): self.assertEqual(base_options['project_id'], br.instance.project_id) self.assertEqual(1, br.block_device_mappings[0].id) + self.assertEqual(br.instance.uuid, br.tags[0].resource_id) br.create.assert_called_with() do_test() @@ -3855,7 +3858,7 @@ class _ComputeAPIUnitTestMixIn(object): min_count, max_count, base_options, boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, - filter_properties, None)) + filter_properties, None, objects.TagList())) rs, br, im = instances_to_build[0] self.assertTrue(uuidutils.is_uuid_like(br.instance.uuid)) self.assertEqual(br.instance_uuid, im.instance_uuid) @@ -3943,14 +3946,14 @@ class _ComputeAPIUnitTestMixIn(object): check_server_group_quota = False filter_properties = {'scheduler_hints': None, 'instance_type': flavor} - + tags = objects.TagList() self.assertRaises(exception.InvalidVolume, self.compute_api._provision_instances, ctxt, flavor, min_count, max_count, base_options, boot_meta, security_groups, block_device_mapping, shutdown_terminate, instance_group, check_server_group_quota, filter_properties, - None) + None, tags) # First instance, build_req, mapping is created and destroyed self.assertTrue(build_req_mocks[0].create.called) self.assertTrue(build_req_mocks[0].destroy.called) @@ -3982,7 +3985,7 @@ class _ComputeAPIUnitTestMixIn(object): self.compute_api._provision_instances(ctxt, None, None, None, mock.MagicMock(), None, None, [], None, None, None, None, - None) + None, objects.TagList()) secgroups = mock_secgroup.populate_security_groups.return_value mock_objects.RequestSpec.from_components.assert_called_once_with( mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, mock.ANY, diff --git a/releasenotes/notes/support-tag-when-boot-4dd124371e3ef446.yaml b/releasenotes/notes/support-tag-when-boot-4dd124371e3ef446.yaml new file mode 100644 index 000000000000..31cdd7c479c3 --- /dev/null +++ b/releasenotes/notes/support-tag-when-boot-4dd124371e3ef446.yaml @@ -0,0 +1,4 @@ +--- +features: + - Adds support for applying tags when creating a server. + The tag schema is the same as in the 2.26 microversion.