diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 0f8b98308..8661aef9c 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1") # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.25") +API_MAX_VERSION = api_versions.APIVersion("2.26") diff --git a/novaclient/tests/unit/fixture_data/servers.py b/novaclient/tests/unit/fixture_data/servers.py index 9b68a6560..bf0dac922 100644 --- a/novaclient/tests/unit/fixture_data/servers.py +++ b/novaclient/tests/unit/fixture_data/servers.py @@ -316,6 +316,50 @@ class Base(base.Fixture): self.url(1234, 'os-server-password'), status_code=202, headers=self.json_headers) + # + # Server tags + # + + self.requests.register_uri('GET', + self.url(1234, 'tags'), + json={'tags': ['tag1', 'tag2']}, + headers=self.json_headers) + + self.requests.register_uri('GET', + self.url(1234, 'tags', 'tag'), + status_code=204, + headers=self.json_headers) + + self.requests.register_uri('DELETE', + self.url(1234, 'tags', 'tag'), + status_code=204, + headers=self.json_headers) + + self.requests.register_uri('DELETE', + self.url(1234, 'tags'), + status_code=204, + headers=self.json_headers) + + def put_server_tag(request, context): + body = jsonutils.loads(request.body) + assert body is None + context.status_code = 201 + return None + + self.requests.register_uri('PUT', + self.url(1234, 'tags', 'tag'), + json=put_server_tag, + headers=self.json_headers) + + def put_server_tags(request, context): + body = jsonutils.loads(request.body) + assert list(body) == ['tags'] + return body + + self.requests.register_uri('PUT', + self.url(1234, 'tags'), + json=put_server_tags, + headers=self.json_headers) class V1(Base): diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index cd2144763..bc4c6e638 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -2221,6 +2221,24 @@ class FakeHTTPClient(base_client.HTTPClient): def delete_servers_1234_migrations_1(self): return (202, {}, None) + def put_servers_1234_tags_tag(self, **kw): + return (201, {}, None) + + def put_servers_1234_tags(self, **kw): + return (201, {}, None) + + def get_servers_1234_tags(self, **kw): + return (200, {}, {'tags': ['tag1', 'tag2']}) + + def delete_servers_1234_tags_tag(self, **kw): + return (204, {}, None) + + def delete_servers_1234_tags(self, **kw): + return (204, {}, None) + + def get_servers_1234_tags_tag(self, **kw): + return (204, {}, None) + class FakeSessionClient(fakes.FakeClient, client.Client): diff --git a/novaclient/tests/unit/v2/test_servers.py b/novaclient/tests/unit/v2/test_servers.py index cdbddd0f9..51e35731b 100644 --- a/novaclient/tests/unit/v2/test_servers.py +++ b/novaclient/tests/unit/v2/test_servers.py @@ -1148,3 +1148,45 @@ class ServersV225Test(ServersV219Test): s = self.cs.servers.get(1234) self.assertRaises(ValueError, s.live_migrate, 'hostname', 'auto', 'True') + + +class ServersV226Test(ServersV225Test): + def setUp(self): + super(ServersV219Test, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.26") + + def test_tag_list(self): + s = self.cs.servers.get(1234) + ret = s.tag_list() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/tags') + + def test_tag_delete(self): + s = self.cs.servers.get(1234) + ret = s.delete_tag('tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234/tags/tag') + + def test_tag_delete_all(self): + s = self.cs.servers.get(1234) + ret = s.delete_all_tags() + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('DELETE', '/servers/1234/tags') + + def test_tag_add(self): + s = self.cs.servers.get(1234) + ret = s.add_tag('tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/servers/1234/tags/tag') + + def test_tags_set(self): + s = self.cs.servers.get(1234) + ret = s.set_tags(['tag1', 'tag2']) + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('PUT', '/servers/1234/tags') + + def test_tag_exists(self): + s = self.cs.servers.get(1234) + ret = s.tag_exists('tag') + self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST) + self.assert_called('GET', '/servers/1234/tags/tag') diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 92257d612..766d7e85f 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2775,6 +2775,52 @@ class ShellTest(utils.TestCase): self.run_command('list', api_version='2.10') self.assert_called('GET', '/servers/detail') + def test_server_tag_add(self): + self.run_command('server-tag-add sample-server tag', + api_version='2.26') + self.assert_called('PUT', '/servers/1234/tags/tag', None) + + def test_server_tag_set(self): + self.run_command('server-tag-set sample-server tag1 tag2', + api_version='2.26') + self.assert_called('PUT', '/servers/1234/tags', + {'tags': ['tag1', 'tag2']}) + + def test_server_tag_list(self): + self.run_command('server-tag-list sample-server', api_version='2.26') + self.assert_called('GET', '/servers/1234/tags') + + def test_server_tag_delete(self): + self.run_command('server-tag-delete sample-server tag', + api_version='2.26') + self.assert_called('DELETE', '/servers/1234/tags/tag') + + def test_server_tag_delete_all(self): + self.run_command('server-tag-delete-all sample-server', + api_version='2.26') + self.assert_called('DELETE', '/servers/1234/tags') + + def test_server_tag_exists(self): + self.run_command('server-tag-exists sample-server tag', + api_version='2.26') + self.assert_called('GET', '/servers/1234/tags/tag') + + def test_list_v2_26_tags(self): + self.run_command('list --tags tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?tags=tag1%2Ctag2') + + def test_list_v2_26_tags_any(self): + self.run_command('list --tags-any tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?tags-any=tag1%2Ctag2') + + def test_list_v2_26_not_tags(self): + self.run_command('list --not-tags tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?not-tags=tag1%2Ctag2') + + def test_list_v2_26_not_tags_any(self): + self.run_command('list --not-tags-any tag1,tag2', api_version='2.26') + self.assert_called('GET', '/servers/detail?not-tags-any=tag1%2Ctag2') + class ShellWithSessionClientTest(ShellTest): diff --git a/novaclient/v2/servers.py b/novaclient/v2/servers.py index b81e0475e..51aef7776 100644 --- a/novaclient/v2/servers.py +++ b/novaclient/v2/servers.py @@ -526,6 +526,42 @@ class Server(base.Resource): """Trigger crash dump in an instance""" return self.manager.trigger_crash_dump(self) + def tag_list(self): + """ + Get list of tags from an instance. + """ + return self.manager.tag_list(self) + + def delete_tag(self, tag): + """ + Remove single tag from an instance. + """ + return self.manager.delete_tag(self, tag) + + def delete_all_tags(self): + """ + Remove all tags from an instance. + """ + return self.manager.delete_all_tags(self) + + def set_tags(self, tags): + """ + Set list of tags to an instance. + """ + return self.manager.set_tags(self, tags) + + def add_tag(self, tag): + """ + Add single tag to an instance. + """ + return self.manager.add_tag(self, tag) + + def tag_exists(self, tag): + """ + Check if an instance has specified tag. + """ + return self.manager.tag_exists(self, tag) + class NetworkInterface(base.Resource): @property @@ -1709,3 +1745,51 @@ class ServerManager(base.BootingManagerWithFind): url = '/servers/%s/remote-consoles' % base.getid(server) resp, body = self.api.client.post(url, body=body) return self.convert_into_with_meta(body, resp) + + @api_versions.wraps('2.26') + def tag_list(self, server): + """ + Get list of tags from an instance. + """ + resp, body = self.api.client.get( + "/servers/%s/tags" % base.getid(server)) + return base.ListWithMeta(body['tags'], resp) + + @api_versions.wraps('2.26') + def delete_tag(self, server, tag): + """ + Remove single tag from an instance. + """ + return self._delete("/servers/%s/tags/%s" % (base.getid(server), tag)) + + @api_versions.wraps('2.26') + def delete_all_tags(self, server): + """ + Remove all tags from an instance. + """ + return self._delete("/servers/%s/tags" % base.getid(server)) + + @api_versions.wraps('2.26') + def set_tags(self, server, tags): + """ + Set list of tags to an instance. + """ + body = {"tags": tags} + return self._update("/servers/%s/tags" % base.getid(server), body) + + @api_versions.wraps('2.26') + def add_tag(self, server, tag): + """ + Add single tag to an instance. + """ + return self._update( + "/servers/%s/tags/%s" % (base.getid(server), tag), None) + + @api_versions.wraps('2.26') + def tag_exists(self, server, tag): + """ + Check if an instance has specified tag. + """ + resp, body = self.api.client.get( + "/servers/%s/tags/%s" % (base.getid(server), tag)) + return self.convert_into_with_meta(body, resp) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 058c6a342..bd4d5d3e9 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -1461,6 +1461,45 @@ def do_image_delete(cs, args): help=_("List only servers changed after a certain point of time." "The provided time should be an ISO 8061 formatted time." "ex 2016-03-04T06:27:59Z .")) +@utils.arg( + '--tags', + dest='tags', + metavar='', + default=None, + help=_("The given tags must all be present for a server to be included in " + "the list result. Boolean expression in this case is 't1 AND t2'. " + "Tags must be separated by commas: --tags "), + start_version="2.26") +@utils.arg( + '--tags-any', + dest='tags-any', + metavar='', + default=None, + help=_("If one of the given tags is present the server will be included " + "in the list result. Boolean expression in this case is " + "'t1 OR t2'. Tags must be separated by commas: " + "--tags-any "), + start_version="2.26") +@utils.arg( + '--not-tags', + dest='not-tags', + metavar='', + default=None, + help=_("Only the servers that do not have any of the given tags will" + "be included in the list results. Boolean expression in this case " + "is 'NOT(t1 AND t2)'. Tags must be separated by commas: " + "--not-tags "), + start_version="2.26") +@utils.arg( + '--not-tags-any', + dest='not-tags-any', + metavar='', + default=None, + help=_("Only the servers that do not have at least one of the given tags" + "will be included in the list result. Boolean expression in this " + "case is 'NOT(t1 OR t2)'. Tags must be separated by commas: " + "--not-tags-any "), + start_version="2.26") def do_list(cs, args): """List active servers.""" imageid = None @@ -1488,6 +1527,10 @@ def do_list(cs, args): 'instance_name': args.instance_name, 'changes-since': args.changes_since} + for arg in ('tags', "tags-any", 'not-tags', 'not-tags-any'): + if arg in args: + search_opts[arg] = getattr(args, arg) + filters = {'flavor': lambda f: f['id'], 'security_groups': utils.format_security_groups} @@ -4956,3 +4999,56 @@ def do_virtual_interface_list(cs, args): server = _find_server(cs, args.server) interface_list = cs.virtual_interfaces.list(base.getid(server)) _print_virtual_interface_list(cs, interface_list) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_tag_list(cs, args): + """Get list of tags from a server.""" + server = _find_server(cs, args.server) + tags = server.tag_list() + utils.print_list(tags, 'name') + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tag', metavar='', help=_('Tag to add.')) +def do_server_tag_add(cs, args): + """Add single tag to a server.""" + server = _find_server(cs, args.server) + server.add_tag(args.tag) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tags', metavar='', nargs='+', help=_('Tag(s) to set.')) +def do_server_tag_set(cs, args): + """Set list of tags to a server.""" + server = _find_server(cs, args.server) + server.set_tags(args.tags) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tag', metavar='', help=_('Tag to delete.')) +def do_server_tag_delete(cs, args): + """Delete single tag from a server.""" + server = _find_server(cs, args.server) + server.delete_tag(args.tag) + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +def do_server_tag_delete_all(cs, args): + """Delete all tags from a server.""" + server = _find_server(cs, args.server) + server.delete_all_tags() + + +@api_versions.wraps("2.26") +@utils.arg('server', metavar='', help=_('Name or ID of server.')) +@utils.arg('tag', metavar='', help=_('Tag to check if it exists or not.')) +def do_server_tag_exists(cs, args): + """Check if a server has specified tag.""" + server = _find_server(cs, args.server) + server.tag_exists(args.tag)