diff --git a/doc/source/cli/command-objects/network.rst b/doc/source/cli/command-objects/network.rst index ed9fd13d1d..5f20dc3884 100644 --- a/doc/source/cli/command-objects/network.rst +++ b/doc/source/cli/command-objects/network.rst @@ -32,6 +32,7 @@ Create new network [--provider-segment ] [--qos-policy ] [--transparent-vlan | --no-transparent-vlan] + [--tag | --no-tag] .. option:: --project @@ -165,6 +166,18 @@ Create new network *Network version 2 only* +.. option:: --tag + + Tag to be added to the network (repeat option to set multiple tags) + + *Network version 2 only* + +.. option:: --no-tag + + No tags associated with the network + + *Network version 2 only* + .. _network_create-name: .. describe:: @@ -206,6 +219,8 @@ List networks [--provider-physical-network ] [--provider-segment ] [--agent ] + [--tags [,,...]] [--any-tags [,,...]] + [--not-tags [,,...]] [--not-any-tags [,,...]] .. option:: --external @@ -297,6 +312,32 @@ List networks List networks hosted by agent (ID only) + *Network version 2 only* + +.. option:: --tags [,,...] + + List networks which have all given tag(s) + + *Network version 2 only* + +.. option:: --any-tags [,,...] + + List networks which have any given tag(s) + + *Network version 2 only* + +.. option:: --not-tags [,,...] + + Exclude networks which have all given tag(s) + + *Network version 2 only* + +.. option:: --not-any-tags [,,...] + + Exclude networks which have any given tag(s) + + *Network version 2 only* + network set ----------- @@ -318,6 +359,7 @@ Set network properties [--provider-physical-network ] [--provider-segment ] [--qos-policy | --no-qos-policy] + [--tag ] [--no-tag] .. option:: --name @@ -392,6 +434,15 @@ Set network properties Remove the QoS policy attached to this network +.. option:: --tag + + Tag to be added to the network (repeat option to set multiple tags) + +.. option:: --no-tag + + Clear tags associated with the network. Specify both --tag + and --no-tag to overwrite current tags + .. _network_set-network: .. describe:: @@ -412,3 +463,31 @@ Display network details .. describe:: Network to display (name or ID) + +network unset +------------- + +Unset network properties + +*Network version 2 only* + +.. program:: network unset +.. code:: bash + + openstack network unset + [--tag | --all-tag] + + +.. option:: --tag + + Tag to be removed from the network + (repeat option to remove multiple tags) + +.. option:: --all-tag + + Clear all tags associated with the network + +.. _network_unset-network: +.. describe:: + + Network to modify (name or ID) diff --git a/doc/source/cli/command-objects/port.rst b/doc/source/cli/command-objects/port.rst index 37814a9595..c2da09b321 100644 --- a/doc/source/cli/command-objects/port.rst +++ b/doc/source/cli/command-objects/port.rst @@ -33,6 +33,7 @@ Create new port [--qos-policy ] [--project [--project-domain ]] [--enable-port-security | --disable-port-security] + [--tag | --no-tag] .. option:: --network @@ -126,6 +127,14 @@ Create new port Disable port security for this port +.. option:: --tag + + Tag to be added to the port (repeat option to set multiple tags) + +.. option:: --no-tag + + No tags associated with the port + .. _port_create-name: .. describe:: @@ -163,6 +172,8 @@ List ports [--fixed-ip subnet=,ip-address=] [--long] [--project [--project-domain ]] + [--tags [,,...]] [--any-tags [,,...]] + [--not-tags [,,...]] [--not-any-tags [,,...]] .. option:: --device-owner @@ -204,6 +215,22 @@ List ports Domain the project belongs to (name or ID). This can be used in case collisions between project names exist. +.. option:: --tags [,,...] + + List ports which have all given tag(s) + +.. option:: --any-tags [,,...] + + List ports which have any given tag(s) + +.. option:: --not-tags [,,...] + + Exclude ports which have all given tag(s) + +.. option:: --not-any-tags [,,...] + + Exclude ports which have any given tag(s) + port set -------- @@ -233,6 +260,7 @@ Set port properties [--allowed-address ip-address=[,mac-address=]] [--no-allowed-address] [--data-plane-status ] + [--tag ] [--no-tag] .. option:: --description @@ -342,6 +370,15 @@ Set port properties Unset it to None with the 'port unset' command (requires data plane status extension) +.. option:: --tag + + Tag to be added to the port (repeat option to set multiple tags) + +.. option:: --no-tag + + Clear tags associated with the port. Specify both --tag + and --no-tag to overwrite current tags + .. _port_set-port: .. describe:: @@ -378,6 +415,7 @@ Unset port properties [--allowed-address ip-address=[,mac-address=] [...]] [--qos-policy] [--data-plane-status] + [--tag | --all-tag] .. option:: --fixed-ip subnet=,ip-address= @@ -410,6 +448,15 @@ Unset port properties Clear existing information of data plane status +.. option:: --tag + + Tag to be removed from the port + (repeat option to remove multiple tags) + +.. option:: --all-tag + + Clear all tags associated with the port + .. _port_unset-port: .. describe:: diff --git a/doc/source/cli/command-objects/router.rst b/doc/source/cli/command-objects/router.rst index 8bdf81dbf7..9c9364bc75 100644 --- a/doc/source/cli/command-objects/router.rst +++ b/doc/source/cli/command-objects/router.rst @@ -67,6 +67,7 @@ Create new router [--ha | --no-ha] [--description ] [--availability-zone-hint ] + [--tag | --no-tag] .. option:: --project @@ -121,6 +122,14 @@ Create new router (Router Availability Zone extension required, repeat option to set multiple availability zones) +.. option:: --tag + + Tag to be added to the router (repeat option to set multiple tags) + +.. option:: --no-tag + + No tags associated with the router + .. _router_create-name: .. describe:: @@ -156,6 +165,8 @@ List routers [--long] [--project [--project-domain ]] [--agent ] + [--tags [,,...]] [--any-tags [,,...]] + [--not-tags [,,...]] [--not-any-tags [,,...]] .. option:: --agent @@ -186,6 +197,22 @@ List routers Domain the project belongs to (name or ID). This can be used in case collisions between project names exist. +.. option:: --tags [,,...] + + List routers which have all given tag(s) + +.. option:: --any-tags [,,...] + + List routers which have any given tag(s) + +.. option:: --not-tags [,,...] + + Exclude routers which have all given tag(s) + +.. option:: --not-any-tags [,,...] + + Exclude routers which have any given tag(s) + router remove port ------------------ @@ -246,6 +273,7 @@ Set router properties [--route destination=,gateway= | --no-route] [--ha | --no-ha] [--external-gateway [--enable-snat|--disable-snat] [--fixed-ip subnet=,ip-address=]] + [--tag ] [--no-tag] .. option:: --name @@ -311,6 +339,15 @@ Set router properties subnet=,ip-address= (repeat option to set multiple fixed IP addresses) +.. option:: --tag + + Tag to be added to the router (repeat option to set multiple tags) + +.. option:: --no-tag + + Clear tags associated with the router. Specify both --tag + and --no-tag to overwrite current tags + .. _router_set-router: .. describe:: @@ -343,6 +380,7 @@ Unset router properties openstack router unset [--route destination=,gateway=] [--external-gateway] + [--tag | --all-tag] .. option:: --route destination=,gateway= @@ -356,6 +394,15 @@ Unset router properties Remove external gateway information from the router +.. option:: --tag + + Tag to be removed from the router + (repeat option to remove multiple tags) + +.. option:: --all-tag + + Clear all tags associated with the router + .. _router_unset-router: .. describe:: diff --git a/doc/source/cli/command-objects/subnet-pool.rst b/doc/source/cli/command-objects/subnet-pool.rst index 3a60974a97..0cff4d7f56 100644 --- a/doc/source/cli/command-objects/subnet-pool.rst +++ b/doc/source/cli/command-objects/subnet-pool.rst @@ -25,6 +25,7 @@ Create subnet pool [--default | --no-default] [--share | --no-share] [--default-quota ] + [--tag | --no-tag] --pool-prefix [...] @@ -79,6 +80,14 @@ Create subnet pool Set default quota for subnet pool as the number of IP addresses allowed in a subnet +.. option:: --tag + + Tag to be added to the subnet pool (repeat option to set multiple tags) + +.. option:: --no-tag + + No tags associated with the subnet pool + .. option:: --pool-prefix Set subnet pool prefixes (in CIDR notation) @@ -120,6 +129,8 @@ List subnet pools [--project [--project-domain ]] [--name ] [--address-scope ] + [--tags [,,...]] [--any-tags [,,...]] + [--not-tags [,,...]] [--not-any-tags [,,...]] .. option:: --long @@ -158,6 +169,22 @@ List subnet pools List only subnet pools of given address scope in output (name or ID) +.. option:: --tags [,,...] + + List subnet pools which have all given tag(s) + +.. option:: --any-tags [,,...] + + List subnet pools which have any given tag(s) + +.. option:: --not-tags [,,...] + + Exclude subnet pools which have all given tag(s) + +.. option:: --not-any-tags [,,...] + + Exclude subnet pools which have any given tag(s) + subnet pool set --------------- @@ -176,6 +203,7 @@ Set subnet pool properties [--default | --no-default] [--description ] [--default-quota ] + [--tag ] [--no-tag] .. option:: --name @@ -225,6 +253,15 @@ Set subnet pool properties Set default quota for subnet pool as the number of IP addresses allowed in a subnet +.. option:: --tag + + Tag to be added to the subnet pool (repeat option to set multiple tags) + +.. option:: --no-tag + + Clear tags associated with the subnet pool. Specify both --tag + and --no-tag to overwrite current tags + .. _subnet_pool_set-subnet-pool: .. describe:: @@ -256,6 +293,7 @@ Unset subnet pool properties openstack subnet pool unset [--pool-prefix [...]] + [--tag | --all-tag] .. option:: --pool-prefix @@ -263,6 +301,15 @@ Unset subnet pool properties Remove subnet pool prefixes (in CIDR notation). (repeat option to unset multiple prefixes). +.. option:: --tag + + Tag to be removed from the subnet pool + (repeat option to remove multiple tags) + +.. option:: --all-tag + + Clear all tags associated with the subnet pool + .. _subnet_pool_unset-subnet-pool: .. describe:: diff --git a/doc/source/cli/command-objects/subnet.rst b/doc/source/cli/command-objects/subnet.rst index 4e60936120..c228dc207d 100644 --- a/doc/source/cli/command-objects/subnet.rst +++ b/doc/source/cli/command-objects/subnet.rst @@ -31,6 +31,7 @@ Create new subnet [--ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}] [--network-segment ] [--service-type ] + [--tag | --no-tag] --network @@ -125,6 +126,14 @@ Create new subnet Must be a valid device owner value for a network port (repeat option to set multiple service types) +.. option:: --tag + + Tag to be added to the subnet (repeat option to set multiple tags) + +.. option:: --no-tag + + No tags associated with the subnet + .. option:: --network Network this subnet belongs to (name or ID) @@ -167,6 +176,8 @@ List subnets [--gateway ] [--name ] [--subnet-range ] + [--tags [,,...]] [--any-tags [,,...]] + [--not-tags [,,...]] [--not-any-tags [,,...]] .. option:: --long @@ -218,6 +229,22 @@ List subnets List only subnets of given subnet range (in CIDR notation) in output e.g.: ``--subnet-range 10.10.0.0/16`` +.. option:: --tags [,,...] + + List subnets which have all given tag(s) + +.. option:: --any-tags [,,...] + + List subnets which have any given tag(s) + +.. option:: --not-tags [,,...] + + Exclude subnets which have all given tag(s) + +.. option:: --not-any-tags [,,...] + + Exclude subnets which have any given tag(s) + subnet set ---------- @@ -238,6 +265,7 @@ Set subnet properties [--service-type ] [--name ] [--description ] + [--tag ] [--no-tag] .. option:: --allocation-pool start=,end= @@ -305,6 +333,15 @@ Set subnet properties Updated name of the subnet +.. option:: --tag + + Tag to be added to the subnet (repeat option to set multiple tags) + +.. option:: --no-tag + + Clear tags associated with the subnet. Specify both --tag + and --no-tag to overwrite current tags + .. _subnet_set-subnet: .. describe:: @@ -340,6 +377,7 @@ Unset subnet properties [--dns-nameserver [...]] [--host-route destination=,gateway= [...]] [--service-type ] + [--tag | --all-tag] .. option:: --dns-nameserver @@ -368,6 +406,15 @@ Unset subnet properties Must be a valid device owner value for a network port (repeat option to unset multiple service types) +.. option:: --tag + + Tag to be removed from the subnet + (repeat option to remove multiple tags) + +.. option:: --all-tag + + Clear all tags associated with the subnet + .. _subnet_unset-subnet: .. describe:: diff --git a/openstackclient/network/v2/_tag.py b/openstackclient/network/v2/_tag.py new file mode 100644 index 0000000000..d1e59937fa --- /dev/null +++ b/openstackclient/network/v2/_tag.py @@ -0,0 +1,134 @@ +# 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 argparse + +from openstackclient.i18n import _ + + +class _CommaListAction(argparse.Action): + + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values.split(',')) + + +def add_tag_filtering_option_to_parser(parser, collection_name): + parser.add_argument( + '--tags', + metavar='[,,...]', + action=_CommaListAction, + help=_('List %s which have all given tag(s) ' + '(Comma-separated list of tags)') % collection_name + ) + parser.add_argument( + '--any-tags', + metavar='[,,...]', + action=_CommaListAction, + help=_('List %s which have any given tag(s) ' + '(Comma-separated list of tags)') % collection_name + ) + parser.add_argument( + '--not-tags', + metavar='[,,...]', + action=_CommaListAction, + help=_('Exclude %s which have all given tag(s) ' + '(Comma-separated list of tags)') % collection_name + ) + parser.add_argument( + '--not-any-tags', + metavar='[,,...]', + action=_CommaListAction, + help=_('Exclude %s which have any given tag(s) ' + '(Comma-separated list of tags)') % collection_name + ) + + +def get_tag_filtering_args(parsed_args, args): + if parsed_args.tags: + args['tags'] = ','.join(parsed_args.tags) + if parsed_args.any_tags: + args['any_tags'] = ','.join(parsed_args.any_tags) + if parsed_args.not_tags: + args['not_tags'] = ','.join(parsed_args.not_tags) + if parsed_args.not_any_tags: + args['not_any_tags'] = ','.join(parsed_args.not_any_tags) + + +def add_tag_option_to_parser_for_create(parser, resource_name): + tag_group = parser.add_mutually_exclusive_group() + tag_group.add_argument( + '--tag', + action='append', + dest='tags', + metavar='', + help=_("Tag to be added to the %s " + "(repeat option to set multiple tags)") % resource_name + ) + tag_group.add_argument( + '--no-tag', + action='store_true', + help=_("No tags associated with the %s") % resource_name + ) + + +def add_tag_option_to_parser_for_set(parser, resource_name): + parser.add_argument( + '--tag', + action='append', + dest='tags', + metavar='', + help=_("Tag to be added to the %s " + "(repeat option to set multiple tags)") % resource_name + ) + parser.add_argument( + '--no-tag', + action='store_true', + help=_("Clear tags associated with the %s. Specify both " + "--tag and --no-tag to overwrite current tags") % resource_name + ) + + +def update_tags_for_set(client, obj, parsed_args): + if parsed_args.no_tag: + tags = set() + else: + tags = set(obj.tags) + if parsed_args.tags: + tags |= set(parsed_args.tags) + if set(obj.tags) != tags: + client.set_tags(obj, list(tags)) + + +def add_tag_option_to_parser_for_unset(parser, resource_name): + tag_group = parser.add_mutually_exclusive_group() + tag_group.add_argument( + '--tag', + action='append', + dest='tags', + metavar='', + help=_("Tag to be removed from the %s " + "(repeat option to remove multiple tags)") % resource_name) + tag_group.add_argument( + '--all-tag', + action='store_true', + help=_("Clear all tags associated with the %s") % resource_name) + + +def update_tags_for_unset(client, obj, parsed_args): + tags = set(obj.tags) + if parsed_args.all_tag: + tags = set() + if parsed_args.tags: + tags -= set(parsed_args.tags) + if set(obj.tags) != tags: + client.set_tags(obj, list(tags)) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 33decd8213..4c1725c5f6 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -20,6 +20,7 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common from openstackclient.network import sdk_utils +from openstackclient.network.v2 import _tag def _format_admin_state(item): @@ -280,6 +281,7 @@ class CreateNetwork(common.NetworkAndComputeShowOne): help=_("Do not make the network VLAN transparent")) _add_additional_network_options(parser) + _tag.add_tag_option_to_parser_for_create(parser, _('network')) return parser def update_parser_compute(self, parser): @@ -299,6 +301,8 @@ class CreateNetwork(common.NetworkAndComputeShowOne): attrs['vlan_transparent'] = False obj = client.create_network(**attrs) + # tags cannot be set when created, so tags need to be set later. + _tag.update_tags_for_set(client, obj, parsed_args) display_columns, columns = _get_columns_network(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -424,7 +428,9 @@ class ListNetwork(common.NetworkAndComputeLister): '--agent', metavar='', dest='agent_id', - help=_('List networks hosted by agent (ID only)')) + help=_('List networks hosted by agent (ID only)') + ) + _tag.add_tag_filtering_option_to_parser(parser, _('networks')) return parser def take_action_network(self, client, parsed_args): @@ -441,6 +447,7 @@ class ListNetwork(common.NetworkAndComputeLister): 'provider_network_type', 'is_router_external', 'availability_zones', + 'tags', ) column_headers = ( 'ID', @@ -453,6 +460,7 @@ class ListNetwork(common.NetworkAndComputeLister): 'Network Type', 'Router Type', 'Availability Zones', + 'Tags', ) elif parsed_args.agent_id: columns = ( @@ -534,6 +542,8 @@ class ListNetwork(common.NetworkAndComputeLister): args['provider:segmentation_id'] = parsed_args.segmentation_id args['provider_segmentation_id'] = parsed_args.segmentation_id + _tag.get_tag_filtering_args(parsed_args, args) + data = client.networks(**args) return (column_headers, @@ -656,6 +666,7 @@ class SetNetwork(command.Command): action='store_true', help=_("Remove the QoS policy attached to this network") ) + _tag.add_tag_option_to_parser_for_set(parser, _('network')) _add_additional_network_options(parser) return parser @@ -664,7 +675,11 @@ class SetNetwork(command.Command): obj = client.find_network(parsed_args.network, ignore_missing=False) attrs = _get_attrs_network(self.app.client_manager, parsed_args) - client.update_network(obj, **attrs) + if attrs: + client.update_network(obj, **attrs) + + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_set(client, obj, parsed_args) class ShowNetwork(common.NetworkAndComputeShowOne): @@ -689,3 +704,27 @@ class ShowNetwork(common.NetworkAndComputeShowOne): display_columns, columns = _get_columns_compute(obj) data = utils.get_dict_properties(obj, columns) return (display_columns, data) + + +class UnsetNetwork(command.Command): + _description = _("Unset network properties") + + def get_parser(self, prog_name): + parser = super(UnsetNetwork, self).get_parser(prog_name) + parser.add_argument( + 'network', + metavar="", + help=_("Network to modify (name or ID)") + ) + _tag.add_tag_option_to_parser_for_unset(parser, _('network')) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_network(parsed_args.network, ignore_missing=False) + + # NOTE: As of now, UnsetNetwork has no attributes which need + # to be updated by update_network(). + + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_unset(client, obj, parsed_args) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index d7f197e016..9536fe8687 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -26,6 +26,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils +from openstackclient.network.v2 import _tag LOG = logging.getLogger(__name__) @@ -47,6 +48,7 @@ _formatters = { 'extra_dhcp_opts': utils.format_list_of_dicts, 'fixed_ips': utils.format_list_of_dicts, 'security_group_ids': utils.format_list, + 'tags': utils.format_list, } @@ -384,6 +386,7 @@ class CreatePort(command.ShowOne): "ip-address=[,mac-address=] " "(repeat option to set multiple allowed-address pairs)") ) + _tag.add_tag_option_to_parser_for_create(parser, _('port')) return parser def take_action(self, parsed_args): @@ -416,6 +419,8 @@ class CreatePort(command.ShowOne): attrs['qos_policy_id'] = client.find_qos_policy( parsed_args.qos_policy, ignore_missing=False).id obj = client.create_port(**attrs) + # tags cannot be set when created, so tags need to be set later. + _tag.update_tags_for_set(client, obj, parsed_args) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) @@ -512,6 +517,7 @@ class ListPort(command.Lister): "(name or ID): subnet=,ip-address= " "(repeat option to set multiple fixed IP addresses)"), ) + _tag.add_tag_filtering_option_to_parser(parser, _('ports')) return parser def take_action(self, parsed_args): @@ -535,8 +541,8 @@ class ListPort(command.Lister): filters = {} if parsed_args.long: - columns += ('security_group_ids', 'device_owner',) - column_headers += ('Security Groups', 'Device Owner',) + columns += ('security_group_ids', 'device_owner', 'tags') + column_headers += ('Security Groups', 'Device Owner', 'Tags') if parsed_args.device_owner is not None: filters['device_owner'] = parsed_args.device_owner if parsed_args.router: @@ -566,6 +572,8 @@ class ListPort(command.Lister): filters['fixed_ips'] = _prepare_filter_fixed_ips( self.app.client_manager, parsed_args) + _tag.get_tag_filtering_args(parsed_args, filters) + data = network_client.ports(**filters) return (column_headers, @@ -694,6 +702,7 @@ class SetPort(command.Command): "Unset it to None with the 'port unset' command " "(requires data plane status extension)") ) + _tag.add_tag_option_to_parser_for_set(parser, _('port')) return parser @@ -750,7 +759,11 @@ class SetPort(command.Command): if parsed_args.data_plane_status: attrs['data_plane_status'] = parsed_args.data_plane_status - client.update_port(obj, **attrs) + if attrs: + client.update_port(obj, **attrs) + + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_set(client, obj, parsed_args) class ShowPort(command.ShowOne): @@ -834,6 +847,8 @@ class UnsetPort(command.Command): help=_("Clear existing information of data plane status") ) + _tag.add_tag_option_to_parser_for_unset(parser, _('port')) + return parser def take_action(self, parsed_args): @@ -889,3 +904,6 @@ class UnsetPort(command.Command): if attrs: client.update_port(obj, **attrs) + + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_unset(client, obj, parsed_args) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 8db0c4393b..4f9085373c 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -26,6 +26,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils +from openstackclient.network.v2 import _tag LOG = logging.getLogger(__name__) @@ -57,6 +58,7 @@ _formatters = { 'availability_zones': utils.format_list, 'availability_zone_hints': utils.format_list, 'routes': _format_routes, + 'tags': utils.format_list, } @@ -217,6 +219,7 @@ class CreateRouter(command.ShowOne): "(Router Availability Zone extension required, " "repeat option to set multiple availability zones)") ) + _tag.add_tag_option_to_parser_for_create(parser, _('router')) return parser @@ -229,6 +232,8 @@ class CreateRouter(command.ShowOne): if parsed_args.no_ha: attrs['ha'] = False obj = client.create_router(**attrs) + # tags cannot be set when created, so tags need to be set later. + _tag.update_tags_for_set(client, obj, parsed_args) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) @@ -310,6 +315,7 @@ class ListRouter(command.Lister): metavar='', help=_("List routers hosted by an agent (ID only)") ) + _tag.add_tag_filtering_option_to_parser(parser, _('routers')) return parser @@ -357,6 +363,8 @@ class ListRouter(command.Lister): args['tenant_id'] = project_id args['project_id'] = project_id + _tag.get_tag_filtering_args(parsed_args, args) + if parsed_args.agent is not None: agent = client.get_agent(parsed_args.agent) data = client.agent_hosted_routers(agent) @@ -384,6 +392,8 @@ class ListRouter(command.Lister): column_headers = column_headers + ( 'Availability zones', ) + columns = columns + ('tags',) + column_headers = column_headers + ('Tags',) return (column_headers, (utils.get_item_properties( @@ -567,6 +577,7 @@ class SetRouter(command.Command): action='store_true', help=_("Disable Source NAT on external gateway") ) + _tag.add_tag_option_to_parser_for_set(parser, _('router')) return parser def take_action(self, parsed_args): @@ -625,7 +636,10 @@ class SetRouter(command.Command): ips.append(ip_spec) gateway_info['external_fixed_ips'] = ips attrs['external_gateway_info'] = gateway_info - client.update_router(obj, **attrs) + if attrs: + client.update_router(obj, **attrs) + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_set(client, obj, parsed_args) class ShowRouter(command.ShowOne): @@ -675,6 +689,7 @@ class UnsetRouter(command.Command): metavar="", help=_("Router to modify (name or ID)") ) + _tag.add_tag_option_to_parser_for_unset(parser, _('router')) return parser def take_action(self, parsed_args): @@ -695,3 +710,5 @@ class UnsetRouter(command.Command): attrs['external_gateway_info'] = {} if attrs: client.update_router(obj, **attrs) + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_unset(client, obj, parsed_args) diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 2fdd11f07f..b96dff7f94 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -24,6 +24,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils +from openstackclient.network.v2 import _tag LOG = logging.getLogger(__name__) @@ -55,6 +56,7 @@ _formatters = { 'dns_nameservers': utils.format_list, 'host_routes': _format_host_routes, 'service_types': utils.format_list, + 'tags': utils.format_list, } @@ -336,12 +338,15 @@ class CreateSubnet(command.ShowOne): help=_("Set subnet description") ) _get_common_parse_arguments(parser) + _tag.add_tag_option_to_parser_for_create(parser, _('subnet')) return parser def take_action(self, parsed_args): client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) obj = client.create_subnet(**attrs) + # tags cannot be set when created, so tags need to be set later. + _tag.update_tags_for_set(client, obj, parsed_args) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -454,6 +459,7 @@ class ListSubnet(command.Lister): "(in CIDR notation) in output " "e.g.: --subnet-range 10.10.0.0/16") ) + _tag.add_tag_filtering_option_to_parser(parser, _('subnets')) return parser def take_action(self, parsed_args): @@ -488,6 +494,7 @@ class ListSubnet(command.Lister): filters['name'] = parsed_args.name if parsed_args.subnet_range: filters['cidr'] = parsed_args.subnet_range + _tag.get_tag_filtering_args(parsed_args, filters) data = network_client.subnets(**filters) headers = ('ID', 'Name', 'Network', 'Subnet') @@ -495,10 +502,10 @@ class ListSubnet(command.Lister): if parsed_args.long: headers += ('Project', 'DHCP', 'Name Servers', 'Allocation Pools', 'Host Routes', 'IP Version', - 'Gateway', 'Service Types') + 'Gateway', 'Service Types', 'Tags') columns += ('project_id', 'is_dhcp_enabled', 'dns_nameservers', 'allocation_pools', 'host_routes', 'ip_version', - 'gateway_ip', 'service_types') + 'gateway_ip', 'service_types', 'tags') return (headers, (utils.get_item_properties( @@ -549,6 +556,7 @@ class SetSubnet(command.Command): metavar='', help=_("Set subnet description") ) + _tag.add_tag_option_to_parser_for_set(parser, _('subnet')) _get_common_parse_arguments(parser, is_create=False) return parser @@ -574,7 +582,10 @@ class SetSubnet(command.Command): attrs['allocation_pools'] = [] if 'service_types' in attrs: attrs['service_types'] += obj.service_types - client.update_subnet(obj, **attrs) + if attrs: + client.update_subnet(obj, **attrs) + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_set(client, obj, parsed_args) return @@ -643,6 +654,7 @@ class UnsetSubnet(command.Command): 'Must be a valid device owner value for a network port ' '(repeat option to unset multiple service types)') ) + _tag.add_tag_option_to_parser_for_unset(parser, _('subnet')) parser.add_argument( 'subnet', metavar="", @@ -678,3 +690,6 @@ class UnsetSubnet(command.Command): attrs['service_types'] = tmp_obj.service_types if attrs: client.update_subnet(obj, **attrs) + + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_unset(client, obj, parsed_args) diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index b72a74fc19..a583986856 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -24,6 +24,7 @@ from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import sdk_utils +from openstackclient.network.v2 import _tag LOG = logging.getLogger(__name__) @@ -42,6 +43,7 @@ def _get_columns(item): _formatters = { 'prefixes': utils.format_list, + 'tags': utils.format_list, } @@ -191,6 +193,7 @@ class CreateSubnetPool(command.ShowOne): metavar='', help=_("Set default quota for subnet pool as the number of" "IP addresses allowed in a subnet")), + _tag.add_tag_option_to_parser_for_create(parser, _('subnet pool')) return parser def take_action(self, parsed_args): @@ -200,6 +203,8 @@ class CreateSubnetPool(command.ShowOne): if "prefixes" not in attrs: attrs['prefixes'] = [] obj = client.create_subnet_pool(**attrs) + # tags cannot be set when created, so tags need to be set later. + _tag.update_tags_for_set(client, obj, parsed_args) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) @@ -293,6 +298,7 @@ class ListSubnetPool(command.Lister): help=_("List only subnet pools of given address scope " "in output (name or ID)") ) + _tag.add_tag_filtering_option_to_parser(parser, _('subnet pools')) return parser def take_action(self, parsed_args): @@ -324,15 +330,16 @@ class ListSubnetPool(command.Lister): parsed_args.address_scope, ignore_missing=False) filters['address_scope_id'] = address_scope.id + _tag.get_tag_filtering_args(parsed_args, filters) data = network_client.subnet_pools(**filters) headers = ('ID', 'Name', 'Prefixes') columns = ('id', 'name', 'prefixes') if parsed_args.long: headers += ('Default Prefix Length', 'Address Scope', - 'Default Subnet Pool', 'Shared') + 'Default Subnet Pool', 'Shared', 'Tags') columns += ('default_prefix_length', 'address_scope_id', - 'is_default', 'is_shared') + 'is_default', 'is_shared', 'tags') return (headers, (utils.get_item_properties( @@ -384,6 +391,8 @@ class SetSubnetPool(command.Command): metavar='', help=_("Set default quota for subnet pool as the number of" "IP addresses allowed in a subnet")), + _tag.add_tag_option_to_parser_for_set(parser, _('subnet pool')) + return parser def take_action(self, parsed_args): @@ -397,7 +406,10 @@ class SetSubnetPool(command.Command): if 'prefixes' in attrs: attrs['prefixes'].extend(obj.prefixes) - client.update_subnet_pool(obj, **attrs) + if attrs: + client.update_subnet_pool(obj, **attrs) + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_set(client, obj, parsed_args) class ShowSubnetPool(command.ShowOne): @@ -441,6 +453,7 @@ class UnsetSubnetPool(command.Command): metavar="", help=_("Subnet pool to modify (name or ID)") ) + _tag.add_tag_option_to_parser_for_unset(parser, _('subnet pool')) return parser def take_action(self, parsed_args): @@ -461,3 +474,5 @@ class UnsetSubnetPool(command.Command): attrs['prefixes'] = tmp_prefixes if attrs: client.update_subnet_pool(obj, **attrs) + # tags is a subresource and it needs to be updated separately. + _tag.update_tags_for_unset(client, obj, parsed_args) diff --git a/openstackclient/tests/functional/network/v2/common.py b/openstackclient/tests/functional/network/v2/common.py index e3835abf59..a18bc48faf 100644 --- a/openstackclient/tests/functional/network/v2/common.py +++ b/openstackclient/tests/functional/network/v2/common.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import json +import uuid + from openstackclient.tests.functional import base @@ -20,3 +23,81 @@ class NetworkTests(base.TestCase): def setUpClass(cls): super(NetworkTests, cls).setUpClass() cls.haz_network = base.is_service_enabled('network') + + +class NetworkTagTests(NetworkTests): + """Functional tests with tag operation""" + + base_command = None + + def test_tag_operation(self): + # Get project IDs + cmd_output = json.loads(self.openstack('token issue -f json ')) + auth_project_id = cmd_output['project_id'] + + # Network create with no options + name1 = self._create_resource_and_tag_check('', []) + # Network create with tags + name2 = self._create_resource_and_tag_check('--tag red --tag blue', + ['red', 'blue']) + # Network create with no tag explicitly + name3 = self._create_resource_and_tag_check('--no-tag', []) + + self._set_resource_and_tag_check('set', name1, '--tag red --tag green', + ['red', 'green']) + + list_expected = ((name1, ['red', 'green']), + (name2, ['red', 'blue']), + (name3, [])) + self._list_tag_check(auth_project_id, list_expected) + + self._set_resource_and_tag_check('set', name1, '--tag blue', + ['red', 'green', 'blue']) + self._set_resource_and_tag_check( + 'set', name1, + '--no-tag --tag yellow --tag orange --tag purple', + ['yellow', 'orange', 'purple']) + self._set_resource_and_tag_check('unset', name1, '--tag yellow', + ['orange', 'purple']) + self._set_resource_and_tag_check('unset', name1, '--all-tag', []) + self._set_resource_and_tag_check('set', name2, '--no-tag', []) + + def _assertTagsEqual(self, expected, actual): + # TODO(amotoki): Should migrate to cliff format columns. + # At now, unit test assert method needs to be replaced + # to handle format columns, so format_list() is used. + # NOTE: The order of tag is undeterminestic. + actual_tags = filter(bool, actual.split(', ')) + self.assertEqual(set(expected), set(actual_tags)) + + def _list_tag_check(self, project_id, expected): + cmd_output = json.loads(self.openstack( + '{} list --long --project {} -f json'.format(self.base_command, + project_id))) + for name, tags in expected: + net = [n for n in cmd_output if n['Name'] == name][0] + self._assertTagsEqual(tags, net['Tags']) + + def _create_resource_for_tag_test(self, name, args): + return json.loads(self.openstack( + '{} create -f json {} {}'.format(self.base_command, args, name) + )) + + def _create_resource_and_tag_check(self, args, expected): + name = uuid.uuid4().hex + cmd_output = self._create_resource_for_tag_test(name, args) + self.addCleanup( + self.openstack, '{} delete {}'.format(self.base_command, name)) + self.assertIsNotNone(cmd_output["id"]) + self._assertTagsEqual(expected, cmd_output['tags']) + return name + + def _set_resource_and_tag_check(self, command, name, args, expected): + cmd_output = self.openstack( + '{} {} {} {}'.format(self.base_command, command, args, name) + ) + self.assertFalse(cmd_output) + cmd_output = json.loads(self.openstack( + '{} show -f json {}'.format(self.base_command, name) + )) + self._assertTagsEqual(expected, cmd_output['tags']) diff --git a/openstackclient/tests/functional/network/v2/test_network.py b/openstackclient/tests/functional/network/v2/test_network.py index 91939703d1..40fb382a9e 100644 --- a/openstackclient/tests/functional/network/v2/test_network.py +++ b/openstackclient/tests/functional/network/v2/test_network.py @@ -16,9 +16,11 @@ import uuid from openstackclient.tests.functional.network.v2 import common -class NetworkTests(common.NetworkTests): +class NetworkTests(common.NetworkTagTests): """Functional tests for network""" + base_command = 'network' + def setUp(self): super(NetworkTests, self).setUp() # Nothing in this class works with Nova Network diff --git a/openstackclient/tests/functional/network/v2/test_port.py b/openstackclient/tests/functional/network/v2/test_port.py index 09ac3566e4..a705979028 100644 --- a/openstackclient/tests/functional/network/v2/test_port.py +++ b/openstackclient/tests/functional/network/v2/test_port.py @@ -16,9 +16,14 @@ import uuid from openstackclient.tests.functional.network.v2 import common -class PortTests(common.NetworkTests): +class PortTests(common.NetworkTagTests): """Functional tests for port""" + base_command = 'port' + + NAME = uuid.uuid4().hex + NETWORK_NAME = uuid.uuid4().hex + @classmethod def setUpClass(cls): common.NetworkTests.setUpClass() @@ -250,3 +255,9 @@ class PortTests(common.NetworkTests): sg_id2, json_output.get('security_group_ids'), ) + + def _create_resource_for_tag_test(self, name, args): + return json.loads(self.openstack( + '{} create -f json --network {} {} {}' + .format(self.base_command, self.NETWORK_NAME, args, name) + )) diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py index 2e5cb5ef55..95c5a96f8b 100644 --- a/openstackclient/tests/functional/network/v2/test_router.py +++ b/openstackclient/tests/functional/network/v2/test_router.py @@ -16,9 +16,11 @@ import uuid from openstackclient.tests.functional.network.v2 import common -class RouterTests(common.NetworkTests): +class RouterTests(common.NetworkTagTests): """Functional tests for router""" + base_command = 'router' + def setUp(self): super(RouterTests, self).setUp() # Nothing in this class works with Nova Network diff --git a/openstackclient/tests/functional/network/v2/test_subnet.py b/openstackclient/tests/functional/network/v2/test_subnet.py index 040b645b1b..d5309ee674 100644 --- a/openstackclient/tests/functional/network/v2/test_subnet.py +++ b/openstackclient/tests/functional/network/v2/test_subnet.py @@ -17,9 +17,11 @@ import uuid from openstackclient.tests.functional.network.v2 import common -class SubnetTests(common.NetworkTests): +class SubnetTests(common.NetworkTagTests): """Functional tests for subnet""" + base_command = 'subnet' + @classmethod def setUpClass(cls): common.NetworkTests.setUpClass() @@ -285,3 +287,9 @@ class SubnetTests(common.NetworkTests): # break and no longer retry if create successfully break return cmd_output + + def _create_resource_for_tag_test(self, name, args): + cmd = ('subnet create -f json --network ' + + self.NETWORK_NAME + ' ' + args + + ' --subnet-range') + return self._subnet_create(cmd, name) diff --git a/openstackclient/tests/functional/network/v2/test_subnet_pool.py b/openstackclient/tests/functional/network/v2/test_subnet_pool.py index a4b823f100..46aa6f1433 100644 --- a/openstackclient/tests/functional/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/functional/network/v2/test_subnet_pool.py @@ -17,9 +17,11 @@ import uuid from openstackclient.tests.functional.network.v2 import common -class SubnetPoolTests(common.NetworkTests): +class SubnetPoolTests(common.NetworkTagTests): """Functional tests for subnet pool""" + base_command = 'subnet pool' + def setUp(self): super(SubnetPoolTests, self).setUp() # Nothing in this class works with Nova Network @@ -321,3 +323,7 @@ class SubnetPoolTests(common.NetworkTests): break return cmd_output, pool_prefix + + def _create_resource_for_tag_test(self, name, args): + cmd_output, _pool_prefix = self._subnet_pool_create(args, name) + return cmd_output diff --git a/openstackclient/tests/unit/network/v2/_test_tag.py b/openstackclient/tests/unit/network/v2/_test_tag.py new file mode 100644 index 0000000000..bd46153782 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/_test_tag.py @@ -0,0 +1,190 @@ +# 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 openstackclient.tests.unit import utils as tests_utils + + +class TestCreateTagMixin(object): + """Test case mixin to test network tag operation for resource creation. + + * Each test class must create a mock for self.network.set_tags + * If you test tag operation combined with other options, + you need to write test(s) directly in individual test cases. + * The following instance attributes must be defined: + + * _tag_test_resource: Test resource returned by mocked create_. + * _tag_create_resource_mock: Mocked create_ method of SDK. + * _tag_create_required_arglist: List of required arguments when creating + a resource with default options. + * _tag_create_required_verifylist: List of expected parsed_args params + when creating a resource with default options. + * _tag_create_required_attrs: Expected attributes passed to a mocked + create_resource method when creating a resource with default options. + """ + + def _test_create_with_tag(self, add_tags=True): + arglist = self._tag_create_required_arglist[:] + if add_tags: + arglist += ['--tag', 'red', '--tag', 'blue'] + else: + arglist += ['--no-tag'] + verifylist = self._tag_create_required_verifylist[:] + if add_tags: + verifylist.append(('tags', ['red', 'blue'])) + else: + verifylist.append(('no_tag', True)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = (self.cmd.take_action(parsed_args)) + + self._tag_create_resource_mock.assert_called_once_with( + **self._tag_create_required_attrs) + if add_tags: + self.network.set_tags.assert_called_once_with( + self._tag_test_resource, + tests_utils.CompareBySet(['red', 'blue'])) + else: + self.assertFalse(self.network.set_tags.called) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_with_tags(self): + self._test_create_with_tag(add_tags=True) + + def test_create_with_no_tag(self): + self._test_create_with_tag(add_tags=False) + + +class TestListTagMixin(object): + """Test case mixin to test network tag operation for resource listing. + + * A test resource returned by find_ must contains + "red" and "green" tags. + * Each test class must create a mock for self.network.set_tags + * If you test tag operation combined with other options, + you need to write test(s) directly in individual test cases. + * The following instance attributes must be defined: + + * _tag_create_resource_mock: Mocked list_ method of SDK. + """ + + def test_list_with_tag_options(self): + arglist = [ + '--tags', 'red,blue', + '--any-tags', 'red,green', + '--not-tags', 'orange,yellow', + '--not-any-tags', 'black,white', + ] + verifylist = [ + ('tags', ['red', 'blue']), + ('any_tags', ['red', 'green']), + ('not_tags', ['orange', 'yellow']), + ('not_any_tags', ['black', 'white']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self._tag_list_resource_mock.assert_called_once_with( + **{'tags': 'red,blue', + 'any_tags': 'red,green', + 'not_tags': 'orange,yellow', + 'not_any_tags': 'black,white'} + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestSetTagMixin(object): + """Test case mixin to test network tag operation for resource update. + + * A test resource returned by find_ must contains + "red" and "green" tags. + * Each test class must create a mock for self.network.set_tags + * If you test tag operation combined with other options, + you need to write test(s) directly in individual test cases. + * The following instance attributes must be defined: + + * _tag_resource_name: positional arg name of a resource to be updated. + * _tag_test_resource: Test resource returned by mocked update_. + * _tag_update_resource_mock: Mocked update_ method of SDK. + """ + + def _test_set_tags(self, with_tags=True): + if with_tags: + arglist = ['--tag', 'red', '--tag', 'blue'] + verifylist = [('tags', ['red', 'blue'])] + expected_args = ['red', 'blue', 'green'] + else: + arglist = ['--no-tag'] + verifylist = [('no_tag', True)] + expected_args = [] + arglist.append(self._tag_test_resource.name) + verifylist.append( + (self._tag_resource_name, self._tag_test_resource.name)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.assertFalse(self._tag_update_resource_mock.called) + self.network.set_tags.assert_called_once_with( + self._tag_test_resource, + tests_utils.CompareBySet(expected_args)) + self.assertIsNone(result) + + def test_set_with_tags(self): + self._test_set_tags(with_tags=True) + + def test_set_with_no_tag(self): + self._test_set_tags(with_tags=False) + + +class TestUnsetTagMixin(object): + """Test case mixin to test network tag operation for resource update. + + * Each test class must create a mock for self.network.set_tags + * If you test tag operation combined with other options, + you need to write test(s) directly in individual test cases. + * The following instance attributes must be defined: + + * _tag_resource_name: positional arg name of a resource to be updated. + * _tag_test_resource: Test resource returned by mocked update_. + * _tag_update_resource_mock: Mocked update_ method of SDK. + """ + + def _test_unset_tags(self, with_tags=True): + if with_tags: + arglist = ['--tag', 'red', '--tag', 'blue'] + verifylist = [('tags', ['red', 'blue'])] + expected_args = ['green'] + else: + arglist = ['--all-tag'] + verifylist = [('all_tag', True)] + expected_args = [] + arglist.append(self._tag_test_resource.name) + verifylist.append( + (self._tag_resource_name, self._tag_test_resource.name)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.assertFalse(self._tag_update_resource_mock.called) + self.network.set_tags.assert_called_once_with( + self._tag_test_resource, + tests_utils.CompareBySet(expected_args)) + self.assertIsNone(result) + + def test_unset_with_tags(self): + self._test_unset_tags(with_tags=True) + + def test_unset_with_all_tag(self): + self._test_unset_tags(with_tags=False) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index 98bda1649f..eadab58461 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -350,6 +350,7 @@ class FakeNetwork(object): 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, 'ipv4_address_scope': 'ipv4' + uuid.uuid4().hex, 'ipv6_address_scope': 'ipv6' + uuid.uuid4().hex, + 'tags': [], } # Overwrite default attributes. @@ -576,6 +577,7 @@ class FakePort(object): 'status': 'ACTIVE', 'tenant_id': 'project-id-' + uuid.uuid4().hex, 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, + 'tags': [], } # Overwrite default attributes. @@ -1053,6 +1055,7 @@ class FakeRouter(object): 'external_gateway_info': {}, 'availability_zone_hints': [], 'availability_zones': [], + 'tags': [], } # Overwrite default attributes. @@ -1294,6 +1297,7 @@ class FakeSubnet(object): 'service_types': [], 'subnetpool_id': None, 'description': 'subnet-description-' + uuid.uuid4().hex, + 'tags': [], } # Overwrite default attributes. @@ -1544,6 +1548,7 @@ class FakeSubnetPool(object): 'default_quota': None, 'ip_version': '4', 'description': 'subnet-pool-description-' + uuid.uuid4().hex, + 'tags': [], } # Overwrite default attributes. diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index bc8c402487..e620cd9c1d 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -22,6 +22,7 @@ from openstackclient.network.v2 import network from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes_v2 from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import _test_tag from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -41,7 +42,7 @@ class TestNetwork(network_fakes.TestNetworkV2): self.domains_mock = self.app.client_manager.identity.domains -class TestCreateNetworkIdentityV3(TestNetwork): +class TestCreateNetworkIdentityV3(TestNetwork, _test_tag.TestCreateTagMixin): project = identity_fakes_v3.FakeProject.create_one_project() domain = identity_fakes_v3.FakeDomain.create_one_domain() @@ -105,6 +106,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): super(TestCreateNetworkIdentityV3, self).setUp() self.network.create_network = mock.Mock(return_value=self._network) + self.network.set_tags = mock.Mock(return_value=None) # Get the command object to test self.cmd = network.CreateNetwork(self.app, self.namespace) @@ -113,6 +115,22 @@ class TestCreateNetworkIdentityV3(TestNetwork): self.domains_mock.get.return_value = self.domain self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy) + # TestCreateTagMixin + self._tag_test_resource = self._network + self._tag_create_resource_mock = self.network.create_network + self._tag_create_required_arglist = [self._network.name] + self._tag_create_required_verifylist = [ + ('name', self._network.name), + ('enable', True), + ('share', None), + ('project', None), + ('external', False), + ] + self._tag_create_required_attrs = { + 'admin_state_up': True, + 'name': self._network.name, + } + def test_create_no_options(self): arglist = [] verifylist = [] @@ -139,6 +157,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): 'admin_state_up': True, 'name': self._network.name, }) + self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -287,6 +306,7 @@ class TestCreateNetworkIdentityV2(TestNetwork): super(TestCreateNetworkIdentityV2, self).setUp() self.network.create_network = mock.Mock(return_value=self._network) + self.network.set_tags = mock.Mock(return_value=None) # Get the command object to test self.cmd = network.CreateNetwork(self.app, self.namespace) @@ -328,6 +348,7 @@ class TestCreateNetworkIdentityV2(TestNetwork): 'tenant_id': self.project.id, 'project_id': self.project.id, }) + self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -440,7 +461,7 @@ class TestDeleteNetwork(TestNetwork): self.network.delete_network.assert_has_calls(calls) -class TestListNetwork(TestNetwork): +class TestListNetwork(TestNetwork, _test_tag.TestListTagMixin): # The networks going to be listed up. _network = network_fakes.FakeNetwork.create_networks(count=3) @@ -461,6 +482,7 @@ class TestListNetwork(TestNetwork): 'Network Type', 'Router Type', 'Availability Zones', + 'Tags', ) data = [] @@ -484,6 +506,7 @@ class TestListNetwork(TestNetwork): net.provider_network_type, network._format_router_external(net.is_router_external), utils.format_list(net.availability_zones), + utils.format_list(net.tags), )) def setUp(self): @@ -501,6 +524,9 @@ class TestListNetwork(TestNetwork): self.network.dhcp_agent_hosting_networks = mock.Mock( return_value=self._network) + # TestListTagMixin + self._tag_list_resource_mock = self.network.networks + def test_network_list_no_options(self): arglist = [] verifylist = [ @@ -795,10 +821,11 @@ class TestListNetwork(TestNetwork): self.assertEqual(list(data), list(self.data)) -class TestSetNetwork(TestNetwork): +class TestSetNetwork(TestNetwork, _test_tag.TestSetTagMixin): # The network to set. - _network = network_fakes.FakeNetwork.create_one_network() + _network = network_fakes.FakeNetwork.create_one_network( + {'tags': ['green', 'red']}) qos_policy = (network_fakes.FakeNetworkQosPolicy. create_one_qos_policy(attrs={'id': _network.qos_policy_id})) @@ -806,6 +833,7 @@ class TestSetNetwork(TestNetwork): super(TestSetNetwork, self).setUp() self.network.update_network = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) self.network.find_network = mock.Mock(return_value=self._network) self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy) @@ -813,6 +841,11 @@ class TestSetNetwork(TestNetwork): # Get the command object to test self.cmd = network.SetNetwork(self.app, self.namespace) + # TestSetTagMixin + self._tag_resource_name = 'network' + self._tag_test_resource = self._network + self._tag_update_resource_mock = self.network.update_network + def test_set_this(self): arglist = [ self._network.name, @@ -902,9 +935,8 @@ class TestSetNetwork(TestNetwork): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - attrs = {} - self.network.update_network.assert_called_once_with( - self._network, **attrs) + self.assertFalse(self.network.update_network.called) + self.assertFalse(self.network.set_tags.called) self.assertIsNone(result) @@ -990,3 +1022,40 @@ class TestShowNetwork(TestNetwork): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + + +class TestUnsetNetwork(TestNetwork, _test_tag.TestUnsetTagMixin): + + # The network to set. + _network = network_fakes.FakeNetwork.create_one_network( + {'tags': ['green', 'red']}) + qos_policy = (network_fakes.FakeNetworkQosPolicy. + create_one_qos_policy(attrs={'id': _network.qos_policy_id})) + + def setUp(self): + super(TestUnsetNetwork, self).setUp() + + self.network.update_network = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) + + self.network.find_network = mock.Mock(return_value=self._network) + self.network.find_qos_policy = mock.Mock(return_value=self.qos_policy) + + # Get the command object to test + self.cmd = network.UnsetNetwork(self.app, self.namespace) + + # TestUnsetNetwork + self._tag_resource_name = 'network' + self._tag_test_resource = self._network + self._tag_update_resource_mock = self.network.update_network + + def test_unset_nothing(self): + arglist = [self._network.name, ] + verifylist = [('network', self._network.name), ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.assertFalse(self.network.update_network.called) + self.assertFalse(self.network.set_tags.called) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index a8a6dba9be..deda6b4128 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -21,6 +21,7 @@ from osc_lib import utils from openstackclient.network.v2 import port from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.tests.unit.network.v2 import _test_tag from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -35,7 +36,8 @@ class TestPort(network_fakes.TestNetworkV2): # Get a shortcut to the ProjectManager Mock self.projects_mock = self.app.client_manager.identity.projects - def _get_common_cols_data(self, fake_port): + @staticmethod + def _get_common_cols_data(fake_port): columns = ( 'admin_state_up', 'allowed_address_pairs', @@ -61,6 +63,7 @@ class TestPort(network_fakes.TestNetworkV2): 'qos_policy_id', 'security_group_ids', 'status', + 'tags', ) data = ( @@ -88,19 +91,22 @@ class TestPort(network_fakes.TestNetworkV2): fake_port.qos_policy_id, utils.format_list(fake_port.security_group_ids), fake_port.status, + utils.format_list(fake_port.tags), ) return columns, data -class TestCreatePort(TestPort): +class TestCreatePort(TestPort, _test_tag.TestCreateTagMixin): _port = network_fakes.FakePort.create_one_port() + columns, data = TestPort._get_common_cols_data(_port) def setUp(self): super(TestCreatePort, self).setUp() self.network.create_port = mock.Mock(return_value=self._port) + self.network.set_tags = mock.Mock(return_value=None) fake_net = network_fakes.FakeNetwork.create_one_network({ 'id': self._port.network_id, }) @@ -110,6 +116,24 @@ class TestCreatePort(TestPort): # Get the command object to test self.cmd = port.CreatePort(self.app, self.namespace) + # TestUnsetTagMixin + self._tag_test_resource = self._port + self._tag_create_resource_mock = self.network.create_port + self._tag_create_required_arglist = [ + '--network', self._port.network_id, + 'test-port', + ] + self._tag_create_required_verifylist = [ + ('network', self._port.network_id,), + ('enable', True), + ('name', 'test-port'), + ] + self._tag_create_required_attrs = { + 'admin_state_up': True, + 'network_id': self._port.network_id, + 'name': 'test-port', + } + def test_create_default_options(self): arglist = [ '--network', self._port.network_id, @@ -129,10 +153,10 @@ class TestCreatePort(TestPort): 'network_id': self._port.network_id, 'name': 'test-port', }) + self.assertFalse(self.network.set_tags.called) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_full_options(self): arglist = [ @@ -166,7 +190,6 @@ class TestCreatePort(TestPort): ('network', self._port.network_id), ('dns_name', '8.8.8.8'), ('name', 'test-port'), - ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -187,9 +210,8 @@ class TestCreatePort(TestPort): 'name': 'test-port', }) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_invalid_json_binding_profile(self): arglist = [ @@ -239,9 +261,8 @@ class TestCreatePort(TestPort): 'name': 'test-port', }) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_with_security_group(self): secgroup = network_fakes.FakeSecurityGroup.create_one_security_group() @@ -269,9 +290,8 @@ class TestCreatePort(TestPort): 'name': 'test-port', }) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_port_with_dns_name(self): arglist = [ @@ -296,9 +316,8 @@ class TestCreatePort(TestPort): 'name': 'test-port', }) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_with_security_groups(self): sg_1 = network_fakes.FakeSecurityGroup.create_one_security_group() @@ -327,9 +346,8 @@ class TestCreatePort(TestPort): 'name': 'test-port', }) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_with_no_security_groups(self): arglist = [ @@ -354,9 +372,8 @@ class TestCreatePort(TestPort): 'name': 'test-port', }) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_port_with_allowed_address_pair_ipaddr(self): pairs = [{'ip_address': '192.168.1.123'}, @@ -385,9 +402,8 @@ class TestCreatePort(TestPort): 'name': 'test-port', }) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_port_with_allowed_address_pair(self): pairs = [{'ip_address': '192.168.1.123', @@ -422,9 +438,8 @@ class TestCreatePort(TestPort): 'name': 'test-port', }) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_port_with_qos(self): qos_policy = network_fakes.FakeNetworkQosPolicy.create_one_qos_policy() @@ -451,9 +466,8 @@ class TestCreatePort(TestPort): 'name': 'test-port', }) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_create_port_security_enabled(self): arglist = [ @@ -583,7 +597,7 @@ class TestDeletePort(TestPort): ) -class TestListPort(TestPort): +class TestListPort(TestPort, _test_tag.TestListTagMixin): _ports = network_fakes.FakePort.create_ports(count=3) @@ -603,6 +617,7 @@ class TestListPort(TestPort): 'Status', 'Security Groups', 'Device Owner', + 'Tags', ) data = [] @@ -625,6 +640,7 @@ class TestListPort(TestPort): prt.status, utils.format_list(prt.security_group_ids), prt.device_owner, + utils.format_list(prt.tags), )) def setUp(self): @@ -642,6 +658,8 @@ class TestListPort(TestPort): self.network.find_router = mock.Mock(return_value=fake_router) self.network.find_network = mock.Mock(return_value=fake_network) self.app.client_manager.compute = mock.Mock() + # TestUnsetTagMixin + self._tag_list_resource_mock = self.network.ports def test_port_list_no_options(self): arglist = [] @@ -902,9 +920,9 @@ class TestListPort(TestPort): self.assertEqual(self.data, list(data)) -class TestSetPort(TestPort): +class TestSetPort(TestPort, _test_tag.TestSetTagMixin): - _port = network_fakes.FakePort.create_one_port() + _port = network_fakes.FakePort.create_one_port({'tags': ['green', 'red']}) def setUp(self): super(TestSetPort, self).setUp() @@ -912,9 +930,14 @@ class TestSetPort(TestPort): self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) self.network.find_port = mock.Mock(return_value=self._port) self.network.update_port = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) # Get the command object to test self.cmd = port.SetPort(self.app, self.namespace) + # TestSetTagMixin + self._tag_resource_name = 'port' + self._tag_test_resource = self._port + self._tag_update_resource_mock = self.network.update_port def test_set_port_defaults(self): arglist = [ @@ -926,8 +949,8 @@ class TestSetPort(TestPort): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - attrs = {} - self.network.update_port.assert_called_once_with(self._port, **attrs) + self.assertFalse(self.network.update_port.called) + self.assertFalse(self.network.set_tags.called) self.assertIsNone(result) def test_set_port_fixed_ip(self): @@ -1412,6 +1435,7 @@ class TestShowPort(TestPort): # The port to show. _port = network_fakes.FakePort.create_one_port() + columns, data = TestPort._get_common_cols_data(_port) def setUp(self): super(TestShowPort, self).setUp() @@ -1442,12 +1466,11 @@ class TestShowPort(TestPort): self.network.find_port.assert_called_once_with( self._port.name, ignore_missing=False) - ref_columns, ref_data = self._get_common_cols_data(self._port) - self.assertEqual(ref_columns, columns) - self.assertEqual(ref_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) -class TestUnsetPort(TestPort): +class TestUnsetPort(TestPort, _test_tag.TestUnsetTagMixin): def setUp(self): super(TestUnsetPort, self).setUp() @@ -1456,14 +1479,20 @@ class TestUnsetPort(TestPort): 'ip_address': '0.0.0.1'}, {'subnet_id': '042eb10a-3a18-4658-ab-cf47c8d03152', 'ip_address': '1.0.0.0'}], - 'binding:profile': {'batman': 'Joker', 'Superman': 'LexLuthor'}}) + 'binding:profile': {'batman': 'Joker', 'Superman': 'LexLuthor'}, + 'tags': ['green', 'red'], }) self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet( {'id': '042eb10a-3a18-4658-ab-cf47c8d03152'}) self.network.find_subnet = mock.Mock(return_value=self.fake_subnet) self.network.find_port = mock.Mock(return_value=self._testport) self.network.update_port = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) # Get the command object to test self.cmd = port.UnsetPort(self.app, self.namespace) + # TestUnsetTagMixin + self._tag_resource_name = 'port' + self._tag_test_resource = self._testport + self._tag_update_resource_mock = self.network.update_port def test_unset_port_parameters(self): arglist = [ diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index c153fe4a08..d65c9aa9f7 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -19,6 +19,7 @@ from osc_lib import utils as osc_utils from openstackclient.network.v2 import router from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import _test_tag from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -111,7 +112,7 @@ class TestAddSubnetToRouter(TestRouter): self.assertIsNone(result) -class TestCreateRouter(TestRouter): +class TestCreateRouter(TestRouter, _test_tag.TestCreateTagMixin): # The new router created. new_router = network_fakes.FakeRouter.create_one_router() @@ -129,6 +130,7 @@ class TestCreateRouter(TestRouter): 'project_id', 'routes', 'status', + 'tags', ) data = ( router._format_admin_state(new_router.admin_state_up), @@ -143,22 +145,42 @@ class TestCreateRouter(TestRouter): new_router.tenant_id, router._format_routes(new_router.routes), new_router.status, + osc_utils.format_list(new_router.tags), ) def setUp(self): super(TestCreateRouter, self).setUp() self.network.create_router = mock.Mock(return_value=self.new_router) + self.network.set_tags = mock.Mock(return_value=None) # Get the command object to test self.cmd = router.CreateRouter(self.app, self.namespace) + # TestCreateTagMixin + self._tag_test_resource = self.new_router + self._tag_create_resource_mock = self.network.create_router + self._tag_create_required_arglist = [ + self.new_router.name, + ] + self._tag_create_required_verifylist = [ + ('name', self.new_router.name), + ('enable', True), + ('distributed', False), + ('ha', False), + ] + self._tag_create_required_attrs = { + 'admin_state_up': True, + 'name': self.new_router.name, + } + def test_create_no_options(self): arglist = [] verifylist = [] self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) + self.assertFalse(self.network.set_tags.called) def test_create_default_options(self): arglist = [ @@ -178,6 +200,7 @@ class TestCreateRouter(TestRouter): 'admin_state_up': True, 'name': self.new_router.name, }) + self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -345,7 +368,7 @@ class TestDeleteRouter(TestRouter): ) -class TestListRouter(TestRouter): +class TestListRouter(TestRouter, _test_tag.TestListTagMixin): # The routers going to be listed up. routers = network_fakes.FakeRouter.create_routers(count=3) @@ -363,11 +386,13 @@ class TestListRouter(TestRouter): columns_long = columns + ( 'Routes', 'External gateway info', - 'Availability zones' + 'Availability zones', + 'Tags', ) columns_long_no_az = columns + ( 'Routes', 'External gateway info', + 'Tags', ) data = [] @@ -404,6 +429,7 @@ class TestListRouter(TestRouter): router._format_routes(r.routes), router._format_external_gateway_info(r.external_gateway_info), osc_utils.format_list(r.availability_zones), + osc_utils.format_list(r.tags), ) ) data_long_no_az = [] @@ -413,6 +439,7 @@ class TestListRouter(TestRouter): data[i] + ( router._format_routes(r.routes), router._format_external_gateway_info(r.external_gateway_info), + osc_utils.format_list(r.tags), ) ) @@ -432,6 +459,9 @@ class TestListRouter(TestRouter): self.network.get_agent = mock.Mock(return_value=self._testagent) self.network.get_router = mock.Mock(return_value=self.routers[0]) + # TestListTagMixin + self._tag_list_resource_mock = self.network.routers + def test_router_list_no_options(self): arglist = [] verifylist = [ @@ -684,26 +714,33 @@ class TestRemoveSubnetFromRouter(TestRouter): self.assertIsNone(result) -class TestSetRouter(TestRouter): +class TestSetRouter(TestRouter, _test_tag.TestSetTagMixin): # The router to set. _default_route = {'destination': '10.20.20.0/24', 'nexthop': '10.20.30.1'} _network = network_fakes.FakeNetwork.create_one_network() _subnet = network_fakes.FakeSubnet.create_one_subnet() _router = network_fakes.FakeRouter.create_one_router( - attrs={'routes': [_default_route]} + attrs={'routes': [_default_route], + 'tags': ['green', 'red']} ) def setUp(self): super(TestSetRouter, self).setUp() self.network.router_add_gateway = mock.Mock() self.network.update_router = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) self.network.find_router = mock.Mock(return_value=self._router) self.network.find_network = mock.Mock(return_value=self._network) self.network.find_subnet = mock.Mock(return_value=self._subnet) # Get the command object to test self.cmd = router.SetRouter(self.app, self.namespace) + # TestSetTagMixin + self._tag_resource_name = 'router' + self._tag_test_resource = self._router + self._tag_update_resource_mock = self.network.update_router + def test_set_this(self): arglist = [ self._router.name, @@ -902,9 +939,8 @@ class TestSetRouter(TestRouter): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - attrs = {} - self.network.update_router.assert_called_once_with( - self._router, **attrs) + self.assertFalse(self.network.update_router.called) + self.assertFalse(self.network.set_tags.called) self.assertIsNone(result) def test_wrong_gateway_params(self): @@ -1030,6 +1066,7 @@ class TestShowRouter(TestRouter): 'project_id', 'routes', 'status', + 'tags', ) data = ( router._format_admin_state(_router.admin_state_up), @@ -1044,6 +1081,7 @@ class TestShowRouter(TestRouter): _router.tenant_id, router._format_routes(_router.routes), _router.status, + osc_utils.format_list(_router.tags), ) def setUp(self): @@ -1086,12 +1124,18 @@ class TestUnsetRouter(TestRouter): {'routes': [{"destination": "192.168.101.1/24", "nexthop": "172.24.4.3"}, {"destination": "192.168.101.2/24", - "nexthop": "172.24.4.3"}], }) + "nexthop": "172.24.4.3"}], + 'tags': ['green', 'red'], }) self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() self.network.find_router = mock.Mock(return_value=self._testrouter) self.network.update_router = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) # Get the command object to test self.cmd = router.UnsetRouter(self.app, self.namespace) + # TestUnsetTagMixin + self._tag_resource_name = 'router' + self._tag_test_resource = self._testrouter + self._tag_update_resource_mock = self.network.update_router def test_unset_router_params(self): arglist = [ diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 47de5616df..509fbe6b12 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -19,6 +19,7 @@ from osc_lib import utils from openstackclient.network.v2 import subnet as subnet_v2 from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import _test_tag from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -36,7 +37,7 @@ class TestSubnet(network_fakes.TestNetworkV2): self.domains_mock = self.app.client_manager.identity.domains -class TestCreateSubnet(TestSubnet): +class TestCreateSubnet(TestSubnet, _test_tag.TestCreateTagMixin): project = identity_fakes_v3.FakeProject.create_one_project() domain = identity_fakes_v3.FakeDomain.create_one_domain() @@ -125,6 +126,7 @@ class TestCreateSubnet(TestSubnet): 'segment_id', 'service_types', 'subnetpool_id', + 'tags', ) data = ( @@ -145,6 +147,7 @@ class TestCreateSubnet(TestSubnet): _subnet.segment_id, utils.format_list(_subnet.service_types), _subnet.subnetpool_id, + utils.format_list(_subnet.tags), ) data_subnet_pool = ( @@ -165,6 +168,7 @@ class TestCreateSubnet(TestSubnet): _subnet_from_pool.segment_id, utils.format_list(_subnet_from_pool.service_types), _subnet_from_pool.subnetpool_id, + utils.format_list(_subnet.tags), ) data_ipv6 = ( @@ -185,6 +189,7 @@ class TestCreateSubnet(TestSubnet): _subnet_ipv6.segment_id, utils.format_list(_subnet_ipv6.service_types), _subnet_ipv6.subnetpool_id, + utils.format_list(_subnet.tags), ) def setUp(self): @@ -197,6 +202,8 @@ class TestCreateSubnet(TestSubnet): self.domains_mock.get.return_value = self.domain # Mock SDK calls for all tests. + self.network.create_subnet = mock.Mock(return_value=self._subnet) + self.network.set_tags = mock.Mock(return_value=None) self.network.find_network = mock.Mock(return_value=self._network) self.network.find_segment = mock.Mock( return_value=self._network_segment @@ -205,6 +212,28 @@ class TestCreateSubnet(TestSubnet): return_value=self._subnet_pool ) + # TestUnsetTagMixin + self._tag_test_resource = self._subnet + self._tag_create_resource_mock = self.network.create_subnet + self._tag_create_required_arglist = [ + "--subnet-range", self._subnet.cidr, + "--network", self._subnet.network_id, + self._subnet.name, + ] + self._tag_create_required_verifylist = [ + ('name', self._subnet.name), + ('subnet_range', self._subnet.cidr), + ('network', self._subnet.network_id), + ('ip_version', self._subnet.ip_version), + ('gateway', 'auto'), + ] + self._tag_create_required_attrs = { + 'cidr': self._subnet.cidr, + 'ip_version': self._subnet.ip_version, + 'name': self._subnet.name, + 'network_id': self._subnet.network_id, + } + def test_create_no_options(self): arglist = [] verifylist = [] @@ -213,10 +242,11 @@ class TestCreateSubnet(TestSubnet): # throw a "ParserExecption" self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) + self.assertFalse(self.network.create_subnet.called) + self.assertFalse(self.network.set_tags.called) def test_create_default_options(self): # Mock SDK calls for this test. - self.network.create_subnet = mock.Mock(return_value=self._subnet) self._network.id = self._subnet.network_id arglist = [ @@ -230,7 +260,6 @@ class TestCreateSubnet(TestSubnet): ('network', self._subnet.network_id), ('ip_version', self._subnet.ip_version), ('gateway', 'auto'), - ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -242,13 +271,14 @@ class TestCreateSubnet(TestSubnet): 'name': self._subnet.name, 'network_id': self._subnet.network_id, }) + self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) def test_create_from_subnet_pool_options(self): # Mock SDK calls for this test. - self.network.create_subnet = \ - mock.Mock(return_value=self._subnet_from_pool) + self.network.create_subnet.return_value = self._subnet_from_pool + self.network.set_tags = mock.Mock(return_value=None) self._network.id = self._subnet_from_pool.network_id arglist = [ @@ -309,7 +339,7 @@ class TestCreateSubnet(TestSubnet): def test_create_options_subnet_range_ipv6(self): # Mock SDK calls for this test. - self.network.create_subnet = mock.Mock(return_value=self._subnet_ipv6) + self.network.create_subnet.return_value = self._subnet_ipv6 self._network.id = self._subnet_ipv6.network_id arglist = [ @@ -376,12 +406,12 @@ class TestCreateSubnet(TestSubnet): 'allocation_pools': self._subnet_ipv6.allocation_pools, 'service_types': self._subnet_ipv6.service_types, }) + self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) self.assertEqual(self.data_ipv6, data) def test_create_with_network_segment(self): # Mock SDK calls for this test. - self.network.create_subnet = mock.Mock(return_value=self._subnet) self._network.id = self._subnet.network_id arglist = [ @@ -410,12 +440,12 @@ class TestCreateSubnet(TestSubnet): 'network_id': self._subnet.network_id, 'segment_id': self._network_segment.id, }) + self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) def test_create_with_description(self): # Mock SDK calls for this test. - self.network.create_subnet = mock.Mock(return_value=self._subnet) self._network.id = self._subnet.network_id arglist = [ @@ -444,6 +474,7 @@ class TestCreateSubnet(TestSubnet): 'network_id': self._subnet.network_id, 'description': self._subnet.description, }) + self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -527,7 +558,7 @@ class TestDeleteSubnet(TestSubnet): ) -class TestListSubnet(TestSubnet): +class TestListSubnet(TestSubnet, _test_tag.TestListTagMixin): # The subnets going to be listed up. _subnet = network_fakes.FakeSubnet.create_subnets(count=3) @@ -546,6 +577,7 @@ class TestListSubnet(TestSubnet): 'IP Version', 'Gateway', 'Service Types', + 'Tags', ) data = [] @@ -572,6 +604,7 @@ class TestListSubnet(TestSubnet): subnet.ip_version, subnet.gateway_ip, utils.format_list(subnet.service_types), + utils.format_list(subnet.tags), )) def setUp(self): @@ -582,6 +615,9 @@ class TestListSubnet(TestSubnet): self.network.subnets = mock.Mock(return_value=self._subnet) + # TestUnsetTagMixin + self._tag_list_resource_mock = self.network.subnets + def test_subnet_list_no_options(self): arglist = [] verifylist = [ @@ -802,15 +838,21 @@ class TestListSubnet(TestSubnet): self.assertEqual(self.data, list(data)) -class TestSetSubnet(TestSubnet): +class TestSetSubnet(TestSubnet, _test_tag.TestSetTagMixin): - _subnet = network_fakes.FakeSubnet.create_one_subnet() + _subnet = network_fakes.FakeSubnet.create_one_subnet( + {'tags': ['green', 'red']}) def setUp(self): super(TestSetSubnet, self).setUp() self.network.update_subnet = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) self.network.find_subnet = mock.Mock(return_value=self._subnet) self.cmd = subnet_v2.SetSubnet(self.app, self.namespace) + # TestSetTagMixin + self._tag_resource_name = 'subnet' + self._tag_test_resource = self._subnet + self._tag_update_resource_mock = self.network.update_subnet def test_set_this(self): arglist = [ @@ -867,8 +909,8 @@ class TestSetSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - attrs = {} - self.network.update_subnet.assert_called_with(self._subnet, **attrs) + self.assertFalse(self.network.update_subnet.called) + self.assertFalse(self.network.set_tags.called) self.assertIsNone(result) def test_append_options(self): @@ -982,6 +1024,7 @@ class TestShowSubnet(TestSubnet): 'segment_id', 'service_types', 'subnetpool_id', + 'tags', ) data = ( @@ -1002,6 +1045,7 @@ class TestShowSubnet(TestSubnet): _subnet.segment_id, utils.format_list(_subnet.service_types), _subnet.subnetpool_id, + utils.format_list(_subnet.tags), ) def setUp(self): @@ -1039,7 +1083,7 @@ class TestShowSubnet(TestSubnet): self.assertEqual(self.data, data) -class TestUnsetSubnet(TestSubnet): +class TestUnsetSubnet(TestSubnet, _test_tag.TestUnsetTagMixin): def setUp(self): super(TestUnsetSubnet, self).setUp() @@ -1055,11 +1099,17 @@ class TestUnsetSubnet(TestSubnet): {'start': '8.8.8.160', 'end': '8.8.8.170'}], 'service_types': ['network:router_gateway', - 'network:floatingip_agent_gateway'], }) + 'network:floatingip_agent_gateway'], + 'tags': ['green', 'red'], }) self.network.find_subnet = mock.Mock(return_value=self._testsubnet) self.network.update_subnet = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) # Get the command object to test self.cmd = subnet_v2.UnsetSubnet(self.app, self.namespace) + # TestUnsetTagMixin + self._tag_resource_name = 'subnet' + self._tag_test_resource = self._testsubnet + self._tag_update_resource_mock = self.network.update_subnet def test_unset_subnet_params(self): arglist = [ diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index 80a57bbb30..af49385608 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -20,6 +20,7 @@ from osc_lib import utils from openstackclient.network.v2 import subnet_pool from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 +from openstackclient.tests.unit.network.v2 import _test_tag from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -37,7 +38,7 @@ class TestSubnetPool(network_fakes.TestNetworkV2): self.domains_mock = self.app.client_manager.identity.domains -class TestCreateSubnetPool(TestSubnetPool): +class TestCreateSubnetPool(TestSubnetPool, _test_tag.TestCreateTagMixin): project = identity_fakes_v3.FakeProject.create_one_project() domain = identity_fakes_v3.FakeDomain.create_one_domain() @@ -60,6 +61,7 @@ class TestCreateSubnetPool(TestSubnetPool): 'prefixes', 'project_id', 'shared', + 'tags', ) data = ( _subnet_pool.address_scope_id, @@ -75,6 +77,7 @@ class TestCreateSubnetPool(TestSubnetPool): utils.format_list(_subnet_pool.prefixes), _subnet_pool.project_id, _subnet_pool.shared, + utils.format_list(_subnet_pool.tags), ) def setUp(self): @@ -82,6 +85,7 @@ class TestCreateSubnetPool(TestSubnetPool): self.network.create_subnet_pool = mock.Mock( return_value=self._subnet_pool) + self.network.set_tags = mock.Mock(return_value=None) # Get the command object to test self.cmd = subnet_pool.CreateSubnetPool(self.app, self.namespace) @@ -92,12 +96,29 @@ class TestCreateSubnetPool(TestSubnetPool): self.projects_mock.get.return_value = self.project self.domains_mock.get.return_value = self.domain + # TestUnsetTagMixin + self._tag_test_resource = self._subnet_pool + self._tag_create_resource_mock = self.network.create_subnet_pool + self._tag_create_required_arglist = [ + '--pool-prefix', '10.0.10.0/24', + self._subnet_pool.name, + ] + self._tag_create_required_verifylist = [ + ('prefixes', ['10.0.10.0/24']), + ('name', self._subnet_pool.name), + ] + self._tag_create_required_attrs = { + 'prefixes': ['10.0.10.0/24'], + 'name': self._subnet_pool.name, + } + def test_create_no_options(self): arglist = [] verifylist = [] self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, verifylist) + self.assertFalse(self.network.set_tags.called) def test_create_no_pool_prefix(self): """Make sure --pool-prefix is a required argument""" @@ -127,6 +148,7 @@ class TestCreateSubnetPool(TestSubnetPool): 'prefixes': ['10.0.10.0/24'], 'name': self._subnet_pool.name, }) + self.assertFalse(self.network.set_tags.called) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -374,7 +396,7 @@ class TestDeleteSubnetPool(TestSubnetPool): ) -class TestListSubnetPool(TestSubnetPool): +class TestListSubnetPool(TestSubnetPool, _test_tag.TestListTagMixin): # The subnet pools going to be listed up. _subnet_pools = network_fakes.FakeSubnetPool.create_subnet_pools(count=3) @@ -388,6 +410,7 @@ class TestListSubnetPool(TestSubnetPool): 'Address Scope', 'Default Subnet Pool', 'Shared', + 'Tags', ) data = [] @@ -408,6 +431,7 @@ class TestListSubnetPool(TestSubnetPool): pool.address_scope_id, pool.is_default, pool.shared, + utils.format_list(pool.tags), )) def setUp(self): @@ -418,6 +442,9 @@ class TestListSubnetPool(TestSubnetPool): self.network.subnet_pools = mock.Mock(return_value=self._subnet_pools) + # TestUnsetTagMixin + self._tag_list_resource_mock = self.network.subnet_pools + def test_subnet_pool_list_no_option(self): arglist = [] verifylist = [ @@ -585,11 +612,12 @@ class TestListSubnetPool(TestSubnetPool): self.assertEqual(self.data, list(data)) -class TestSetSubnetPool(TestSubnetPool): +class TestSetSubnetPool(TestSubnetPool, _test_tag.TestSetTagMixin): # The subnet_pool to set. _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool( - {'default_quota': 10}, + {'default_quota': 10, + 'tags': ['green', 'red']} ) _address_scope = network_fakes.FakeAddressScope.create_one_address_scope() @@ -598,6 +626,7 @@ class TestSetSubnetPool(TestSubnetPool): super(TestSetSubnetPool, self).setUp() self.network.update_subnet_pool = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) self.network.find_subnet_pool = mock.Mock( return_value=self._subnet_pool) @@ -608,6 +637,11 @@ class TestSetSubnetPool(TestSubnetPool): # Get the command object to test self.cmd = subnet_pool.SetSubnetPool(self.app, self.namespace) + # TestUnsetTagMixin + self._tag_resource_name = 'subnet_pool' + self._tag_test_resource = self._subnet_pool + self._tag_update_resource_mock = self.network.update_subnet_pool + def test_set_this(self): arglist = [ '--name', 'noob', @@ -667,9 +701,8 @@ class TestSetSubnetPool(TestSubnetPool): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - attrs = {} - self.network.update_subnet_pool.assert_called_once_with( - self._subnet_pool, **attrs) + self.assertFalse(self.network.update_subnet_pool.called) + self.assertFalse(self.network.set_tags.called) self.assertIsNone(result) def test_set_len_negative(self): @@ -854,6 +887,7 @@ class TestShowSubnetPool(TestSubnetPool): 'prefixes', 'project_id', 'shared', + 'tags', ) data = ( @@ -870,6 +904,7 @@ class TestShowSubnetPool(TestSubnetPool): utils.format_list(_subnet_pool.prefixes), _subnet_pool.tenant_id, _subnet_pool.shared, + utils.format_list(_subnet_pool.tags), ) def setUp(self): @@ -908,26 +943,36 @@ class TestShowSubnetPool(TestSubnetPool): self.assertEqual(self.data, data) -class TestUnsetSubnetPool(TestSubnetPool): +class TestUnsetSubnetPool(TestSubnetPool, _test_tag.TestUnsetTagMixin): def setUp(self): super(TestUnsetSubnetPool, self).setUp() self._subnetpool = network_fakes.FakeSubnetPool.create_one_subnet_pool( {'prefixes': ['10.0.10.0/24', '10.1.10.0/24', - '10.2.10.0/24'], }) + '10.2.10.0/24'], + 'tags': ['green', 'red']}) self.network.find_subnet_pool = mock.Mock( return_value=self._subnetpool) self.network.update_subnet_pool = mock.Mock(return_value=None) + self.network.set_tags = mock.Mock(return_value=None) # Get the command object to test self.cmd = subnet_pool.UnsetSubnetPool(self.app, self.namespace) + # TestUnsetTagMixin + self._tag_resource_name = 'subnet_pool' + self._tag_test_resource = self._subnetpool + self._tag_update_resource_mock = self.network.update_subnet_pool + def test_unset_subnet_pool(self): arglist = [ '--pool-prefix', '10.0.10.0/24', '--pool-prefix', '10.1.10.0/24', self._subnetpool.name, ] - verifylist = [('prefixes', ['10.0.10.0/24', '10.1.10.0/24'])] + verifylist = [ + ('prefixes', ['10.0.10.0/24', '10.1.10.0/24']), + ('subnet_pool', self._subnetpool.name), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) attrs = {'prefixes': ['10.2.10.0/24']} @@ -940,7 +985,10 @@ class TestUnsetSubnetPool(TestSubnetPool): '--pool-prefix', '10.100.1.1/25', self._subnetpool.name, ] - verifylist = [('prefixes', ['10.100.1.1/25'])] + verifylist = [ + ('prefixes', ['10.100.1.1/25']), + ('subnet_pool', self._subnetpool.name), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises(exceptions.CommandError, self.cmd.take_action, diff --git a/openstackclient/tests/unit/utils.py b/openstackclient/tests/unit/utils.py index 3c5c8683f1..8f9cc7b17c 100644 --- a/openstackclient/tests/unit/utils.py +++ b/openstackclient/tests/unit/utils.py @@ -25,6 +25,12 @@ class ParserException(Exception): pass +class CompareBySet(list): + """Class to compare value using set.""" + def __eq__(self, other): + return set(self) == set(other) + + class TestCase(testtools.TestCase): def setUp(self): diff --git a/releasenotes/notes/bp-neutron-client-tag-ff24d13e5c70e052.yaml b/releasenotes/notes/bp-neutron-client-tag-ff24d13e5c70e052.yaml new file mode 100644 index 0000000000..4addd07b7c --- /dev/null +++ b/releasenotes/notes/bp-neutron-client-tag-ff24d13e5c70e052.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Added support for ``tags`` to the following resources: + ``network``, ``subnet``, ``port``, ``router`` and ``subnet pool``. + [Blueprint :oscbp:`neutron-client-tag`] + + - Add ``--tag`` and ``--no-tag`` options to corresponding "create" commands. + - Add ``--tag`` and ``--no-tag`` options to corresponding "set" commands. + - Add ``--tag`` and ``--all-tag`` options to corresponding "unset" commands. + (``network unset`` command is introduced to support the tag operation) + - Add ``--tags``, ``--any-tags``, ``--not-tags`` and ``--not-any-tags`` + options to corresponding "list" commands. diff --git a/setup.cfg b/setup.cfg index 16917e5099..ec91988f10 100644 --- a/setup.cfg +++ b/setup.cfg @@ -395,6 +395,7 @@ openstack.network.v2 = network_list = openstackclient.network.v2.network:ListNetwork network_set = openstackclient.network.v2.network:SetNetwork network_show = openstackclient.network.v2.network:ShowNetwork + network_unset = openstackclient.network.v2.network:UnsetNetwork network_meter_create = openstackclient.network.v2.network_meter:CreateMeter network_meter_delete = openstackclient.network.v2.network_meter:DeleteMeter