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
This commit is contained in:
Hirofumi Ichihara 2016-03-01 12:37:15 +09:00
parent e9560866f7
commit f67f4af8bd
7 changed files with 299 additions and 0 deletions

View File

@ -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

View File

@ -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 <tag[,tag...]>"),
'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 <tag[,tag...]>"),
'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 <tag[,tag...]>"),
'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 "
"<tag[,tag...]>"),
'boolean': False,
'argparse_kwargs': {'metavar': 'TAG'}},
]
def extend_list(self, data, parsed_args):

View File

@ -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)

View File

@ -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}

View File

@ -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&not-tags-any=black"
self._test_tags_query(cmd, resources, args, query)

View File

@ -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)

View File

@ -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.