diff --git a/doc/api_samples/os-server-tags/v2.26/server-tags-index-resp.json b/doc/api_samples/os-server-tags/v2.26/server-tags-index-resp.json new file mode 100644 index 000000000000..dee5cd391fd8 --- /dev/null +++ b/doc/api_samples/os-server-tags/v2.26/server-tags-index-resp.json @@ -0,0 +1,3 @@ +{ + "tags": ["sometag"] +} diff --git a/doc/api_samples/os-server-tags/v2.26/server-tags-put-all-req.json b/doc/api_samples/os-server-tags/v2.26/server-tags-put-all-req.json new file mode 100644 index 000000000000..54757dd00858 --- /dev/null +++ b/doc/api_samples/os-server-tags/v2.26/server-tags-put-all-req.json @@ -0,0 +1,5 @@ +{ + "tags": [ + "sometag" + ] +} diff --git a/doc/api_samples/os-server-tags/v2.26/server-tags-put-req.json b/doc/api_samples/os-server-tags/v2.26/server-tags-put-req.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/doc/api_samples/os-server-tags/v2.26/server-tags-show-details-resp.json b/doc/api_samples/os-server-tags/v2.26/server-tags-show-details-resp.json new file mode 100644 index 000000000000..70ce44223dab --- /dev/null +++ b/doc/api_samples/os-server-tags/v2.26/server-tags-show-details-resp.json @@ -0,0 +1,62 @@ +{ + "server": { + "tags": [ + "sometag" + ], + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff", + "OS-EXT-IPS:type": "fixed", + "version": 4 + } + ] + }, + "created": "2012-12-02T02:11:55Z", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "c949ab4256cea23b6089b710aa2df48bf6577ed915278b62e33ad8bb", + "id": "5046e2f2-3b33-4041-b3cf-e085f73e78e7", + "image": { + "id": "70a599e0-31e7-49b7-b260-868f441e862b", + "links": [ + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/images/70a599e0-31e7-49b7-b260-868f441e862b", + "rel": "bookmark" + } + ] + }, + "links": [ + { + "href": "http://openstack.example.com/v2/6f70656e737461636b20342065766572/servers/5046e2f2-3b33-4041-b3cf-e085f73e78e7", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/5046e2f2-3b33-4041-b3cf-e085f73e78e7", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "6f70656e737461636b20342065766572", + "updated": "2012-12-02T02:11:55Z", + "key_name": null, + "user_id": "fake", + "locked": false, + "description": null + } +} diff --git a/doc/api_samples/os-server-tags/v2.26/servers-tags-details-resp.json b/doc/api_samples/os-server-tags/v2.26/servers-tags-details-resp.json new file mode 100644 index 000000000000..6244bc28726a --- /dev/null +++ b/doc/api_samples/os-server-tags/v2.26/servers-tags-details-resp.json @@ -0,0 +1,62 @@ +{ + "servers": [ + { + "accessIPv4": "1.2.3.4", + "accessIPv6": "80fe::", + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "OS-EXT-IPS-MAC:mac_addr": "aa:bb:cc:dd:ee:ff", + "OS-EXT-IPS:type": "fixed", + "version": 4 + } + ] + }, + "created": "2013-09-03T04:01:32Z", + "flavor": { + "id": "1", + "links": [ + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/flavors/1", + "rel": "bookmark" + } + ] + }, + "hostId": "bcf92836fc9ed4203a75cb0337afc7f917d2be504164b995c2334b25", + "id": "f5dc173b-6804-445a-a6d8-c705dad5b5eb", + "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/6f70656e737461636b20342065766572/servers/f5dc173b-6804-445a-a6d8-c705dad5b5eb", + "rel": "self" + }, + { + "href": "http://openstack.example.com/6f70656e737461636b20342065766572/servers/f5dc173b-6804-445a-a6d8-c705dad5b5eb", + "rel": "bookmark" + } + ], + "metadata": { + "My Server Name": "Apache1" + }, + "name": "new-server-test", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "6f70656e737461636b20342065766572", + "updated": "2013-09-03T04:01:32Z", + "user_id": "fake", + "locked": false, + "tags": ["sometag"], + "description": null + } + ] +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 41151e581139..a5edda21c181 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.25", + "version": "2.26", "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 f5bbb1931878..6c944f5998be 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.25", + "version": "2.26", "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 1e0d59f544d8..7e252c83d27b 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -71,7 +71,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: * 2.24 - Add API to cancel a running live migration * 2.25 - Make block_migration support 'auto' and remove disk_over_commit for os-migrateLive. - + * 2.26 - Adds support of server tags """ # The minimum and maximum versions of the API supported @@ -80,7 +80,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.25" +_MAX_API_VERSION = "2.26" DEFAULT_API_VERSION = _MIN_API_VERSION diff --git a/nova/api/openstack/compute/extension_info.py b/nova/api/openstack/compute/extension_info.py index 45e8195ea845..8f0dcfee82ce 100644 --- a/nova/api/openstack/compute/extension_info.py +++ b/nova/api/openstack/compute/extension_info.py @@ -82,7 +82,8 @@ v21_to_v2_extension_list_mapping = { v2_extension_suppress_list = ['servers', 'images', 'versions', 'flavors', 'os-block-device-mapping-v1', 'os-consoles', 'extensions', 'image-metadata', 'ips', 'limits', - 'server-metadata', 'server-migrations' + 'server-metadata', 'server-migrations', + 'os-server-tags' ] # v2.1 plugins which should appear under a different name in v2 diff --git a/nova/api/openstack/compute/server_tags.py b/nova/api/openstack/compute/server_tags.py index 93d9bc6b7ea0..e614240cadfe 100644 --- a/nova/api/openstack/compute/server_tags.py +++ b/nova/api/openstack/compute/server_tags.py @@ -35,6 +35,7 @@ def _get_tags_names(tags): class ServerTagsController(wsgi.Controller): _view_builder_class = server_tags.ViewBuilder + @wsgi.Controller.api_version("2.26") @wsgi.response(204) @extensions.expected_errors(404) def show(self, req, server_id, id): @@ -51,6 +52,7 @@ class ServerTagsController(wsgi.Controller): % {'server_id': server_id, 'tag': id}) raise exc.HTTPNotFound(explanation=msg) + @wsgi.Controller.api_version("2.26") @extensions.expected_errors(404) def index(self, req, server_id): context = req.environ["nova.context"] @@ -63,6 +65,7 @@ class ServerTagsController(wsgi.Controller): return {'tags': _get_tags_names(tags)} + @wsgi.Controller.api_version("2.26") @extensions.expected_errors((400, 404)) @validation.schema(schema.update) def update(self, req, server_id, id, body): @@ -109,6 +112,7 @@ class ServerTagsController(wsgi.Controller): req, server_id, id) return response + @wsgi.Controller.api_version("2.26") @extensions.expected_errors((400, 404)) @validation.schema(schema.update_all) def update_all(self, req, server_id, body): @@ -149,6 +153,7 @@ class ServerTagsController(wsgi.Controller): return {'tags': _get_tags_names(tags)} + @wsgi.Controller.api_version("2.26") @wsgi.response(204) @extensions.expected_errors(404) def delete(self, req, server_id, id): @@ -162,6 +167,7 @@ class ServerTagsController(wsgi.Controller): except exception.InstanceNotFound as e: raise exc.HTTPNotFound(explanation=e.format_message()) + @wsgi.Controller.api_version("2.26") @wsgi.response(204) @extensions.expected_errors(404) def delete_all(self, req, server_id): diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 8e0869a84fa3..c34dffda9b14 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -45,6 +45,7 @@ from nova import objects from nova import utils ALIAS = 'servers' +TAG_SEARCH_FILTERS = ('tags', 'tags-any', 'not-tags', 'not-tags-any') CONF = cfg.CONF CONF.import_opt('enable_instance_password', @@ -353,6 +354,12 @@ class ServersController(wsgi.Controller): msg = _("Only administrators may list deleted instances") raise exc.HTTPForbidden(explanation=msg) + if api_version_request.is_supported(req, min_version='2.26'): + for tag_filter in TAG_SEARCH_FILTERS: + if tag_filter in search_opts: + search_opts[tag_filter] = search_opts[ + tag_filter].split(',') + # If tenant_id is passed as a search parameter this should # imply that all_tenants is also enabled unless explicitly # disabled. Note that the tenant_id parameter is filtered out @@ -397,6 +404,9 @@ class ServersController(wsgi.Controller): expected_attrs = ['pci_devices'] if is_detail: + if api_version_request.is_supported(req, '2.26'): + expected_attrs.append("tags") + # merge our expected attrs with what the view builder needs for # showing details expected_attrs = self._view_builder.get_show_expected_attrs( @@ -1141,6 +1151,8 @@ class ServersController(wsgi.Controller): 'ip', 'changes-since', 'all_tenants') if api_version_request.is_supported(req, min_version='2.5'): opt_list += ('ip6',) + if api_version_request.is_supported(req, min_version='2.26'): + opt_list += TAG_SEARCH_FILTERS return opt_list def _get_instance(self, context, instance_uuid): diff --git a/nova/api/openstack/compute/views/servers.py b/nova/api/openstack/compute/views/servers.py index 14947f517345..3ccc9f4d87a5 100644 --- a/nova/api/openstack/compute/views/servers.py +++ b/nova/api/openstack/compute/views/servers.py @@ -313,4 +313,7 @@ class ViewBuilderV21(ViewBuilder): server["server"]["description"] = instance.get( "display_description") + if api_version_request.is_supported(request, min_version="2.26"): + server["server"]["tags"] = [t.tag for t in instance.tags] + return server diff --git a/nova/api/openstack/rest_api_version_history.rst b/nova/api/openstack/rest_api_version_history.rst index 0710cb43cd37..01ca564b12c5 100644 --- a/nova/api/openstack/rest_api_version_history.rst +++ b/nova/api/openstack/rest_api_version_history.rst @@ -218,3 +218,68 @@ user documentation. Modify input parameter for ``os-migrateLive``. The block_migration will support 'auto' value, and disk_over_commit flag will be removed. + +2.26 +---- + + Added support of server tags. + + A user can create, update, delete or check existence of simple string tags + for servers by the os-server-tags plugin. + + 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 + /servers//tags/ + + where is any valid tag name. + + A user can replace **all** current server tags to the new set of tags + by sending PUT request to the /servers//tags. New set of tags + must be specified in request body. This set must be in list 'tags'. + + A user can remove specified tag from the server by sending DELETE request + to the /servers//tags/ + + where is tag name which user wants to remove. + + A user can remove **all** tags from the server by sending DELETE request + to the /servers//tags + + A user can get a set of server tags with information about server by sending + GET request to the /servers/ + + Request returns dictionary with information about specified server, including + list 'tags' :: + + { + 'id': {server_id}, + ... + 'tags': ['foo', 'bar', 'baz'] + } + + A user can get **only** a set of server tags by sending GET request to the + /servers//tags + + Response :: + + { + 'tags': ['foo', 'bar', 'baz'] + } + + A user can check if a tag exists or not on a server by sending + GET /servers/{server_id}/tags/{tag} + + Request returns `204 No Content` if tag exist on a server or `404 Not Found` + if tag doesn't exist on a server. + + A user can filter servers in GET /servers request by new filters: + + * tags + * tags-any + * not-tags + * not-tags-any + + These filters can be combined. Also user can use more than one string tags + for each filter. In this case string tags for each filter must be separated + by comma: GET /servers?tags=red&tags-any=green,orange diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-index-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-index-resp.json.tpl new file mode 100644 index 000000000000..1cfb48373346 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-index-resp.json.tpl @@ -0,0 +1,3 @@ +{ + "tags": ["%(tag)s"] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-put-all-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-put-all-req.json.tpl new file mode 100644 index 000000000000..e80aff6d8643 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-put-all-req.json.tpl @@ -0,0 +1,5 @@ +{ + "tags": [ + "%(tag)s" + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-put-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-put-req.json.tpl new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-show-details-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-show-details-resp.json.tpl new file mode 100644 index 000000000000..a70d8ad7441d --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/server-tags-show-details-resp.json.tpl @@ -0,0 +1,62 @@ +{ + "server": { + "tags": [ + "%(tag)s" + ], + "accessIPv4": "%(access_ip_v4)s", + "accessIPv6": "%(access_ip_v6)s", + "addresses": { + "private": [ + { + "addr": "192.168.0.3", + "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" + } + ] + }, + "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", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "6f70656e737461636b20342065766572", + "updated": "%(isotime)s", + "key_name": null, + "user_id": "fake", + "locked": false, + "description": null + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/servers-tags-details-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/servers-tags-details-resp.json.tpl new file mode 100644 index 000000000000..cca555ef4c9c --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-server-tags/v2.26/servers-tags-details-resp.json.tpl @@ -0,0 +1,62 @@ +{ + "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", + "progress": 0, + "status": "ACTIVE", + "tenant_id": "6f70656e737461636b20342065766572", + "updated": "%(isotime)s", + "user_id": "fake", + "locked": false, + "tags": ["%(tag)s"], + "description": null + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/test_server_tags.py b/nova/tests/functional/api_sample_tests/test_server_tags.py new file mode 100644 index 000000000000..c808c3b921b2 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/test_server_tags.py @@ -0,0 +1,96 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.db.sqlalchemy import models +from nova.tests.functional.api_sample_tests import test_servers + +TAG = 'sometag' + + +class ServerTagsJsonTest(test_servers.ServersSampleBase): + extension_name = 'os-server-tags' + microversion = '2.26' + scenarios = [('v2_26', {'api_major_version': 'v2.1'})] + + def _get_create_subs(self): + return {'tag': TAG} + + def _put_server_tags(self): + """Verify the response status and returns the UUID of the + newly created server with tags. + """ + uuid = self._post_server() + subs = self._get_create_subs() + response = self._do_put('servers/%s/tags' % uuid, + 'server-tags-put-all-req', subs) + self.assertEqual(200, response.status_code) + return uuid + + def test_server_tags_update_all(self): + self._put_server_tags() + + def test_server_tags_show(self): + uuid = self._put_server_tags() + response = self._do_get('servers/%s/tags/%s' % (uuid, TAG)) + self.assertEqual(204, response.status_code) + + def test_server_tags_show_with_details_information(self): + uuid = self._put_server_tags() + response = self._do_get('servers/%s' % uuid) + subs = self._get_regexes() + subs['hostid'] = '[a-f0-9]+' + subs['tag'] = '[0-9a-zA-Z]+' + subs['access_ip_v4'] = '1.2.3.4' + subs['access_ip_v6'] = '80fe::' + self._verify_response('server-tags-show-details-resp', + subs, response, 200) + + def test_server_tags_list_with_details_information(self): + self._put_server_tags() + response = self._do_get('servers/detail') + subs = self._get_regexes() + subs['hostid'] = '[a-f0-9]+' + subs['tag'] = '[0-9a-zA-Z]+' + subs['access_ip_v4'] = '1.2.3.4' + subs['access_ip_v6'] = '80fe::' + self._verify_response('servers-tags-details-resp', subs, response, 200) + + def test_server_tags_index(self): + uuid = self._put_server_tags() + response = self._do_get('servers/%s/tags' % uuid) + subs = self._get_regexes() + subs['tag'] = '[0-9a-zA-Z]+' + self._verify_response('server-tags-index-resp', subs, response, 200) + + def test_server_tags_update(self): + uuid = self._put_server_tags() + tag = models.Tag() + tag.resource_id = uuid + tag.tag = 'OtherTag' + response = self._do_put('servers/%s/tags/%s' % (uuid, tag.tag), + 'server-tags-put-req', {}) + self.assertEqual(201, response.status_code) + expected_location = "%s/servers/%s/tags/%s" % ( + self._get_vers_compute_endpoint(), uuid, tag.tag) + self.assertEqual(expected_location, response.headers['Location']) + + def test_server_tags_delete(self): + uuid = self._put_server_tags() + response = self._do_delete('servers/%s/tags/%s' % (uuid, TAG)) + self.assertEqual(204, response.status_code) + self.assertEqual('', response.content) + + def test_server_tags_delete_all(self): + uuid = self._put_server_tags() + response = self._do_delete('servers/%s/tags' % uuid) + self.assertEqual(204, response.status_code) + self.assertEqual('', response.content) diff --git a/nova/tests/unit/api/openstack/compute/test_server_tags.py b/nova/tests/unit/api/openstack/compute/test_server_tags.py index 48e42dccf960..7781044fd239 100644 --- a/nova/tests/unit/api/openstack/compute/test_server_tags.py +++ b/nova/tests/unit/api/openstack/compute/test_server_tags.py @@ -32,6 +32,8 @@ NON_EXISTING_UUID = '123' class ServerTagsTest(test.TestCase): + api_version = '2.26' + def setUp(self): super(ServerTagsTest, self).setUp() self.controller = server_tags.ServerTagsController() @@ -43,7 +45,7 @@ class ServerTagsTest(test.TestCase): return tag def _get_request(self, url, method): - request = fakes.HTTPRequest.blank(url) + request = fakes.HTTPRequest.blank(url, version=self.api_version) request.method = method return request diff --git a/nova/tests/unit/api/openstack/compute/test_serversV21.py b/nova/tests/unit/api/openstack/compute/test_serversV21.py index a40929742eb0..078b8a3e6309 100644 --- a/nova/tests/unit/api/openstack/compute/test_serversV21.py +++ b/nova/tests/unit/api/openstack/compute/test_serversV21.py @@ -1462,6 +1462,65 @@ class ServersControllerTestV219(ServersControllerTest): self._test_list_server_detail_with_descriptions('desc1', 'desc2') +class ServersControllerTestV226(ControllerTest): + wsgi_api_version = '2.26' + + @mock.patch.object(compute_api.API, 'get') + def test_get_server_with_tags_by_id(self, mock_get): + req = fakes.HTTPRequest.blank('/fake/servers/%s' % FAKE_UUID, + version=self.wsgi_api_version) + ctxt = req.environ['nova.context'] + + fake_server = fakes.stub_instance_obj( + ctxt, id=2, vm_state=vm_states.ACTIVE, progress=100) + + tags = ['tag1', 'tag2'] + tag_list = objects.TagList(objects=[ + objects.Tag(resource_id=FAKE_UUID, tag=tag) + for tag in tags]) + + fake_server.tags = tag_list + mock_get.return_value = fake_server + + res_dict = self.controller.show(req, FAKE_UUID) + + self.assertIn('tags', res_dict['server']) + self.assertEqual(res_dict['server']['tags'], tags) + + @mock.patch.object(compute_api.API, 'get_all') + def _test_get_servers_allows_tag_filters(self, filter_name, mock_get_all): + server_uuid = str(uuid.uuid4()) + req = fakes.HTTPRequest.blank('/fake/servers?%s=t1,t2' % filter_name, + version=self.wsgi_api_version) + ctxt = req.environ['nova.context'] + + def fake_get_all(*a, **kw): + self.assertIsNotNone(kw['search_opts']) + self.assertIn(filter_name, kw['search_opts']) + self.assertEqual(kw['search_opts'][filter_name], ['t1', 't2']) + return objects.InstanceList( + objects=[fakes.stub_instance_obj(ctxt, uuid=server_uuid)]) + + mock_get_all.side_effect = fake_get_all + + servers = self.controller.index(req)['servers'] + + self.assertEqual(len(servers), 1) + self.assertEqual(servers[0]['id'], server_uuid) + + def test_get_servers_allows_tags_filter(self): + self._test_get_servers_allows_tag_filters('tags') + + def test_get_servers_allows_tags_any_filter(self): + self._test_get_servers_allows_tag_filters('tags-any') + + def test_get_servers_allows_not_tags_filter(self): + self._test_get_servers_allows_tag_filters('not-tags') + + def test_get_servers_allows_not_tags_any_filter(self): + self._test_get_servers_allows_tag_filters('not-tags-any') + + class ServersControllerDeleteTest(ControllerTest): def setUp(self): diff --git a/releasenotes/notes/bp-instance-tags-3acb227083320796.yaml b/releasenotes/notes/bp-instance-tags-3acb227083320796.yaml new file mode 100644 index 000000000000..2b6134ba7dcf --- /dev/null +++ b/releasenotes/notes/bp-instance-tags-3acb227083320796.yaml @@ -0,0 +1,4 @@ +--- +features: + - Microversion v2.26 allows to create/update/delete simple string tags. + They can be used for filtering servers by these tags. diff --git a/setup.cfg b/setup.cfg index 120d41b9a8e2..5304381c2aa5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -142,6 +142,7 @@ nova.api.v21.extensions = server_metadata = nova.api.openstack.compute.server_metadata:ServerMetadata server_migrations = nova.api.openstack.compute.server_migrations:ServerMigrations server_password = nova.api.openstack.compute.server_password:ServerPassword + server_tags = nova.api.openstack.compute.server_tags:ServerTags server_usage = nova.api.openstack.compute.server_usage:ServerUsage server_groups = nova.api.openstack.compute.server_groups:ServerGroups servers = nova.api.openstack.compute.servers:Servers