From f67f4af8bd733f5bd186a7c02211ed0667e08a96 Mon Sep 17 00:00:00 2001 From: Hirofumi Ichihara Date: Tue, 1 Mar 2016 12:37:15 +0900 Subject: [PATCH] Add tags support This patch adds the tag support for CLI. Change-Id: Ia84b873e3916e7b9668181fe14c1448ef608bf1d Partial-Implements: blueprint add-tags-to-core-resources Related-Bug: #1489291 --- neutronclient/neutron/v2_0/__init__.py | 3 + neutronclient/neutron/v2_0/network.py | 21 +++ neutronclient/neutron/v2_0/tag.py | 105 ++++++++++++++ neutronclient/shell.py | 4 + neutronclient/tests/unit/test_cli20_tag.py | 128 ++++++++++++++++++ neutronclient/v2_0/client.py | 22 +++ .../add-tag-support-bad62d60ecc7075c.yaml | 16 +++ 7 files changed, 299 insertions(+) create mode 100644 neutronclient/neutron/v2_0/tag.py create mode 100644 neutronclient/tests/unit/test_cli20_tag.py create mode 100644 releasenotes/notes/add-tag-support-bad62d60ecc7075c.yaml diff --git a/neutronclient/neutron/v2_0/__init__.py b/neutronclient/neutron/v2_0/__init__.py index d740aac3e..9da48286a 100644 --- a/neutronclient/neutron/v2_0/__init__.py +++ b/neutronclient/neutron/v2_0/__init__.py @@ -36,6 +36,7 @@ HEX_ELEM = '[0-9A-Fa-f]' UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}', HEX_ELEM + '{4}', HEX_ELEM + '{4}', HEX_ELEM + '{12}']) +HYPHEN_OPTS = ['tags_any', 'not_tags', 'not_tags_any'] def _get_resource_plural(resource, client): @@ -691,6 +692,8 @@ class ListCommand(NeutronCommand, lister.Lister): for field in self.filter_attrs] for attr in filter_attrs: val = getattr(parsed_args, attr, None) + if attr in HYPHEN_OPTS: + attr = attr.replace('_', '-') if val: search_opts[attr] = val return search_opts diff --git a/neutronclient/neutron/v2_0/network.py b/neutronclient/neutron/v2_0/network.py index 2fb26d327..4ec7ace8e 100644 --- a/neutronclient/neutron/v2_0/network.py +++ b/neutronclient/neutron/v2_0/network.py @@ -60,6 +60,27 @@ class ListNetwork(neutronV20.ListCommand): {'name': 'router:external', 'help': _('Filter and list the networks which are external.'), 'boolean': True}, + {'name': 'tags', + 'help': _("Filter and list %s which has all given tags. " + "Multiple tags can be set like --tags "), + 'boolean': False, + 'argparse_kwargs': {'metavar': 'TAG'}}, + {'name': 'tags_any', + 'help': _("Filter and list %s which has any given tags. " + "Multiple tags can be set like --tags-any "), + 'boolean': False, + 'argparse_kwargs': {'metavar': 'TAG'}}, + {'name': 'not_tags', + 'help': _("Filter and list %s which does not have all given tags. " + "Multiple tags can be set like --not-tags "), + 'boolean': False, + 'argparse_kwargs': {'metavar': 'TAG'}}, + {'name': 'not_tags_any', + 'help': _("Filter and list %s which does not have any given tags. " + "Multiple tags can be set like --not-tags-any " + ""), + 'boolean': False, + 'argparse_kwargs': {'metavar': 'TAG'}}, ] def extend_list(self, data, parsed_args): diff --git a/neutronclient/neutron/v2_0/tag.py b/neutronclient/neutron/v2_0/tag.py new file mode 100644 index 000000000..2507d417e --- /dev/null +++ b/neutronclient/neutron/v2_0/tag.py @@ -0,0 +1,105 @@ +# 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 neutronclient._i18n import _ +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronv20 + + +# List of resources can be set tag +TAG_RESOURCES = ['network'] + + +def _convert_resource_args(client, parsed_args): + resource_type = neutronv20._get_resource_plural( + parsed_args.resource_type, client) + resource_id = neutronv20.find_resourceid_by_name_or_id( + client, parsed_args.resource_type, parsed_args.resource) + return resource_type, resource_id + + +def _add_common_arguments(parser): + parser.add_argument('--resource-type', + choices=TAG_RESOURCES, + dest='resource_type', + required=True, + help=_('Resource Type.')) + parser.add_argument('--resource', + required=True, + help=_('Resource name or ID.')) + + +class AddTag(neutronv20.NeutronCommand): + """Add a tag into the resource.""" + + def get_parser(self, prog_name): + parser = super(AddTag, self).get_parser(prog_name) + _add_common_arguments(parser) + parser.add_argument('--tag', + required=True, + help=_('Tag to be added.')) + return parser + + def take_action(self, parsed_args): + client = self.get_client() + resource_type, resource_id = _convert_resource_args(client, + parsed_args) + client.add_tag(resource_type, resource_id, parsed_args.tag) + + +class ReplaceTag(neutronv20.NeutronCommand): + """Replace all tags on the resource.""" + + def get_parser(self, prog_name): + parser = super(ReplaceTag, self).get_parser(prog_name) + _add_common_arguments(parser) + parser.add_argument('--tag', + metavar='TAG', + action='append', + dest='tags', + required=True, + help=_('Tag (This option can be repeated).')) + return parser + + def take_action(self, parsed_args): + client = self.get_client() + resource_type, resource_id = _convert_resource_args(client, + parsed_args) + body = {'tags': parsed_args.tags} + client.replace_tag(resource_type, resource_id, body) + + +class RemoveTag(neutronv20.NeutronCommand): + """Remove a tag on the resource.""" + + def get_parser(self, prog_name): + parser = super(RemoveTag, self).get_parser(prog_name) + _add_common_arguments(parser) + tag_opt = parser.add_mutually_exclusive_group() + tag_opt.add_argument('--all', + action='store_true', + help=_('Remove all tags on the resource.')) + tag_opt.add_argument('--tag', + help=_('Tag to be removed.')) + return parser + + def take_action(self, parsed_args): + if not parsed_args.all and not parsed_args.tag: + raise exceptions.CommandError( + _("--all or --tag must be specified")) + client = self.get_client() + resource_type, resource_id = _convert_resource_args(client, + parsed_args) + if parsed_args.all: + client.remove_tag_all(resource_type, resource_id) + else: + client.remove_tag(resource_type, resource_id, parsed_args.tag) diff --git a/neutronclient/shell.py b/neutronclient/shell.py index bb923c2e8..5261a349e 100644 --- a/neutronclient/shell.py +++ b/neutronclient/shell.py @@ -83,6 +83,7 @@ from neutronclient.neutron.v2_0 import securitygroup from neutronclient.neutron.v2_0 import servicetype from neutronclient.neutron.v2_0 import subnet from neutronclient.neutron.v2_0 import subnetpool +from neutronclient.neutron.v2_0 import tag from neutronclient.neutron.v2_0.vpn import endpoint_group from neutronclient.neutron.v2_0.vpn import ikepolicy from neutronclient.neutron.v2_0.vpn import ipsec_site_connection @@ -444,6 +445,9 @@ COMMAND_V2 = { 'bgp-peer-delete': bgp_peer.DeletePeer, 'net-ip-availability-list': network_ip_availability.ListIpAvailability, 'net-ip-availability-show': network_ip_availability.ShowIpAvailability, + 'tag-add': tag.AddTag, + 'tag-replace': tag.ReplaceTag, + 'tag-remove': tag.RemoveTag, } COMMANDS = {'2.0': COMMAND_V2} diff --git a/neutronclient/tests/unit/test_cli20_tag.py b/neutronclient/tests/unit/test_cli20_tag.py new file mode 100644 index 000000000..24f07b73a --- /dev/null +++ b/neutronclient/tests/unit/test_cli20_tag.py @@ -0,0 +1,128 @@ +# 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. + +import sys + +from mox3 import mox + +from neutronclient.common import exceptions +from neutronclient.neutron import v2_0 as neutronV2_0 +from neutronclient.neutron.v2_0 import network +from neutronclient.neutron.v2_0 import tag +from neutronclient import shell +from neutronclient.tests.unit import test_cli20 + + +class CLITestV20Tag(test_cli20.CLITestV20Base): + def _test_tag_operation(self, cmd, path, method, args, prog_name, + body=None): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + if body: + body = test_cli20.MyComparator(body, self.client) + self.client.httpclient.request( + test_cli20.MyUrlComparator( + test_cli20.end_url(path, format=self.format), self.client), + method, body=body, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(204), None)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser(prog_name) + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + + def _test_tags_query(self, cmd, resources, args, query): + self.mox.StubOutWithMock(cmd, "get_client") + self.mox.StubOutWithMock(self.client.httpclient, "request") + cmd.get_client().MultipleTimes().AndReturn(self.client) + path = getattr(self.client, resources + "_path") + res = {resources: [{'id': 'myid'}]} + resstr = self.client.serialize(res) + self.client.httpclient.request( + test_cli20.MyUrlComparator( + test_cli20.end_url(path, query, format=self.format), + self.client), + 'GET', body=None, + headers=mox.ContainsKeyValue( + 'X-Auth-Token', test_cli20.TOKEN)).AndReturn( + (test_cli20.MyResp(200), resstr)) + self.mox.ReplayAll() + cmd_parser = cmd.get_parser("list_networks") + shell.run_command(cmd, cmd_parser, args) + self.mox.VerifyAll() + self.mox.UnsetStubs() + _str = self.fake_stdout.make_string() + self.assertIn('myid', _str) + + def _make_tag_path(self, resource, resource_id, tag): + path = getattr(self.client, "tag_path") + resource_plural = neutronV2_0._get_resource_plural(resource, + self.client) + return path % (resource_plural, resource_id, tag) + + def _make_tags_path(self, resource, resource_id): + path = getattr(self.client, "tags_path") + resource_plural = neutronV2_0._get_resource_plural(resource, + self.client) + return path % (resource_plural, resource_id) + + def test_add_tag(self): + cmd = tag.AddTag(test_cli20.MyApp(sys.stdout), None) + path = self._make_tag_path('network', 'myid', 'red') + args = ['--resource-type', 'network', '--resource', 'myid', + '--tag', 'red'] + self._test_tag_operation(cmd, path, 'PUT', args, "tag-add") + + def test_replace_tag(self): + cmd = tag.ReplaceTag(test_cli20.MyApp(sys.stdout), None) + path = self._make_tags_path('network', 'myid') + args = ['--resource-type', 'network', '--resource', 'myid', + '--tag', 'red', '--tag', 'blue'] + body = {'tags': ['red', 'blue']} + self._test_tag_operation(cmd, path, 'PUT', args, "tag-replace", + body=body) + + def test_remove_tag(self): + cmd = tag.RemoveTag(test_cli20.MyApp(sys.stdout), None) + path = self._make_tag_path('network', 'myid', 'red') + args = ['--resource-type', 'network', '--resource', 'myid', + '--tag', 'red'] + self._test_tag_operation(cmd, path, 'DELETE', args, "tag-remove") + + def test_remove_tag_all(self): + cmd = tag.RemoveTag(test_cli20.MyApp(sys.stdout), None) + path = self._make_tags_path('network', 'myid') + args = ['--resource-type', 'network', '--resource', 'myid', + '--all'] + self._test_tag_operation(cmd, path, 'DELETE', args, "tag-remove") + + def test_no_tag_nor_all(self): + cmd = tag.RemoveTag(test_cli20.MyApp(sys.stdout), None) + path = self._make_tags_path('network', 'myid') + args = ['--resource-type', 'network', '--resource', 'myid'] + self.assertRaises(exceptions.CommandError, self._test_tag_operation, + cmd, path, 'DELETE', args, "tag-remove") + + def test_tags_query(self): + # This test examines that '-' in the tag related filters + # is not converted to '_'. + resources = 'networks' + cmd = network.ListNetwork(test_cli20.MyApp(sys.stdout), None) + self.mox.StubOutWithMock(network.ListNetwork, "extend_list") + network.ListNetwork.extend_list(mox.IsA(list), mox.IgnoreArg()) + args = ['--not-tags', 'red,blue', '--tags-any', 'green', + '--not-tags-any', 'black'] + query = "not-tags=red,blue&tags-any=green¬-tags-any=black" + self._test_tags_query(cmd, resources, args, query) diff --git a/neutronclient/v2_0/client.py b/neutronclient/v2_0/client.py index dbe3af5ef..f25b007e5 100644 --- a/neutronclient/v2_0/client.py +++ b/neutronclient/v2_0/client.py @@ -528,6 +528,8 @@ class Client(ClientBase): bgp_peer_path = "/bgp-peers/%s" network_ip_availabilities_path = '/network-ip-availabilities' network_ip_availability_path = '/network-ip-availabilities/%s' + tags_path = "/%s/%s/tags" + tag_path = "/%s/%s/tags/%s" # API has no way to report plurals, so we have to hard code them EXTED_PLURALS = {'routers': 'router', @@ -2002,6 +2004,26 @@ class Client(ClientBase): return self.get(self.network_ip_availability_path % (network), params=_params) + @APIParamsCall + def add_tag(self, resource_type, resource_id, tag, **_params): + """Add a tag on the resource.""" + return self.put(self.tag_path % (resource_type, resource_id, tag)) + + @APIParamsCall + def replace_tag(self, resource_type, resource_id, body, **_params): + """Replace tags on the resource.""" + return self.put(self.tags_path % (resource_type, resource_id), body) + + @APIParamsCall + def remove_tag(self, resource_type, resource_id, tag, **_params): + """Remove a tag on the resource.""" + return self.delete(self.tag_path % (resource_type, resource_id, tag)) + + @APIParamsCall + def remove_tag_all(self, resource_type, resource_id, **_params): + """Remove all tags on the resource.""" + return self.delete(self.tags_path % (resource_type, resource_id)) + def __init__(self, **kwargs): """Initialize a new client for the Neutron v2.0 API.""" super(Client, self).__init__(**kwargs) diff --git a/releasenotes/notes/add-tag-support-bad62d60ecc7075c.yaml b/releasenotes/notes/add-tag-support-bad62d60ecc7075c.yaml new file mode 100644 index 000000000..cc5b6eec2 --- /dev/null +++ b/releasenotes/notes/add-tag-support-bad62d60ecc7075c.yaml @@ -0,0 +1,16 @@ +--- +features: + - | + CLI support for tag. + + * The ``tag-add`` command sets a tag on the network resource. It also + includes ``--resource-type``, ``--resource`` and ``--tag`` options. + * The ``tag-replace`` command replaces tags on the network resource. It + also includes ``--resource-type``, ``--resource`` and ``--tag`` + options. More than one ``--tag`` options can be set. + * The ``tag-remove`` command removes tags on the network resource. It also + includes ``--resource-type``, ``--resource``, ``--tag`` and ``--all`` + options. The ``--all`` option allow to remove all tags on the network + resource. + * The ``net-list`` command includes ``--tags``, ``--tags-any``, + ``--not-tags`` and ``--not-tags-any`` options. \ No newline at end of file