diff options
Diffstat (limited to 'openstackclient')
20 files changed, 1018 insertions, 284 deletions
diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 1481ed65..82f09cec 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -41,8 +41,18 @@ def make_client(instance): version = _compute_api_version else: version = instance._api_version[API_NAME] + from novaclient import api_versions + # convert to APIVersion object + version = api_versions.get_api_version(version) + + if version.is_latest(): + import novaclient + # NOTE(RuiChen): executing version discovery make sense, but that need + # an initialized REST client, it's not available now, + # fallback to use the max version of novaclient side. + version = novaclient.API_MAX_VERSION - LOG.debug('Instantiating compute client for V%s', version) + LOG.debug('Instantiating compute client for %s', version) # Set client http_log_debug to True if verbosity level is high enough http_log_debug = utils.get_effective_log_level() <= logging.DEBUG @@ -91,30 +101,27 @@ def check_api_version(check_version): """ # Defer client imports until we actually need them - try: - from novaclient import api_versions - except ImportError: - # Retain previous behaviour - return False - import novaclient + from novaclient import api_versions global _compute_api_version - # Copy some logic from novaclient 2.27.0 for basic version detection + # Copy some logic from novaclient 3.3.0 for basic version detection # NOTE(dtroyer): This is only enough to resume operations using API # version 2.0 or any valid version supplied by the user. _compute_api_version = api_versions.get_api_version(check_version) - if _compute_api_version > api_versions.APIVersion("2.0"): - if not _compute_api_version.matches( - novaclient.API_MIN_VERSION, - novaclient.API_MAX_VERSION, - ): - raise exceptions.CommandError( - "versions supported by client: %s - %s" % ( - novaclient.API_MIN_VERSION.get_string(), - novaclient.API_MAX_VERSION.get_string(), - ), - ) + # Bypass X.latest format microversion + if not _compute_api_version.is_latest(): + if _compute_api_version > api_versions.APIVersion("2.0"): + if not _compute_api_version.matches( + novaclient.API_MIN_VERSION, + novaclient.API_MAX_VERSION, + ): + raise exceptions.CommandError( + "versions supported by client: %s - %s" % ( + novaclient.API_MIN_VERSION.get_string(), + novaclient.API_MAX_VERSION.get_string(), + ), + ) return True diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py index 1e2c4cce..a3047d84 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -15,6 +15,7 @@ import abc import six from openstackclient.common import command +from openstackclient.common import exceptions @six.add_metaclass(abc.ABCMeta) @@ -69,6 +70,42 @@ class NetworkAndComputeCommand(command.Command): @six.add_metaclass(abc.ABCMeta) +class NetworkAndComputeDelete(NetworkAndComputeCommand): + """Network and Compute Delete + + Delete class for commands that support implementation via + the network or compute endpoint. Such commands have different + implementations for take_action() and may even have different + arguments. This class supports bulk deletion, and error handling + following the rules in doc/source/command-errors.rst. + """ + + def take_action(self, parsed_args): + ret = 0 + resources = getattr(parsed_args, self.resource, []) + + for r in resources: + self.r = r + try: + if self.app.client_manager.is_network_endpoint_enabled(): + self.take_action_network(self.app.client_manager.network, + parsed_args) + else: + self.take_action_compute(self.app.client_manager.compute, + parsed_args) + except Exception as e: + self.app.log.error("Failed to delete %s with name or ID " + "'%s': %s" % (self.resource, r, e)) + ret += 1 + + if ret: + total = len(resources) + msg = "%s of %s %ss failed to delete." % (ret, total, + self.resource) + raise exceptions.CommandError(msg) + + +@six.add_metaclass(abc.ABCMeta) class NetworkAndComputeLister(command.Lister): """Network and Compute Lister diff --git a/openstackclient/network/v2/address_scope.py b/openstackclient/network/v2/address_scope.py index fac0849f..614900c9 100644 --- a/openstackclient/network/v2/address_scope.py +++ b/openstackclient/network/v2/address_scope.py @@ -185,7 +185,7 @@ class SetAddressScope(command.Command): if parsed_args.no_share: attrs['shared'] = False if attrs == {}: - msg = "Nothing specified to be set." + msg = _("Nothing specified to be set.") raise exceptions.CommandError(msg) client.update_address_scope(obj, **attrs) diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 4b77971a..bf01e2ec 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -32,7 +32,7 @@ def _format_router_external(item): _formatters = { 'subnets': utils.format_list, 'admin_state_up': _format_admin_state, - 'router_external': _format_router_external, + 'router:external': _format_router_external, 'availability_zones': utils.format_list, 'availability_zone_hints': utils.format_list, } @@ -43,9 +43,6 @@ def _get_columns(item): if 'tenant_id' in columns: columns.remove('tenant_id') columns.append('project_id') - if 'router:external' in columns: - columns.remove('router:external') - columns.append('router_external') return tuple(sorted(columns)) @@ -93,11 +90,17 @@ def _get_attrs(client_manager, parsed_args): attrs['provider:physical_network'] = parsed_args.physical_network if parsed_args.segmentation_id: attrs['provider:segmentation_id'] = parsed_args.segmentation_id + # Update VLAN Transparency for networks + if parsed_args.transparent_vlan: + attrs['vlan_transparent'] = True + if parsed_args.no_transparent_vlan: + attrs['vlan_transparent'] = False return attrs -def _add_provider_network_options(parser): - # Add provider network options +def _add_additional_network_options(parser): + # Add additional network options + parser.add_argument( '--provider-network-type', metavar='<provider-network-type>', @@ -119,6 +122,16 @@ def _add_provider_network_options(parser): help=_("VLAN ID for VLAN networks or Tunnel ID for GRE/VXLAN " "networks")) + vlan_transparent_grp = parser.add_mutually_exclusive_group() + vlan_transparent_grp.add_argument( + '--transparent-vlan', + action='store_true', + help=_("Make the network VLAN transparent")) + vlan_transparent_grp.add_argument( + '--no-transparent-vlan', + action='store_true', + help=_("Do not make the network VLAN transparent")) + def _get_attrs_compute(client_manager, parsed_args): attrs = {} @@ -206,10 +219,10 @@ class CreateNetwork(common.NetworkAndComputeShowOne): default_router_grp.add_argument( '--no-default', action='store_true', - help=_("Do not use the network as the default external network. " + help=_("Do not use the network as the default external network " "(default)") ) - _add_provider_network_options(parser) + _add_additional_network_options(parser) return parser def update_parser_compute(self, parser): @@ -235,30 +248,30 @@ class CreateNetwork(common.NetworkAndComputeShowOne): return (columns, data) -class DeleteNetwork(common.NetworkAndComputeCommand): +class DeleteNetwork(common.NetworkAndComputeDelete): """Delete network(s)""" + # Used by base class to find resources in parsed_args. + resource = 'network' + r = None + def update_parser_common(self, parser): parser.add_argument( 'network', metavar="<network>", nargs="+", - help=("Network(s) to delete (name or ID)") + help=_("Network(s) to delete (name or ID)") ) + return parser def take_action_network(self, client, parsed_args): - for network in parsed_args.network: - obj = client.find_network(network) - client.delete_network(obj) + obj = client.find_network(self.r, ignore_missing=False) + client.delete_network(obj) def take_action_compute(self, client, parsed_args): - for network in parsed_args.network: - network = utils.find_resource( - client.networks, - network, - ) - client.networks.delete(network.id) + network = utils.find_resource(client.networks, self.r) + client.networks.delete(network.id) class ListNetwork(common.NetworkAndComputeLister): @@ -269,13 +282,13 @@ class ListNetwork(common.NetworkAndComputeLister): '--external', action='store_true', default=False, - help='List external networks', + help=_("List external networks") ) parser.add_argument( '--long', action='store_true', default=False, - help='List additional fields in output', + help=_("List additional fields in output") ) return parser @@ -290,7 +303,7 @@ class ListNetwork(common.NetworkAndComputeLister): 'shared', 'subnets', 'provider_network_type', - 'router_external', + 'router:external', 'availability_zones', ) column_headers = ( @@ -413,7 +426,7 @@ class SetNetwork(command.Command): action='store_true', help=_("Do not use the network as the default external network") ) - _add_provider_network_options(parser) + _add_additional_network_options(parser) return parser def take_action(self, parsed_args): @@ -422,7 +435,7 @@ class SetNetwork(command.Command): attrs = _get_attrs(self.app.client_manager, parsed_args) if attrs == {}: - msg = "Nothing specified to be set" + msg = _("Nothing specified to be set") raise exceptions.CommandError(msg) client.update_network(obj, **attrs) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 3cc76a6e..ca02281f 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -414,7 +414,7 @@ class SetPort(command.Command): attrs['fixed_ips'] = [] if attrs == {}: - msg = "Nothing specified to be set" + msg = _("Nothing specified to be set") raise exceptions.CommandError(msg) client.update_port(obj, **attrs) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index a32ab5ea..a2f0df1d 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -13,7 +13,9 @@ """Router action implementations""" +import argparse import json +import logging from openstackclient.common import command from openstackclient.common import exceptions @@ -23,6 +25,9 @@ from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +LOG = logging.getLogger(__name__) + + def _format_admin_state(state): return 'UP' if state else 'DOWN' @@ -379,10 +384,15 @@ class SetRouter(command.Command): "(repeat option to set multiple routes)") ) routes_group.add_argument( - '--clear-routes', + '--no-route', action='store_true', help=_("Clear routes associated with the router") ) + routes_group.add_argument( + '--clear-routes', + action='store_true', + help=argparse.SUPPRESS, + ) # TODO(tangchen): Support setting 'ha' property in 'router set' # command. It appears that changing the ha state is supported by @@ -401,8 +411,14 @@ class SetRouter(command.Command): attrs = _get_attrs(self.app.client_manager, parsed_args) # Get the route attributes. - if parsed_args.clear_routes: + if parsed_args.no_route: + attrs['routes'] = [] + elif parsed_args.clear_routes: attrs['routes'] = [] + LOG.warning(_( + 'The --clear-routes option is deprecated, ' + 'please use --no-route instead.' + )) elif parsed_args.routes is not None: # Map the route keys and append to the current routes. # The REST API will handle route validation and duplicates. @@ -411,7 +427,7 @@ class SetRouter(command.Command): attrs['routes'] = obj.routes + parsed_args.routes if attrs == {}: - msg = "Nothing specified to be set" + msg = _("Nothing specified to be set") raise exceptions.CommandError(msg) client.update_router(obj, **attrs) diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index 5b22a0dd..5abe9b9d 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -36,9 +36,21 @@ def _format_security_group_rule_show(obj): def _format_network_port_range(rule): + # Display port range or ICMP type and code. For example: + # - ICMP type: 'type=3' + # - ICMP type and code: 'type=3:code=0' + # - ICMP code: Not supported + # - Matching port range: '443:443' + # - Different port range: '22:24' + # - Single port: '80:80' + # - No port range: '' port_range = '' - if (rule.protocol != 'icmp' and - (rule.port_range_min or rule.port_range_max)): + if _is_icmp_protocol(rule.protocol): + if rule.port_range_min: + port_range += 'type=' + str(rule.port_range_min) + if rule.port_range_max: + port_range += ':code=' + str(rule.port_range_max) + elif rule.port_range_min or rule.port_range_max: port_range_min = str(rule.port_range_min) port_range_max = str(rule.port_range_max) if rule.port_range_min is None: @@ -61,6 +73,17 @@ def _convert_to_lowercase(string): return string.lower() +def _is_icmp_protocol(protocol): + # NOTE(rtheis): Neutron has deprecated protocol icmpv6. + # However, while the OSC CLI doesn't document the protocol, + # the code must still handle it. In addition, handle both + # protocol names and numbers. + if protocol in ['icmp', 'icmpv6', 'ipv6-icmp', '1', '58']: + return True + else: + return False + + class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): """Create a new security group rule""" @@ -68,19 +91,7 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): parser.add_argument( 'group', metavar='<group>', - help='Create rule in this security group (name or ID)', - ) - # TODO(rtheis): Add support for additional protocols for network. - # Until then, continue enforcing the compute choices. When additional - # protocols are added, the default ethertype must be determined - # based on the protocol. - parser.add_argument( - "--proto", - metavar="<proto>", - default="tcp", - choices=['icmp', 'tcp', 'udp'], - type=_convert_to_lowercase, - help=_("IP protocol (icmp, tcp, udp; default: tcp)") + help=_("Create rule in this security group (name or ID)") ) source_group = parser.add_mutually_exclusive_group() source_group.add_argument( @@ -94,17 +105,49 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): metavar="<group>", help=_("Source security group (name or ID)") ) - parser.add_argument( - "--dst-port", - metavar="<port-range>", - default=(0, 0), - action=parseractions.RangeAction, - help=_("Destination port, may be a single port or port range: " - "137:139 (only required for IP protocols tcp and udp)") - ) return parser def update_parser_network(self, parser): + parser.add_argument( + '--dst-port', + metavar='<port-range>', + action=parseractions.RangeAction, + help=_("Destination port, may be a single port or a starting and " + "ending port range: 137:139. Required for IP protocols TCP " + "and UDP. Ignored for ICMP IP protocols.") + ) + parser.add_argument( + '--icmp-type', + metavar='<icmp-type>', + type=int, + help=_("ICMP type for ICMP IP protocols") + ) + parser.add_argument( + '--icmp-code', + metavar='<icmp-code>', + type=int, + help=_("ICMP code for ICMP IP protocols") + ) + # NOTE(rtheis): Support either protocol option name for now. + # However, consider deprecating and then removing --proto in + # a future release. + protocol_group = parser.add_mutually_exclusive_group() + protocol_group.add_argument( + '--protocol', + metavar='<protocol>', + type=_convert_to_lowercase, + help=_("IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, " + "ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, " + "ipv6-opts, ipv6-route, ospf, pgm, rsvp, sctp, tcp, " + "udp, udplite, vrrp and integer representations [0-255]; " + "default: tcp)") + ) + protocol_group.add_argument( + '--proto', + metavar='<proto>', + type=_convert_to_lowercase, + help=argparse.SUPPRESS + ) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( '--ingress', @@ -120,7 +163,8 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): '--ethertype', metavar='<ethertype>', choices=['IPv4', 'IPv6'], - help=_("Ethertype of network traffic (IPv4, IPv6; default: IPv4)") + help=_("Ethertype of network traffic " + "(IPv4, IPv6; default: based on IP protocol)") ) parser.add_argument( '--project', @@ -130,6 +174,55 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): identity_common.add_project_domain_option_to_parser(parser) return parser + def update_parser_compute(self, parser): + parser.add_argument( + '--dst-port', + metavar='<port-range>', + default=(0, 0), + action=parseractions.RangeAction, + help=_("Destination port, may be a single port or a starting and " + "ending port range: 137:139. Required for IP protocols TCP " + "and UDP. Ignored for ICMP IP protocols.") + ) + # NOTE(rtheis): Support either protocol option name for now. + # However, consider deprecating and then removing --proto in + # a future release. + protocol_group = parser.add_mutually_exclusive_group() + protocol_group.add_argument( + '--protocol', + metavar='<protocol>', + choices=['icmp', 'tcp', 'udp'], + type=_convert_to_lowercase, + help=_("IP protocol (icmp, tcp, udp; default: tcp)") + ) + protocol_group.add_argument( + '--proto', + metavar='<proto>', + choices=['icmp', 'tcp', 'udp'], + type=_convert_to_lowercase, + help=argparse.SUPPRESS + ) + return parser + + def _get_protocol(self, parsed_args): + protocol = 'tcp' + if parsed_args.protocol is not None: + protocol = parsed_args.protocol + if parsed_args.proto is not None: + protocol = parsed_args.proto + return protocol + + def _is_ipv6_protocol(self, protocol): + # NOTE(rtheis): Neutron has deprecated protocol icmpv6. + # However, while the OSC CLI doesn't document the protocol, + # the code must still handle it. In addition, handle both + # protocol names and numbers. + if (protocol.startswith('ipv6-') or + protocol in ['icmpv6', '41', '43', '44', '58', '59', '60']): + return True + else: + return False + def take_action_network(self, client, parsed_args): # Get the security group ID to hold the rule. security_group_id = client.find_security_group( @@ -139,24 +232,50 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): # Build the create attributes. attrs = {} + attrs['protocol'] = self._get_protocol(parsed_args) + # NOTE(rtheis): A direction must be specified and ingress # is the default. if parsed_args.ingress or not parsed_args.egress: attrs['direction'] = 'ingress' if parsed_args.egress: attrs['direction'] = 'egress' + + # NOTE(rtheis): Use ethertype specified else default based + # on IP protocol. if parsed_args.ethertype: attrs['ethertype'] = parsed_args.ethertype + elif self._is_ipv6_protocol(attrs['protocol']): + attrs['ethertype'] = 'IPv6' else: - # NOTE(rtheis): Default based on protocol is IPv4 for now. - # Once IPv6 protocols are added, this will need to be updated. attrs['ethertype'] = 'IPv4' - # TODO(rtheis): Add port range support (type and code) for icmp - # protocol. Until then, continue ignoring the port range. - if parsed_args.proto != 'icmp': + + # NOTE(rtheis): Validate the port range and ICMP type and code. + # It would be ideal if argparse could do this. + if parsed_args.dst_port and (parsed_args.icmp_type or + parsed_args.icmp_code): + msg = _('Argument --dst-port not allowed with arguments ' + '--icmp-type and --icmp-code') + raise exceptions.CommandError(msg) + if parsed_args.icmp_type is None and parsed_args.icmp_code is not None: + msg = _('Argument --icmp-type required with argument --icmp-code') + raise exceptions.CommandError(msg) + is_icmp_protocol = _is_icmp_protocol(attrs['protocol']) + if not is_icmp_protocol and (parsed_args.icmp_type or + parsed_args.icmp_code): + msg = _('ICMP IP protocol required with arguments ' + '--icmp-type and --icmp-code') + raise exceptions.CommandError(msg) + # NOTE(rtheis): For backwards compatibility, continue ignoring + # the destination port range when an ICMP IP protocol is specified. + if parsed_args.dst_port and not is_icmp_protocol: attrs['port_range_min'] = parsed_args.dst_port[0] attrs['port_range_max'] = parsed_args.dst_port[1] - attrs['protocol'] = parsed_args.proto + if parsed_args.icmp_type: + attrs['port_range_min'] = parsed_args.icmp_type + if parsed_args.icmp_code: + attrs['port_range_max'] = parsed_args.icmp_code + if parsed_args.src_group is not None: attrs['remote_group_id'] = client.find_security_group( parsed_args.src_group, @@ -187,7 +306,8 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): client.security_groups, parsed_args.group, ) - if parsed_args.proto == 'icmp': + protocol = self._get_protocol(parsed_args) + if protocol == 'icmp': from_port, to_port = -1, -1 else: from_port, to_port = parsed_args.dst_port @@ -203,7 +323,7 @@ class CreateSecurityGroupRule(common.NetworkAndComputeShowOne): src_ip = '0.0.0.0/0' obj = client.security_group_rules.create( group.id, - parsed_args.proto, + protocol, from_port, to_port, src_ip, @@ -399,8 +519,8 @@ class ShowSecurityGroupRule(common.NetworkAndComputeShowOne): break if obj is None: - msg = "Could not find security group rule " \ - "with ID %s" % parsed_args.rule + msg = _("Could not find security group rule with ID ") + \ + parsed_args.rule raise exceptions.CommandError(msg) # NOTE(rtheis): Format security group rule diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index fb441cbf..e7e1be99 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -141,9 +141,9 @@ def _get_attrs(client_manager, parsed_args, is_create=True): gateway = parsed_args.gateway.lower() if not is_create and gateway == 'auto': - raise exceptions.CommandError("Auto option is not available" - " for Subnet Set. Valid options are" - " <ip-address> or none") + msg = _("Auto option is not available for Subnet Set. " + "Valid options are <ip-address> or none") + raise exceptions.CommandError(msg) elif gateway != 'auto': if gateway == 'none': attrs['gateway_ip'] = None @@ -296,10 +296,22 @@ class ListSubnet(command.Lister): default=False, help=_("List additional fields in output") ) + parser.add_argument( + '--ip-version', + type=int, + choices=[4, 6], + metavar='<ip-version>', + dest='ip_version', + help=_("List only subnets of given IP version in output" + "Allowed values for IP version are 4 and 6."), + ) return parser def take_action(self, parsed_args): - data = self.app.client_manager.network.subnets() + filters = {} + if parsed_args.ip_version: + filters['ip_version'] = parsed_args.ip_version + data = self.app.client_manager.network.subnets(**filters) headers = ('ID', 'Name', 'Network', 'Subnet') columns = ('id', 'name', 'network_id', 'cidr') diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index f1174dda..a1a94426 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -287,7 +287,7 @@ class SetSubnetPool(command.Command): attrs = _get_attrs(self.app.client_manager, parsed_args) if attrs == {}: - msg = "Nothing specified to be set" + msg = _("Nothing specified to be set") raise exceptions.CommandError(msg) # Existing prefixes must be a subset of the new prefixes. diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index b3f3fb49..70fc386f 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -859,6 +859,25 @@ class FakeNetwork(object): return networks + @staticmethod + def get_networks(networks=None, count=2): + """Get an iterable MagicMock object with a list of faked networks. + + If networks list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List networks: + A list of FakeResource objects faking networks + :param int count: + The number of networks to fake + :return: + An iterable Mock object with side_effect set to a list of faked + networks + """ + if networks is None: + networks = FakeNetwork.create_networks(count=count) + return mock.Mock(side_effect=networks) + class FakeHost(object): """Fake one host.""" diff --git a/openstackclient/tests/image/v2/test_image.py b/openstackclient/tests/image/v2/test_image.py index 0248f30b..33f25331 100644 --- a/openstackclient/tests/image/v2/test_image.py +++ b/openstackclient/tests/image/v2/test_image.py @@ -20,6 +20,7 @@ import warlock from glanceclient.v2 import schemas from openstackclient.common import exceptions +from openstackclient.common import utils as common_utils from openstackclient.image.v2 import image from openstackclient.tests import fakes from openstackclient.tests.identity.v3 import fakes as identity_fakes @@ -341,28 +342,31 @@ class TestImageCreate(TestImage): class TestAddProjectToImage(TestImage): + _image = image_fakes.FakeImage.create_one_image() + columns = ( 'image_id', 'member_id', 'status', ) + datalist = ( - image_fakes.image_id, + _image.id, identity_fakes.project_id, - image_fakes.member_status, + image_fakes.member_status ) def setUp(self): super(TestAddProjectToImage, self).setUp() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) + self.images_mock.get.return_value = self._image + + # Update the image_id in the MEMBER dict + self.new_member = copy.deepcopy(image_fakes.MEMBER) + self.new_member['image_id'] = self._image.id self.image_members_mock.create.return_value = fakes.FakeModel( - copy.deepcopy(image_fakes.MEMBER), + self.new_member, ) self.project_mock.get.return_value = fakes.FakeResource( None, @@ -379,11 +383,11 @@ class TestAddProjectToImage(TestImage): def test_add_project_to_image_no_option(self): arglist = [ - image_fakes.image_id, + self._image.id, identity_fakes.project_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('image', self._image.id), ('project', identity_fakes.project_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -393,20 +397,21 @@ class TestAddProjectToImage(TestImage): # data to be shown. columns, data = self.cmd.take_action(parsed_args) self.image_members_mock.create.assert_called_with( - image_fakes.image_id, + self._image.id, identity_fakes.project_id ) + self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) def test_add_project_to_image_with_option(self): arglist = [ - image_fakes.image_id, + self._image.id, identity_fakes.project_id, '--project-domain', identity_fakes.domain_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('image', self._image.id), ('project', identity_fakes.project_id), ('project_domain', identity_fakes.domain_id), ] @@ -417,7 +422,7 @@ class TestAddProjectToImage(TestImage): # data to be shown. columns, data = self.cmd.take_action(parsed_args) self.image_members_mock.create.assert_called_with( - image_fakes.image_id, + self._image.id, identity_fakes.project_id ) self.assertEqual(self.columns, columns) @@ -468,25 +473,26 @@ class TestImageDelete(TestImage): class TestImageList(TestImage): + _image = image_fakes.FakeImage.create_one_image() + columns = ( 'ID', 'Name', 'Status', ) + datalist = ( - ( - image_fakes.image_id, - image_fakes.image_name, - '', - ), - ) + _image.id, + _image.name, + '', + ), def setUp(self): super(TestImageList, self).setUp() self.api_mock = mock.Mock() self.api_mock.image_list.side_effect = [ - [copy.deepcopy(image_fakes.IMAGE)], [], + [image_fakes.FakeImage.get_image_info(self._image)], [], ] self.app.client_manager.image.api = self.api_mock @@ -611,23 +617,24 @@ class TestImageList(TestImage): self.assertEqual(collist, columns) datalist = (( - image_fakes.image_id, - image_fakes.image_name, + self._image.id, + self._image.name, '', '', '', '', - 'public', - False, - image_fakes.image_owner, - '', + self._image.visibility, + self._image.protected, + self._image.owner, + common_utils.format_list(self._image.tags), ), ) self.assertEqual(datalist, tuple(data)) @mock.patch('openstackclient.api.utils.simple_filter') def test_image_list_property_option(self, sf_mock): sf_mock.return_value = [ - copy.deepcopy(image_fakes.IMAGE), + copy.deepcopy( + image_fakes.FakeImage.get_image_info(self._image)), ] arglist = [ @@ -644,7 +651,7 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with() sf_mock.assert_called_with( - [image_fakes.IMAGE], + [image_fakes.FakeImage.get_image_info(self._image)], attr='a', value='1', property_field='properties', @@ -656,7 +663,8 @@ class TestImageList(TestImage): @mock.patch('openstackclient.common.utils.sort_items') def test_image_list_sort_option(self, si_mock): si_mock.return_value = [ - copy.deepcopy(image_fakes.IMAGE) + copy.deepcopy( + image_fakes.FakeImage.get_image_info(self._image)) ] arglist = ['--sort', 'name:asc'] @@ -669,10 +677,9 @@ class TestImageList(TestImage): columns, data = self.cmd.take_action(parsed_args) self.api_mock.image_list.assert_called_with() si_mock.assert_called_with( - [image_fakes.IMAGE], + [image_fakes.FakeImage.get_image_info(self._image)], 'name:asc' ) - self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) @@ -720,12 +727,10 @@ class TestRemoveProjectImage(TestImage): def setUp(self): super(TestRemoveProjectImage, self).setUp() + self._image = image_fakes.FakeImage.create_one_image() # This is the return value for utils.find_resource() - self.images_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(image_fakes.IMAGE), - loaded=True, - ) + self.images_mock.get.return_value = self._image + self.project_mock.get.return_value = fakes.FakeResource( None, copy.deepcopy(identity_fakes.PROJECT), @@ -742,11 +747,11 @@ class TestRemoveProjectImage(TestImage): def test_remove_project_image_no_options(self): arglist = [ - image_fakes.image_id, + self._image.id, identity_fakes.project_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('image', self._image.id), ('project', identity_fakes.project_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -754,19 +759,19 @@ class TestRemoveProjectImage(TestImage): result = self.cmd.take_action(parsed_args) self.image_members_mock.delete.assert_called_with( - image_fakes.image_id, + self._image.id, identity_fakes.project_id, ) self.assertIsNone(result) def test_remove_project_image_with_options(self): arglist = [ - image_fakes.image_id, + self._image.id, identity_fakes.project_id, '--project-domain', identity_fakes.domain_id, ] verifylist = [ - ('image', image_fakes.image_id), + ('image', self._image.id), ('project', identity_fakes.project_id), ('project_domain', identity_fakes.domain_id), ] @@ -775,7 +780,7 @@ class TestRemoveProjectImage(TestImage): result = self.cmd.take_action(parsed_args) self.image_members_mock.delete.assert_called_with( - image_fakes.image_id, + self._image.id, identity_fakes.project_id, ) self.assertIsNone(result) diff --git a/openstackclient/tests/network/v2/fakes.py b/openstackclient/tests/network/v2/fakes.py index 7c4604bd..84ede381 100644 --- a/openstackclient/tests/network/v2/fakes.py +++ b/openstackclient/tests/network/v2/fakes.py @@ -184,8 +184,7 @@ class FakeNetwork(object): :param Dictionary attrs: A dictionary with all attributes :return: - A FakeResource object, with id, name, admin_state_up, - router_external, status, subnets, tenant_id + A FakeResource object, with id, name, etc. """ attrs = attrs or {} @@ -199,7 +198,7 @@ class FakeNetwork(object): 'shared': False, 'subnets': ['a', 'b'], 'provider_network_type': 'vlan', - 'router_external': True, + 'router:external': True, 'availability_zones': [], 'availability_zone_hints': [], 'is_default': False, @@ -213,6 +212,7 @@ class FakeNetwork(object): # Set attributes with special mapping in OpenStack SDK. network.project_id = network_attrs['tenant_id'] + network.is_router_external = network_attrs['router:external'] return network @@ -496,8 +496,8 @@ class FakeSecurityGroupRule(object): 'direction': 'ingress', 'ethertype': 'IPv4', 'id': 'security-group-rule-id-' + uuid.uuid4().hex, - 'port_range_max': 0, - 'port_range_min': 0, + 'port_range_max': None, + 'port_range_min': None, 'protocol': 'tcp', 'remote_group_id': None, 'remote_ip_prefix': '0.0.0.0/0', diff --git a/openstackclient/tests/network/v2/test_network.py b/openstackclient/tests/network/v2/test_network.py index 1269b0a1..ba810f16 100644 --- a/openstackclient/tests/network/v2/test_network.py +++ b/openstackclient/tests/network/v2/test_network.py @@ -14,6 +14,7 @@ import copy import mock +from mock import call from openstackclient.common import exceptions from openstackclient.common import utils from openstackclient.network.v2 import network @@ -55,7 +56,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): 'name', 'project_id', 'provider_network_type', - 'router_external', + 'router:external', 'shared', 'status', 'subnets', @@ -70,7 +71,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): _network.name, _network.project_id, _network.provider_network_type, - network._format_router_external(_network.router_external), + network._format_router_external(_network.is_router_external), _network.shared, _network.status, utils.format_list(_network.subnets), @@ -148,6 +149,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): "--provider-network-type", "vlan", "--provider-physical-network", "physnet1", "--provider-segment", "400", + "--transparent-vlan", self._network.name, ] verifylist = [ @@ -161,6 +163,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): ('provider_network_type', 'vlan'), ('physical_network', 'physnet1'), ('segmentation_id', '400'), + ('transparent_vlan', True), ('name', self._network.name), ] @@ -178,6 +181,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): 'provider:network_type': 'vlan', 'provider:physical_network': 'physnet1', 'provider:segmentation_id': '400', + 'vlan_transparent': True, }) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -223,7 +227,7 @@ class TestCreateNetworkIdentityV2(TestNetwork): 'name', 'project_id', 'provider_network_type', - 'router_external', + 'router:external', 'shared', 'status', 'subnets', @@ -238,7 +242,7 @@ class TestCreateNetworkIdentityV2(TestNetwork): _network.name, _network.project_id, _network.provider_network_type, - network._format_router_external(_network.router_external), + network._format_router_external(_network.is_router_external), _network.shared, _network.status, utils.format_list(_network.subnets), @@ -320,33 +324,88 @@ class TestCreateNetworkIdentityV2(TestNetwork): class TestDeleteNetwork(TestNetwork): - # The network to delete. - _network = network_fakes.FakeNetwork.create_one_network() - def setUp(self): super(TestDeleteNetwork, self).setUp() + # The networks to delete + self._networks = network_fakes.FakeNetwork.create_networks(count=3) + self.network.delete_network = mock.Mock(return_value=None) - self.network.find_network = mock.Mock(return_value=self._network) + self.network.find_network = network_fakes.FakeNetwork.get_networks( + networks=self._networks) # Get the command object to test self.cmd = network.DeleteNetwork(self.app, self.namespace) - def test_delete(self): + def test_delete_one_network(self): arglist = [ - self._network.name, + self._networks[0].name, ] verifylist = [ - ('network', [self._network.name]), + ('network', [self._networks[0].name]), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network.delete_network.assert_called_once_with(self._networks[0]) + self.assertIsNone(result) + + def test_delete_multiple_networks(self): + arglist = [] + for n in self._networks: + arglist.append(n.id) + verifylist = [ + ('network', arglist), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) - self.network.delete_network.assert_called_once_with(self._network) + calls = [] + for n in self._networks: + calls.append(call(n)) + self.network.delete_network.assert_has_calls(calls) self.assertIsNone(result) + def test_delete_multiple_networks_exception(self): + arglist = [ + self._networks[0].id, + 'xxxx-yyyy-zzzz', + self._networks[1].id, + ] + verifylist = [ + ('network', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Fake exception in find_network() + ret_find = [ + self._networks[0], + exceptions.NotFound('404'), + self._networks[1], + ] + self.network.find_network = mock.Mock(side_effect=ret_find) + + # Fake exception in delete_network() + ret_delete = [ + None, + exceptions.NotFound('404'), + ] + self.network.delete_network = mock.Mock(side_effect=ret_delete) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + # The second call of find_network() should fail. So delete_network() + # was only called twice. + calls = [ + call(self._networks[0]), + call(self._networks[1]), + ] + self.network.delete_network.assert_has_calls(calls) + class TestListNetwork(TestNetwork): @@ -390,7 +449,7 @@ class TestListNetwork(TestNetwork): net.shared, utils.format_list(net.subnets), net.provider_network_type, - network._format_router_external(net.router_external), + network._format_router_external(net.is_router_external), utils.format_list(net.availability_zones), )) @@ -486,6 +545,7 @@ class TestSetNetwork(TestNetwork): '--provider-network-type', 'vlan', '--provider-physical-network', 'physnet1', '--provider-segment', '400', + '--no-transparent-vlan', ] verifylist = [ ('network', self._network.name), @@ -497,6 +557,7 @@ class TestSetNetwork(TestNetwork): ('provider_network_type', 'vlan'), ('physical_network', 'physnet1'), ('segmentation_id', '400'), + ('no_transparent_vlan', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -511,6 +572,7 @@ class TestSetNetwork(TestNetwork): 'provider:network_type': 'vlan', 'provider:physical_network': 'physnet1', 'provider:segmentation_id': '400', + 'vlan_transparent': False, } self.network.update_network.assert_called_once_with( self._network, **attrs) @@ -565,7 +627,7 @@ class TestShowNetwork(TestNetwork): 'name', 'project_id', 'provider_network_type', - 'router_external', + 'router:external', 'shared', 'status', 'subnets', @@ -580,7 +642,7 @@ class TestShowNetwork(TestNetwork): _network.name, _network.project_id, _network.provider_network_type, - network._format_router_external(_network.router_external), + network._format_router_external(_network.is_router_external), _network.shared, _network.status, utils.format_list(_network.subnets), @@ -746,36 +808,97 @@ class TestCreateNetworkCompute(TestNetworkCompute): class TestDeleteNetworkCompute(TestNetworkCompute): - # The network to delete. - _network = compute_fakes.FakeNetwork.create_one_network() - def setUp(self): super(TestDeleteNetworkCompute, self).setUp() self.app.client_manager.network_endpoint_enabled = False + # The networks to delete + self._networks = compute_fakes.FakeNetwork.create_networks(count=3) + self.compute.networks.delete.return_value = None # Return value of utils.find_resource() - self.compute.networks.get.return_value = self._network + self.compute.networks.get = \ + compute_fakes.FakeNetwork.get_networks(networks=self._networks) # Get the command object to test self.cmd = network.DeleteNetwork(self.app, None) - def test_network_delete(self): + def test_delete_one_network(self): arglist = [ - self._network.label, + self._networks[0].label, ] verifylist = [ - ('network', [self._network.label]), + ('network', [self._networks[0].label]), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.compute.networks.delete.assert_called_once_with( + self._networks[0].id) + self.assertIsNone(result) + + def test_delete_multiple_networks(self): + arglist = [] + for n in self._networks: + arglist.append(n.label) + verifylist = [ + ('network', arglist), + ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) - self.compute.networks.delete.assert_called_once_with(self._network.id) + calls = [] + for n in self._networks: + calls.append(call(n.id)) + self.compute.networks.delete.assert_has_calls(calls) self.assertIsNone(result) + def test_delete_multiple_networks_exception(self): + arglist = [ + self._networks[0].id, + 'xxxx-yyyy-zzzz', + self._networks[1].id, + ] + verifylist = [ + ('network', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # Fake exception in utils.find_resource() + # In compute v2, we use utils.find_resource() to find a network. + # It calls get() several times, but find() only one time. So we + # choose to fake get() always raise exception, then pass through. + # And fake find() to find the real network or not. + self.compute.networks.get.side_effect = Exception() + ret_find = [ + self._networks[0], + Exception(), + self._networks[1], + ] + self.compute.networks.find.side_effect = ret_find + + # Fake exception in delete() + ret_delete = [ + None, + Exception(), + ] + self.compute.networks.delete = mock.Mock(side_effect=ret_delete) + + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + # The second call of utils.find_resource() should fail. So delete() + # was only called twice. + calls = [ + call(self._networks[0].id), + call(self._networks[1].id), + ] + self.compute.networks.delete.assert_has_calls(calls) + class TestListNetworkCompute(TestNetworkCompute): diff --git a/openstackclient/tests/network/v2/test_router.py b/openstackclient/tests/network/v2/test_router.py index 655e86c9..99b41d2d 100644 --- a/openstackclient/tests/network/v2/test_router.py +++ b/openstackclient/tests/network/v2/test_router.py @@ -495,6 +495,42 @@ class TestSetRouter(TestRouter): self._router, **attrs) self.assertIsNone(result) + def test_set_no_route(self): + arglist = [ + self._router.name, + '--no-route', + ] + verifylist = [ + ('router', self._router.name), + ('no_route', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'routes': [], + } + self.network.update_router.assert_called_once_with( + self._router, **attrs) + self.assertIsNone(result) + + def test_set_route_no_route(self): + arglist = [ + self._router.name, + '--route', 'destination=10.20.30.0/24,gateway=10.20.30.1', + '--no-route', + ] + verifylist = [ + ('router', self._router.name), + ('routes', [{'destination': '10.20.30.0/24', + 'gateway': '10.20.30.1'}]), + ('no_route', True), + ] + + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, verifylist) + def test_set_clear_routes(self): arglist = [ self._router.name, diff --git a/openstackclient/tests/network/v2/test_security_group_rule.py b/openstackclient/tests/network/v2/test_security_group_rule.py index bd903f9e..2a64b884 100644 --- a/openstackclient/tests/network/v2/test_security_group_rule.py +++ b/openstackclient/tests/network/v2/test_security_group_rule.py @@ -14,6 +14,7 @@ import copy import mock +from openstackclient.common import exceptions from openstackclient.network import utils as network_utils from openstackclient.network.v2 import security_group_rule from openstackclient.tests.compute.v2 import fakes as compute_fakes @@ -131,22 +132,40 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, []) - def test_create_bad_protocol(self): + def test_create_bad_ethertype(self): arglist = [ - '--protocol', 'foo', + '--ethertype', 'foo', self._security_group.id, ] self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, []) - def test_create_bad_ethertype(self): + def test_create_all_protocol_options(self): arglist = [ - '--ethertype', 'foo', + '--protocol', 'tcp', + '--proto', 'tcp', self._security_group.id, ] self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, []) + def test_create_all_port_range_options(self): + arglist = [ + '--dst-port', '80:80', + '--icmp-type', '3', + '--icmp-code', '1', + self._security_group.id, + ] + verifylist = [ + ('dst_port', (80, 80)), + ('icmp_type', 3), + ('icmp_code', 1), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + def test_create_default_rule(self): self._setup_security_group_rule({ 'port_range_max': 443, @@ -177,6 +196,36 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.assertEqual(self.expected_columns, columns) self.assertEqual(self.expected_data, data) + def test_create_proto_option(self): + self._setup_security_group_rule({ + 'protocol': 'icmp', + 'remote_ip_prefix': '10.0.2.0/24', + }) + arglist = [ + '--proto', self._security_group_rule.protocol, + '--src-ip', self._security_group_rule.remote_ip_prefix, + self._security_group.id, + ] + verifylist = [ + ('proto', self._security_group_rule.protocol), + ('protocol', None), + ('src_ip', self._security_group_rule.remote_ip_prefix), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + def test_create_source_group(self): self._setup_security_group_rule({ 'port_range_max': 22, @@ -215,17 +264,15 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): def test_create_source_ip(self): self._setup_security_group_rule({ 'protocol': 'icmp', - 'port_range_max': -1, - 'port_range_min': -1, 'remote_ip_prefix': '10.0.2.0/24', }) arglist = [ - '--proto', self._security_group_rule.protocol, + '--protocol', self._security_group_rule.protocol, '--src-ip', self._security_group_rule.remote_ip_prefix, self._security_group.id, ] verifylist = [ - ('proto', self._security_group_rule.protocol), + ('protocol', self._security_group_rule.protocol), ('src_ip', self._security_group_rule.remote_ip_prefix), ('group', self._security_group.id), ] @@ -249,6 +296,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): 'ethertype': 'IPv6', 'port_range_max': 443, 'port_range_min': 443, + 'protocol': '6', 'remote_group_id': None, 'remote_ip_prefix': None, }) @@ -258,6 +306,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): '--ethertype', self._security_group_rule.ethertype, '--project', identity_fakes.project_name, '--project-domain', identity_fakes.domain_name, + '--protocol', self._security_group_rule.protocol, self._security_group.id, ] verifylist = [ @@ -267,6 +316,7 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): ('ethertype', self._security_group_rule.ethertype), ('project', identity_fakes.project_name), ('project_domain', identity_fakes.domain_name), + ('protocol', self._security_group_rule.protocol), ('group', self._security_group.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -285,6 +335,136 @@ class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): self.assertEqual(self.expected_columns, columns) self.assertEqual(self.expected_data, data) + def test_create_tcp_with_icmp_type(self): + arglist = [ + '--protocol', 'tcp', + '--icmp-type', '15', + self._security_group.id, + ] + verifylist = [ + ('protocol', 'tcp'), + ('icmp_type', 15), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_create_icmp_code(self): + arglist = [ + '--protocol', '1', + '--icmp-code', '1', + self._security_group.id, + ] + verifylist = [ + ('protocol', '1'), + ('icmp_code', 1), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, self.cmd.take_action, + parsed_args) + + def test_create_icmp_type(self): + self._setup_security_group_rule({ + 'port_range_min': 15, + 'protocol': 'icmp', + 'remote_ip_prefix': '0.0.0.0/0', + }) + arglist = [ + '--icmp-type', str(self._security_group_rule.port_range_min), + '--protocol', self._security_group_rule.protocol, + self._security_group.id, + ] + verifylist = [ + ('dst_port', None), + ('icmp_type', self._security_group_rule.port_range_min), + ('icmp_code', None), + ('protocol', self._security_group_rule.protocol), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'port_range_min': self._security_group_rule.port_range_min, + 'protocol': self._security_group_rule.protocol, + 'remote_ip_prefix': self._security_group_rule.remote_ip_prefix, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_ipv6_icmp_type_code(self): + self._setup_security_group_rule({ + 'ethertype': 'IPv6', + 'port_range_min': 139, + 'port_range_max': 2, + 'protocol': 'ipv6-icmp', + }) + arglist = [ + '--icmp-type', str(self._security_group_rule.port_range_min), + '--icmp-code', str(self._security_group_rule.port_range_max), + '--protocol', self._security_group_rule.protocol, + self._security_group.id, + ] + verifylist = [ + ('dst_port', None), + ('icmp_type', self._security_group_rule.port_range_min), + ('icmp_code', self._security_group_rule.port_range_max), + ('protocol', self._security_group_rule.protocol), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'port_range_min': self._security_group_rule.port_range_min, + 'port_range_max': self._security_group_rule.port_range_max, + 'protocol': self._security_group_rule.protocol, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + + def test_create_icmpv6_type(self): + self._setup_security_group_rule({ + 'ethertype': 'IPv6', + 'port_range_min': 139, + 'protocol': 'icmpv6', + }) + arglist = [ + '--icmp-type', str(self._security_group_rule.port_range_min), + '--protocol', self._security_group_rule.protocol, + self._security_group.id, + ] + verifylist = [ + ('dst_port', None), + ('icmp_type', self._security_group_rule.port_range_min), + ('icmp_code', None), + ('protocol', self._security_group_rule.protocol), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_security_group_rule.assert_called_once_with(**{ + 'direction': self._security_group_rule.direction, + 'ethertype': self._security_group_rule.ethertype, + 'port_range_min': self._security_group_rule.port_range_min, + 'protocol': self._security_group_rule.protocol, + 'security_group_id': self._security_group.id, + }) + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, data) + class TestCreateSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): @@ -337,10 +517,21 @@ class TestCreateSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): self.assertRaises(tests_utils.ParserException, self.check_parser, self.cmd, arglist, []) + def test_create_all_protocol_options(self): + arglist = [ + '--protocol', 'tcp', + '--proto', 'tcp', + self._security_group.id, + ] + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, []) + def test_create_network_options(self): arglist = [ '--ingress', '--ethertype', 'IPv4', + '--icmp-type', '3', + '--icmp-code', '11', '--project', identity_fakes.project_name, '--project-domain', identity_fakes.domain_name, self._security_group.id, @@ -416,12 +607,45 @@ class TestCreateSecurityGroupRuleCompute(TestSecurityGroupRuleCompute): 'ip_range': {'cidr': '10.0.2.0/24'}, }) arglist = [ + '--protocol', self._security_group_rule.ip_protocol, + '--src-ip', self._security_group_rule.ip_range['cidr'], + self._security_group.id, + ] + verifylist = [ + ('protocol', self._security_group_rule.ip_protocol), + ('src_ip', self._security_group_rule.ip_range['cidr']), + ('group', self._security_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute.security_group_rules.create.assert_called_once_with( + self._security_group.id, + self._security_group_rule.ip_protocol, + self._security_group_rule.from_port, + self._security_group_rule.to_port, + self._security_group_rule.ip_range['cidr'], + None, + ) + self.assertEqual(expected_columns, columns) + self.assertEqual(expected_data, data) + + def test_create_proto_option(self): + expected_columns, expected_data = self._setup_security_group_rule({ + 'ip_protocol': 'icmp', + 'from_port': -1, + 'to_port': -1, + 'ip_range': {'cidr': '10.0.2.0/24'}, + }) + arglist = [ '--proto', self._security_group_rule.ip_protocol, '--src-ip', self._security_group_rule.ip_range['cidr'], self._security_group.id, ] verifylist = [ ('proto', self._security_group_rule.ip_protocol), + ('protocol', None), ('src_ip', self._security_group_rule.ip_range['cidr']), ('group', self._security_group.id), ] @@ -522,8 +746,6 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): _security_group_rule_icmp = \ network_fakes.FakeSecurityGroupRule.create_one_security_group_rule({ 'protocol': 'icmp', - 'port_range_max': -1, - 'port_range_min': -1, 'remote_ip_prefix': '10.0.2.0/24', 'security_group_id': _security_group.id, }) diff --git a/openstackclient/tests/network/v2/test_subnet.py b/openstackclient/tests/network/v2/test_subnet.py index 1923286d..22c288f9 100644 --- a/openstackclient/tests/network/v2/test_subnet.py +++ b/openstackclient/tests/network/v2/test_subnet.py @@ -469,6 +469,22 @@ class TestListSubnet(TestSubnet): self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) + def test_subnet_list_ip_version(self): + arglist = [ + '--ip-version', str(4), + ] + verifylist = [ + ('ip_version', 4), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'ip_version': 4} + + self.network.subnets.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestSetSubnet(TestSubnet): diff --git a/openstackclient/tests/volume/v2/fakes.py b/openstackclient/tests/volume/v2/fakes.py index fc45e47b..c4155c69 100644 --- a/openstackclient/tests/volume/v2/fakes.py +++ b/openstackclient/tests/volume/v2/fakes.py @@ -498,3 +498,57 @@ class FakeAvailabilityZone(object): availability_zones.append(availability_zone) return availability_zones + + +class FakeBackup(object): + """Fake one or more backup.""" + + @staticmethod + def create_one_backup(attrs=None): + """Create a fake backup. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object with id, name, volume_id, etc. + """ + attrs = attrs or {} + + # Set default attributes. + backup_info = { + "id": 'backup-id-' + uuid.uuid4().hex, + "name": 'backup-name-' + uuid.uuid4().hex, + "volume_id": 'volume-id-' + uuid.uuid4().hex, + "description": 'description-' + uuid.uuid4().hex, + "object_count": None, + "container": 'container-' + uuid.uuid4().hex, + "size": random.randint(1, 20), + "status": "error", + "availability_zone": 'zone' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + backup_info.update(attrs) + + backup = fakes.FakeResource( + info=copy.deepcopy(backup_info), + loaded=True) + return backup + + @staticmethod + def create_backups(attrs=None, count=2): + """Create multiple fake backups. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of backups to fake + :return: + A list of FakeResource objects faking the backups + """ + backups = [] + for i in range(0, count): + backup = FakeBackup.create_one_backup(attrs) + backups.append(backup) + + return backups diff --git a/openstackclient/tests/volume/v2/test_backup.py b/openstackclient/tests/volume/v2/test_backup.py index 0e906e7b..8a151a91 100644 --- a/openstackclient/tests/volume/v2/test_backup.py +++ b/openstackclient/tests/volume/v2/test_backup.py @@ -12,9 +12,6 @@ # under the License. # -import copy - -from openstackclient.tests import fakes from openstackclient.tests.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import backup @@ -34,83 +31,101 @@ class TestBackup(volume_fakes.TestVolume): class TestBackupCreate(TestBackup): + volume = volume_fakes.FakeVolume.create_one_volume() + new_backup = volume_fakes.FakeBackup.create_one_backup( + attrs={'volume_id': volume.id}) + + columns = ( + 'availability_zone', + 'container', + 'description', + 'id', + 'name', + 'object_count', + 'size', + 'status', + 'volume_id', + ) + data = ( + new_backup.availability_zone, + new_backup.container, + new_backup.description, + new_backup.id, + new_backup.name, + new_backup.object_count, + new_backup.size, + new_backup.status, + new_backup.volume_id, + ) + def setUp(self): super(TestBackupCreate, self).setUp() - self.volumes_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True - ) + self.volumes_mock.get.return_value = self.volume + self.backups_mock.create.return_value = self.new_backup - self.backups_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True - ) # Get the command object to test self.cmd = backup.CreateBackup(self.app, None) def test_backup_create(self): arglist = [ - volume_fakes.volume_id, - "--name", volume_fakes.backup_name, - "--description", volume_fakes.backup_description, - "--container", volume_fakes.backup_name + "--name", self.new_backup.name, + "--description", self.new_backup.description, + "--container", self.new_backup.container, + self.new_backup.volume_id, ] verifylist = [ - ("volume", volume_fakes.volume_id), - ("name", volume_fakes.backup_name), - ("description", volume_fakes.backup_description), - ("container", volume_fakes.backup_name) + ("name", self.new_backup.name), + ("description", self.new_backup.description), + ("container", self.new_backup.container), + ("volume", self.new_backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.backups_mock.create.assert_called_with( - volume_fakes.volume_id, - container=volume_fakes.backup_name, - name=volume_fakes.backup_name, - description=volume_fakes.backup_description + self.new_backup.volume_id, + container=self.new_backup.container, + name=self.new_backup.name, + description=self.new_backup.description ) - self.assertEqual(columns, volume_fakes.BACKUP_columns) - self.assertEqual(data, volume_fakes.BACKUP_data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) def test_backup_create_without_name(self): arglist = [ - volume_fakes.volume_id, - "--description", volume_fakes.backup_description, - "--container", volume_fakes.backup_name + "--description", self.new_backup.description, + "--container", self.new_backup.container, + self.new_backup.volume_id, ] verifylist = [ - ("volume", volume_fakes.volume_id), - ("description", volume_fakes.backup_description), - ("container", volume_fakes.backup_name) + ("description", self.new_backup.description), + ("container", self.new_backup.container), + ("volume", self.new_backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.backups_mock.create.assert_called_with( - volume_fakes.volume_id, - container=volume_fakes.backup_name, + self.new_backup.volume_id, + container=self.new_backup.container, name=None, - description=volume_fakes.backup_description + description=self.new_backup.description ) - self.assertEqual(columns, volume_fakes.BACKUP_columns) - self.assertEqual(data, volume_fakes.BACKUP_data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) class TestBackupDelete(TestBackup): + backup = volume_fakes.FakeBackup.create_one_backup() + def setUp(self): super(TestBackupDelete, self).setUp() - self.backups_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True) + self.backups_mock.get.return_value = self.backup self.backups_mock.delete.return_value = None # Get the command object to mock @@ -118,21 +133,25 @@ class TestBackupDelete(TestBackup): def test_backup_delete(self): arglist = [ - volume_fakes.backup_id + self.backup.id ] verifylist = [ - ("backups", [volume_fakes.backup_id]) + ("backups", [self.backup.id]) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.backups_mock.delete.assert_called_with(volume_fakes.backup_id) + self.backups_mock.delete.assert_called_with(self.backup.id) self.assertIsNone(result) class TestBackupList(TestBackup): + volume = volume_fakes.FakeVolume.create_one_volume() + backups = volume_fakes.FakeBackup.create_backups( + attrs={'volume_id': volume.name}, count=3) + columns = [ 'ID', 'Name', @@ -140,33 +159,39 @@ class TestBackupList(TestBackup): 'Status', 'Size', ] - datalist = ( - ( - volume_fakes.backup_id, - volume_fakes.backup_name, - volume_fakes.backup_description, - volume_fakes.backup_status, - volume_fakes.backup_size - ), - ) + columns_long = columns + [ + 'Availability Zone', + 'Volume', + 'Container', + ] + + data = [] + for b in backups: + data.append(( + b.id, + b.name, + b.description, + b.status, + b.size, + )) + data_long = [] + for b in backups: + data_long.append(( + b.id, + b.name, + b.description, + b.status, + b.size, + b.availability_zone, + b.volume_id, + b.container, + )) def setUp(self): super(TestBackupList, self).setUp() - self.volumes_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True - ) - ] - self.backups_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True - ) - ] + self.volumes_mock.list.return_value = [self.volume] + self.backups_mock.list.return_value = self.backups # Get the command to test self.cmd = backup.ListBackup(self.app, None) @@ -178,7 +203,7 @@ class TestBackupList(TestBackup): columns, data = self.cmd.take_action(parsed_args) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + self.assertEqual(self.data, list(data)) def test_backup_list_with_options(self): arglist = ["--long"] @@ -187,86 +212,87 @@ class TestBackupList(TestBackup): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - columns = self.columns + [ - 'Availability Zone', - 'Volume', - 'Container', - ] - - self.assertEqual(columns, columns) - - datalist = (( - volume_fakes.backup_id, - volume_fakes.backup_name, - volume_fakes.backup_description, - volume_fakes.backup_status, - volume_fakes.backup_size, - volume_fakes.volume_availability_zone, - volume_fakes.backup_volume_id, - volume_fakes.backup_container - ),) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) class TestBackupRestore(TestBackup): + volume = volume_fakes.FakeVolume.create_one_volume() + backup = volume_fakes.FakeBackup.create_one_backup( + attrs={'volume_id': volume.id}) + def setUp(self): super(TestBackupRestore, self).setUp() - self.backups_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True - ) - self.volumes_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.VOLUME), - loaded=True - ) + self.backups_mock.get.return_value = self.backup + self.volumes_mock.get.return_value = self.volume self.restores_mock.restore.return_value = None # Get the command object to mock self.cmd = backup.RestoreBackup(self.app, None) def test_backup_restore(self): arglist = [ - volume_fakes.backup_id, - volume_fakes.volume_id + self.backup.id, + self.backup.volume_id ] verifylist = [ - ("backup", volume_fakes.backup_id), - ("volume", volume_fakes.volume_id) + ("backup", self.backup.id), + ("volume", self.backup.volume_id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.restores_mock.restore.assert_called_with(volume_fakes.backup_id, - volume_fakes.volume_id) + self.restores_mock.restore.assert_called_with(self.backup.id, + self.backup.volume_id) self.assertIsNone(result) class TestBackupShow(TestBackup): + backup = volume_fakes.FakeBackup.create_one_backup() + + columns = ( + 'availability_zone', + 'container', + 'description', + 'id', + 'name', + 'object_count', + 'size', + 'status', + 'volume_id', + ) + data = ( + backup.availability_zone, + backup.container, + backup.description, + backup.id, + backup.name, + backup.object_count, + backup.size, + backup.status, + backup.volume_id, + ) + def setUp(self): super(TestBackupShow, self).setUp() - self.backups_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(volume_fakes.BACKUP), - loaded=True) + self.backups_mock.get.return_value = self.backup # Get the command object to test self.cmd = backup.ShowBackup(self.app, None) def test_backup_show(self): arglist = [ - volume_fakes.backup_id + self.backup.id ] verifylist = [ - ("backup", volume_fakes.backup_id) + ("backup", self.backup.id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.backups_mock.get.assert_called_with(volume_fakes.backup_id) + self.backups_mock.get.assert_called_with(self.backup.id) - self.assertEqual(volume_fakes.BACKUP_columns, columns) - self.assertEqual(volume_fakes.BACKUP_data, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/volume/v2/test_snapshot.py b/openstackclient/tests/volume/v2/test_snapshot.py index 8c75dfb2..d2fa5e07 100644 --- a/openstackclient/tests/volume/v2/test_snapshot.py +++ b/openstackclient/tests/volume/v2/test_snapshot.py @@ -229,7 +229,6 @@ class TestSnapshotList(TestSnapshot): class TestSnapshotSet(TestSnapshot): - def setUp(self): super(TestSnapshotSet, self).setUp() @@ -270,6 +269,23 @@ class TestSnapshotSet(TestSnapshot): ) self.assertIsNone(result) + def test_snapshot_set_state_to_error(self): + arglist = [ + "--state", "error", + volume_fakes.snapshot_id + ] + verifylist = [ + ("state", "error"), + ("snapshot", volume_fakes.snapshot_id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.snapshots_mock.reset_state.assert_called_with( + volume_fakes.snapshot_id, "error") + self.assertIsNone(result) + class TestSnapshotShow(TestSnapshot): @@ -300,7 +316,6 @@ class TestSnapshotShow(TestSnapshot): class TestSnapshotUnset(TestSnapshot): - def setUp(self): super(TestSnapshotUnset, self).setUp() @@ -322,6 +337,7 @@ class TestSnapshotUnset(TestSnapshot): ("snapshot", volume_fakes.snapshot_id), ("property", ["foo"]) ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) diff --git a/openstackclient/volume/v2/snapshot.py b/openstackclient/volume/v2/snapshot.py index f124a5e2..65cb9a75 100644 --- a/openstackclient/volume/v2/snapshot.py +++ b/openstackclient/volume/v2/snapshot.py @@ -179,6 +179,14 @@ class SetSnapshot(command.Command): help='Property to add/change for this snapshot ' '(repeat option to set multiple properties)', ) + parser.add_argument( + '--state', + metavar='<state>', + choices=['available', 'error', 'creating', 'deleting', + 'error-deleting'], + help='New snapshot state. Valid values are available, ' + 'error, creating, deleting, and error-deleting.', + ) return parser def take_action(self, parsed_args): @@ -192,13 +200,17 @@ class SetSnapshot(command.Command): if parsed_args.description: kwargs['description'] = parsed_args.description - if not kwargs and not parsed_args.property: + if (not kwargs and not parsed_args.property and not + parsed_args.state): self.app.log.error("No changes requested\n") return if parsed_args.property: volume_client.volume_snapshots.set_metadata(snapshot.id, parsed_args.property) + if parsed_args.state: + volume_client.volume_snapshots.reset_state(snapshot.id, + parsed_args.state) volume_client.volume_snapshots.update(snapshot.id, **kwargs) |
