diff options
Diffstat (limited to 'openstackclient')
20 files changed, 573 insertions, 38 deletions
diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 13c0a99c..3725a3a8 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -16,6 +16,7 @@ """Keypair action implementations""" import io +import logging import os import sys @@ -27,6 +28,9 @@ import six from openstackclient.i18n import _ +LOG = logging.getLogger(__name__) + + class CreateKeypair(command.ShowOne): """Create new public key""" @@ -78,20 +82,37 @@ class CreateKeypair(command.ShowOne): class DeleteKeypair(command.Command): - """Delete public key""" + """Delete public key(s)""" def get_parser(self, prog_name): parser = super(DeleteKeypair, self).get_parser(prog_name) parser.add_argument( 'name', metavar='<key>', - help=_("Public key to delete (name only)") + nargs='+', + help=_("Public key(s) to delete (name only)") ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute - compute_client.keypairs.delete(parsed_args.name) + result = 0 + for n in parsed_args.name: + try: + data = utils.find_resource( + compute_client.keypairs, n) + compute_client.keypairs.delete(data.name) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete public key with name " + "'%(name)s': %(e)s") + % {'name': n, 'e': e}) + + if result > 0: + total = len(parsed_args.name) + msg = (_("%(result)s of %(total)s public keys failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) class ListKeypair(command.Lister): diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 1441114b..53624f55 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -36,14 +36,28 @@ class DeleteService(command.Command): parser.add_argument( "service", metavar="<service>", - help=_("Compute service to delete (ID only)") + nargs='+', + help=_("Compute service(s) to delete (ID only)") ) return parser def take_action(self, parsed_args): compute_client = self.app.client_manager.compute + result = 0 + for s in parsed_args.service: + try: + compute_client.services.delete(s) + except Exception as e: + result += 1 + LOG.error(_("Failed to delete compute service with " + "ID '%(service)s': %(e)s") + % {'service': s, 'e': e}) - compute_client.services.delete(parsed_args.service) + if result > 0: + total = len(parsed_args.service) + msg = (_("%(result)s of %(total)s compute services failed " + "to delete.") % {'result': result, 'total': total}) + raise exceptions.CommandError(msg) class ListService(command.Lister): diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index 41735500..31dfc798 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -108,7 +108,7 @@ def _add_additional_network_options(parser): 'vlan', 'vxlan'], help=_("The physical mechanism by which the virtual network " "is implemented. The supported options are: " - "flat, geneve, gre, local, vlan, vxlan")) + "flat, geneve, gre, local, vlan, vxlan.")) parser.add_argument( '--provider-physical-network', metavar='<provider-physical-network>', diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 5d1431b5..2a069f09 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -196,7 +196,8 @@ def _add_updatable_args(parser): parser.add_argument( '--device-owner', metavar='<device-owner>', - help=_("Device owner of this port") + help=_("Device owner of this port. This is the entity that uses " + "the port (for example, network:dhcp).") ) parser.add_argument( '--vnic-type', @@ -337,6 +338,13 @@ class ListPort(command.Lister): def get_parser(self, prog_name): parser = super(ListPort, self).get_parser(prog_name) parser.add_argument( + '--device-owner', + metavar='<device-owner>', + help=_("List only ports with the specified device owner. " + "This is the entity that uses the port (for example, " + "network:dhcp).") + ) + parser.add_argument( '--router', metavar='<router>', dest='router', @@ -361,10 +369,12 @@ class ListPort(command.Lister): ) filters = {} + if parsed_args.device_owner is not None: + filters['device_owner'] = parsed_args.device_owner if parsed_args.router: _router = client.find_router(parsed_args.router, ignore_missing=False) - filters = {'device_id': _router.id} + filters['device_id'] = _router.id data = client.ports(**filters) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 2f41838f..f6d96d03 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -14,6 +14,7 @@ """Router action implementations""" import argparse +import copy import json import logging @@ -462,3 +463,45 @@ class ShowRouter(command.ShowOne): columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) return (columns, data) + + +class UnsetRouter(command.Command): + """Unset router properties""" + + def get_parser(self, prog_name): + parser = super(UnsetRouter, self).get_parser(prog_name) + parser.add_argument( + '--route', + metavar='destination=<subnet>,gateway=<ip-address>', + action=parseractions.MultiKeyValueAction, + dest='routes', + default=None, + required_keys=['destination', 'gateway'], + help=_("Routes to be removed from the router " + "destination: destination subnet (in CIDR notation) " + "gateway: nexthop IP address " + "(repeat option to unset multiple routes)")) + parser.add_argument( + 'router', + metavar="<router>", + help=_("Router to modify (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_router(parsed_args.router, ignore_missing=False) + tmp_routes = copy.deepcopy(obj.routes) + attrs = {} + if parsed_args.routes: + try: + for route in parsed_args.routes: + tmp_routes.remove(route) + except ValueError: + msg = (_("Router does not contain route %s") % route) + raise exceptions.CommandError(msg) + for route in tmp_routes: + route['nexthop'] = route.pop('gateway') + attrs['routes'] = tmp_routes + if attrs: + client.update_router(obj, **attrs) diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 752923f7..45d3442d 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -28,6 +28,11 @@ from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) +def _update_arguments(obj_list, parsed_args_list): + for item in parsed_args_list: + obj_list.remove(item) + + def _format_allocation_pools(data): pool_formatted = ['%s-%s' % (pool.get('start', ''), pool.get('end', '')) for pool in data] @@ -235,7 +240,7 @@ class CreateSubnet(command.ShowOne): "'auto': Gateway address should automatically be chosen " "from within the subnet itself, 'none': This subnet will " "not use a gateway, e.g.: --gateway 192.168.9.1, " - "--gateway auto, --gateway none (default is 'auto')") + "--gateway auto, --gateway none (default is 'auto').") ) parser.add_argument( '--ip-version', @@ -244,7 +249,7 @@ class CreateSubnet(command.ShowOne): choices=[4, 6], help=_("IP version (default is 4). Note that when subnet pool is " "specified, IP version is determined from the subnet pool " - "and this option is ignored") + "and this option is ignored.") ) parser.add_argument( '--ipv6-ra-mode', @@ -334,7 +339,7 @@ class ListSubnet(command.Lister): choices=[4, 6], metavar='<ip-version>', dest='ip_version', - help=_("List only subnets of given IP version in output" + help=_("List only subnets of given IP version in output." "Allowed values for IP version are 4 and 6."), ) return parser @@ -395,7 +400,7 @@ class SetSubnet(command.Command): help=_("Specify a gateway for the subnet. The options are: " "<ip-address>: Specific IP address to use as the gateway, " "'none': This subnet will not use a gateway, " - "e.g.: --gateway 192.168.9.1, --gateway none") + "e.g.: --gateway 192.168.9.1, --gateway none.") ) _get_common_parse_arguments(parser) return parser @@ -433,3 +438,81 @@ class ShowSubnet(command.ShowOne): columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) return (columns, data) + + +class UnsetSubnet(command.Command): + """Unset subnet properties""" + + def get_parser(self, prog_name): + parser = super(UnsetSubnet, self).get_parser(prog_name) + parser.add_argument( + '--allocation-pool', + metavar='start=<ip-address>,end=<ip-address>', + dest='allocation_pools', + action=parseractions.MultiKeyValueAction, + required_keys=['start', 'end'], + help=_('Allocation pool to be removed from this subnet ' + 'e.g.: start=192.168.199.2,end=192.168.199.254 ' + '(repeat option to unset multiple Allocation pools)') + ) + parser.add_argument( + '--dns-nameserver', + metavar='<dns-nameserver>', + action='append', + dest='dns_nameservers', + help=_('DNS server to be removed from this subnet ' + '(repeat option to set multiple DNS servers)') + ) + parser.add_argument( + '--host-route', + metavar='destination=<subnet>,gateway=<ip-address>', + dest='host_routes', + action=parseractions.MultiKeyValueAction, + required_keys=['destination', 'gateway'], + help=_('Route to be removed from this subnet ' + 'e.g.: destination=10.10.0.0/16,gateway=192.168.71.254 ' + 'destination: destination subnet (in CIDR notation) ' + 'gateway: nexthop IP address ' + '(repeat option to unset multiple host routes)') + ) + parser.add_argument( + 'subnet', + metavar="<subnet>", + help=_("Subnet to modify (name or ID)") + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_subnet(parsed_args.subnet, ignore_missing=False) + tmp_obj = copy.deepcopy(obj) + attrs = {} + if parsed_args.dns_nameservers: + try: + _update_arguments(tmp_obj.dns_nameservers, + parsed_args.dns_nameservers) + except ValueError as error: + msg = (_("%s not in dns-nameservers") % str(error)) + raise exceptions.CommandError(msg) + attrs['dns_nameservers'] = tmp_obj.dns_nameservers + if parsed_args.host_routes: + try: + _update_arguments( + tmp_obj.host_routes, + convert_entries_to_nexthop(parsed_args.host_routes)) + except ValueError as error: + msg = (_("Subnet does not have %s in host-routes") % + str(error)) + raise exceptions.CommandError(msg) + attrs['host_routes'] = tmp_obj.host_routes + if parsed_args.allocation_pools: + try: + _update_arguments(tmp_obj.allocation_pools, + parsed_args.allocation_pools) + except ValueError as error: + msg = (_("Subnet does not have %s in allocation-pools") % + str(error)) + raise exceptions.CommandError(msg) + attrs['allocation_pools'] = tmp_obj.allocation_pools + if attrs: + client.update_subnet(obj, **attrs) diff --git a/openstackclient/tests/compute/v2/fakes.py b/openstackclient/tests/compute/v2/fakes.py index a7a66d5e..b9add2c8 100644 --- a/openstackclient/tests/compute/v2/fakes.py +++ b/openstackclient/tests/compute/v2/fakes.py @@ -827,6 +827,25 @@ class FakeKeypair(object): return keypairs + @staticmethod + def get_keypairs(keypairs=None, count=2): + """Get an iterable MagicMock object with a list of faked keypairs. + + If keypairs list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List keypairs: + A list of FakeResource objects faking keypairs + :param int count: + The number of keypairs to fake + :return: + An iterable Mock object with side_effect set to a list of faked + keypairs + """ + if keypairs is None: + keypairs = FakeKeypair.create_keypairs(count) + return mock.MagicMock(side_effect=keypairs) + class FakeAvailabilityZone(object): """Fake one or more compute availability zones (AZs).""" diff --git a/openstackclient/tests/compute/v2/test_keypair.py b/openstackclient/tests/compute/v2/test_keypair.py index a50a5323..25949e31 100644 --- a/openstackclient/tests/compute/v2/test_keypair.py +++ b/openstackclient/tests/compute/v2/test_keypair.py @@ -14,6 +14,10 @@ # import mock +from mock import call + +from osc_lib import exceptions +from osc_lib import utils from openstackclient.compute.v2 import keypair from openstackclient.tests.compute.v2 import fakes as compute_fakes @@ -114,22 +118,23 @@ class TestKeypairCreate(TestKeypair): class TestKeypairDelete(TestKeypair): - keypair = compute_fakes.FakeKeypair.create_one_keypair() + keypairs = compute_fakes.FakeKeypair.create_keypairs(count=2) def setUp(self): super(TestKeypairDelete, self).setUp() - self.keypairs_mock.get.return_value = self.keypair + self.keypairs_mock.get = compute_fakes.FakeKeypair.get_keypairs( + self.keypairs) self.keypairs_mock.delete.return_value = None self.cmd = keypair.DeleteKeypair(self.app, None) def test_keypair_delete(self): arglist = [ - self.keypair.name + self.keypairs[0].name ] verifylist = [ - ('name', self.keypair.name), + ('name', [self.keypairs[0].name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -137,7 +142,54 @@ class TestKeypairDelete(TestKeypair): ret = self.cmd.take_action(parsed_args) self.assertIsNone(ret) - self.keypairs_mock.delete.assert_called_with(self.keypair.name) + self.keypairs_mock.delete.assert_called_with(self.keypairs[0].name) + + def test_delete_multiple_keypairs(self): + arglist = [] + for k in self.keypairs: + arglist.append(k.name) + verifylist = [ + ('name', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for k in self.keypairs: + calls.append(call(k.name)) + self.keypairs_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_keypairs_with_exception(self): + arglist = [ + self.keypairs[0].name, + 'unexist_keypair', + ] + verifylist = [ + ('name', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self.keypairs[0], exceptions.CommandError] + with mock.patch.object(utils, 'find_resource', + side_effect=find_mock_result) as find_mock: + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 public keys failed to delete.', + str(e)) + + find_mock.assert_any_call( + self.keypairs_mock, self.keypairs[0].name) + find_mock.assert_any_call(self.keypairs_mock, 'unexist_keypair') + + self.assertEqual(2, find_mock.call_count) + self.keypairs_mock.delete.assert_called_once_with( + self.keypairs[0].name + ) class TestKeypairList(TestKeypair): diff --git a/openstackclient/tests/compute/v2/test_service.py b/openstackclient/tests/compute/v2/test_service.py index e41d633a..1599f466 100644 --- a/openstackclient/tests/compute/v2/test_service.py +++ b/openstackclient/tests/compute/v2/test_service.py @@ -14,6 +14,7 @@ # import mock +from mock import call from osc_lib import exceptions @@ -33,32 +34,74 @@ class TestService(compute_fakes.TestComputev2): class TestServiceDelete(TestService): + services = compute_fakes.FakeService.create_services(count=2) + def setUp(self): super(TestServiceDelete, self).setUp() - self.service = compute_fakes.FakeService.create_one_service() - self.service_mock.delete.return_value = None # Get the command object to test self.cmd = service.DeleteService(self.app, None) - def test_service_delete_no_options(self): + def test_service_delete(self): arglist = [ - self.service.binary, + self.services[0].binary, ] verifylist = [ - ('service', self.service.binary), + ('service', [self.services[0].binary]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) self.service_mock.delete.assert_called_with( - self.service.binary, + self.services[0].binary, ) self.assertIsNone(result) + def test_multi_services_delete(self): + arglist = [] + for s in self.services: + arglist.append(s.binary) + verifylist = [ + ('service', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for s in self.services: + calls.append(call(s.binary)) + self.service_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_multi_services_delete_with_exception(self): + arglist = [ + self.services[0].binary, + 'unexist_service', + ] + verifylist = [ + ('service', arglist) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + delete_mock_result = [None, exceptions.CommandError] + self.service_mock.delete = ( + mock.MagicMock(side_effect=delete_mock_result) + ) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 compute services failed to delete.', str(e)) + + self.service_mock.delete.assert_any_call(self.services[0].binary) + self.service_mock.delete.assert_any_call('unexist_service') + class TestServiceList(TestService): diff --git a/openstackclient/tests/network/v2/test_port.py b/openstackclient/tests/network/v2/test_port.py index a998585e..871fe872 100644 --- a/openstackclient/tests/network/v2/test_port.py +++ b/openstackclient/tests/network/v2/test_port.py @@ -369,6 +369,47 @@ class TestListPort(TestPort): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_port_list_device_owner_opt(self): + arglist = [ + '--device-owner', self._ports[0].device_owner, + ] + + verifylist = [ + ('device_owner', self._ports[0].device_owner) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with(**{ + 'device_owner': self._ports[0].device_owner + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_port_list_all_opt(self): + arglist = [ + '--device-owner', self._ports[0].device_owner, + '--router', 'fake-router-name', + ] + + verifylist = [ + ('device_owner', self._ports[0].device_owner), + ('router', 'fake-router-name') + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.ports.assert_called_once_with(**{ + 'device_owner': self._ports[0].device_owner, + 'device_id': 'fake-router-id' + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestSetPort(TestPort): diff --git a/openstackclient/tests/network/v2/test_router.py b/openstackclient/tests/network/v2/test_router.py index e3da253a..1ef4707b 100644 --- a/openstackclient/tests/network/v2/test_router.py +++ b/openstackclient/tests/network/v2/test_router.py @@ -698,3 +698,54 @@ class TestShowRouter(TestRouter): self._router.name, ignore_missing=False) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + + +class TestUnsetRouter(TestRouter): + + def setUp(self): + super(TestUnsetRouter, self).setUp() + self._testrouter = network_fakes.FakeRouter.create_one_router( + {'routes': [{"destination": "192.168.101.1/24", + "gateway": "172.24.4.3"}, + {"destination": "192.168.101.2/24", + "gateway": "172.24.4.3"}], }) + 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) + # Get the command object to test + self.cmd = router.UnsetRouter(self.app, self.namespace) + + def test_unset_router_params(self): + arglist = [ + '--route', 'destination=192.168.101.1/24,gateway=172.24.4.3', + self._testrouter.name, + ] + verifylist = [ + ('routes', [ + {"destination": "192.168.101.1/24", "gateway": "172.24.4.3"}]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'routes': [{"destination": "192.168.101.2/24", + "nexthop": "172.24.4.3"}], + } + self.network.update_router.assert_called_once_with( + self._testrouter, **attrs) + self.assertIsNone(result) + + def test_unset_router_wrong_routes(self): + arglist = [ + '--route', 'destination=192.168.101.1/24,gateway=172.24.4.2', + self._testrouter.name, + ] + verifylist = [ + ('routes', [ + {"destination": "192.168.101.1/24", "gateway": "172.24.4.2"}]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) diff --git a/openstackclient/tests/network/v2/test_subnet.py b/openstackclient/tests/network/v2/test_subnet.py index 99b558c0..82813d6c 100644 --- a/openstackclient/tests/network/v2/test_subnet.py +++ b/openstackclient/tests/network/v2/test_subnet.py @@ -767,3 +767,109 @@ class TestShowSubnet(TestSubnet): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) + + +class TestUnsetSubnet(TestSubnet): + + def setUp(self): + super(TestUnsetSubnet, self).setUp() + self._testsubnet = network_fakes.FakeSubnet.create_one_subnet( + {'dns_nameservers': ['8.8.8.8', + '8.8.8.4'], + 'host_routes': [{'destination': '10.20.20.0/24', + 'nexthop': '10.20.20.1'}, + {'destination': '10.30.30.30/24', + 'nexthop': '10.30.30.1'}], + 'allocation_pools': [{'start': '8.8.8.100', + 'end': '8.8.8.150'}, + {'start': '8.8.8.160', + 'end': '8.8.8.170'}], }) + self.network.find_subnet = mock.Mock(return_value=self._testsubnet) + self.network.update_subnet = mock.Mock(return_value=None) + # Get the command object to test + self.cmd = subnet_v2.UnsetSubnet(self.app, self.namespace) + + def test_unset_subnet_params(self): + arglist = [ + '--dns-nameserver', '8.8.8.8', + '--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.1', + '--allocation-pool', 'start=8.8.8.100,end=8.8.8.150', + self._testsubnet.name, + ] + verifylist = [ + ('dns_nameservers', ['8.8.8.8']), + ('host_routes', [{ + "destination": "10.30.30.30/24", "gateway": "10.30.30.1"}]), + ('allocation_pools', [{ + 'start': '8.8.8.100', 'end': '8.8.8.150'}]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'dns_nameservers': ['8.8.8.4'], + 'host_routes': [{ + "destination": "10.20.20.0/24", "nexthop": "10.20.20.1"}], + 'allocation_pools': [{'start': '8.8.8.160', 'end': '8.8.8.170'}], + } + self.network.update_subnet.assert_called_once_with( + self._testsubnet, **attrs) + self.assertIsNone(result) + + def test_unset_subnet_wrong_host_routes(self): + arglist = [ + '--dns-nameserver', '8.8.8.8', + '--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.2', + '--allocation-pool', 'start=8.8.8.100,end=8.8.8.150', + self._testsubnet.name, + ] + verifylist = [ + ('dns_nameservers', ['8.8.8.8']), + ('host_routes', [{ + "destination": "10.30.30.30/24", "gateway": "10.30.30.2"}]), + ('allocation_pools', [{ + 'start': '8.8.8.100', 'end': '8.8.8.150'}]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + + def test_unset_subnet_wrong_allocation_pool(self): + arglist = [ + '--dns-nameserver', '8.8.8.8', + '--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.1', + '--allocation-pool', 'start=8.8.8.100,end=8.8.8.156', + self._testsubnet.name, + ] + verifylist = [ + ('dns_nameservers', ['8.8.8.8']), + ('host_routes', [{ + "destination": "10.30.30.30/24", "gateway": "10.30.30.1"}]), + ('allocation_pools', [{ + 'start': '8.8.8.100', 'end': '8.8.8.156'}]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) + + def test_unset_subnet_wrong_dns_nameservers(self): + arglist = [ + '--dns-nameserver', '8.8.8.1', + '--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.1', + '--allocation-pool', 'start=8.8.8.100,end=8.8.8.150', + self._testsubnet.name, + ] + verifylist = [ + ('dns_nameservers', ['8.8.8.1']), + ('host_routes', [{ + "destination": "10.30.30.30/24", "gateway": "10.30.30.1"}]), + ('allocation_pools', [{ + 'start': '8.8.8.100', 'end': '8.8.8.150'}]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) diff --git a/openstackclient/tests/volume/v1/test_qos_specs.py b/openstackclient/tests/volume/v1/test_qos_specs.py index 392017c6..4e1733fd 100644 --- a/openstackclient/tests/volume/v1/test_qos_specs.py +++ b/openstackclient/tests/volume/v1/test_qos_specs.py @@ -211,7 +211,7 @@ class TestQosDelete(TestQos): result = self.cmd.take_action(parsed_args) - self.qos_mock.delete.assert_called_with(volume_fakes.qos_id) + self.qos_mock.delete.assert_called_with(volume_fakes.qos_id, False) self.assertIsNone(result) def test_qos_delete_with_name(self): @@ -225,7 +225,23 @@ class TestQosDelete(TestQos): result = self.cmd.take_action(parsed_args) - self.qos_mock.delete.assert_called_with(volume_fakes.qos_id) + self.qos_mock.delete.assert_called_with(volume_fakes.qos_id, False) + self.assertIsNone(result) + + def test_qos_delete_with_force(self): + arglist = [ + '--force', + volume_fakes.qos_id + ] + verifylist = [ + ('force', True), + ('qos_specs', [volume_fakes.qos_id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.qos_mock.delete.assert_called_with(volume_fakes.qos_id, True) self.assertIsNone(result) diff --git a/openstackclient/tests/volume/v2/test_backup.py b/openstackclient/tests/volume/v2/test_backup.py index 8a151a91..ba0f1c18 100644 --- a/openstackclient/tests/volume/v2/test_backup.py +++ b/openstackclient/tests/volume/v2/test_backup.py @@ -72,12 +72,14 @@ class TestBackupCreate(TestBackup): "--name", self.new_backup.name, "--description", self.new_backup.description, "--container", self.new_backup.container, + "--force", self.new_backup.volume_id, ] verifylist = [ ("name", self.new_backup.name), ("description", self.new_backup.description), ("container", self.new_backup.container), + ("force", True), ("volume", self.new_backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -88,7 +90,8 @@ class TestBackupCreate(TestBackup): self.new_backup.volume_id, container=self.new_backup.container, name=self.new_backup.name, - description=self.new_backup.description + description=self.new_backup.description, + force=True, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -112,7 +115,8 @@ class TestBackupCreate(TestBackup): self.new_backup.volume_id, container=self.new_backup.container, name=None, - description=self.new_backup.description + description=self.new_backup.description, + force=False, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/volume/v2/test_qos_specs.py b/openstackclient/tests/volume/v2/test_qos_specs.py index 11047535..92ffca74 100644 --- a/openstackclient/tests/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/volume/v2/test_qos_specs.py @@ -175,7 +175,23 @@ class TestQosDelete(TestQos): result = self.cmd.take_action(parsed_args) - self.qos_mock.delete.assert_called_with(self.qos_spec.id) + self.qos_mock.delete.assert_called_with(self.qos_spec.id, False) + self.assertIsNone(result) + + def test_qos_delete_with_force(self): + arglist = [ + '--force', + self.qos_spec.id + ] + verifylist = [ + ('force', True), + ('qos_specs', [self.qos_spec.id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.qos_mock.delete.assert_called_with(self.qos_spec.id, True) self.assertIsNone(result) diff --git a/openstackclient/volume/v1/backup.py b/openstackclient/volume/v1/backup.py index 212c74ef..5f34a2c5 100644 --- a/openstackclient/volume/v1/backup.py +++ b/openstackclient/volume/v1/backup.py @@ -76,7 +76,7 @@ class DeleteBackup(command.Command): 'backups', metavar='<backup>', nargs="+", - help=_('Backup(s) to delete (ID only)'), + help=_('Backup(s) to delete (name or ID)'), ) return parser @@ -150,7 +150,7 @@ class RestoreBackup(command.Command): parser.add_argument( 'backup', metavar='<backup>', - help=_('Backup to restore (ID only)') + help=_('Backup to restore (name or ID)') ) parser.add_argument( 'volume', @@ -177,7 +177,7 @@ class ShowBackup(command.ShowOne): parser.add_argument( 'backup', metavar='<backup>', - help=_('Backup to display (ID only)') + help=_('Backup to display (name or ID)') ) return parser diff --git a/openstackclient/volume/v1/qos_specs.py b/openstackclient/volume/v1/qos_specs.py index 56a96256..c5850871 100644 --- a/openstackclient/volume/v1/qos_specs.py +++ b/openstackclient/volume/v1/qos_specs.py @@ -103,13 +103,19 @@ class DeleteQos(command.Command): nargs="+", help=_('QoS specification(s) to delete (name or ID)'), ) + parser.add_argument( + '--force', + action='store_true', + default=False, + help=_("Allow to delete in-use QoS specification(s)") + ) return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume for qos in parsed_args.qos_specs: qos_spec = utils.find_resource(volume_client.qos_specs, qos) - volume_client.qos_specs.delete(qos_spec.id) + volume_client.qos_specs.delete(qos_spec.id, parsed_args.force) class DisassociateQos(command.Command): diff --git a/openstackclient/volume/v2/backup.py b/openstackclient/volume/v2/backup.py index 6a44e30c..519913a9 100644 --- a/openstackclient/volume/v2/backup.py +++ b/openstackclient/volume/v2/backup.py @@ -48,6 +48,12 @@ class CreateBackup(command.ShowOne): metavar="<container>", help=_("Optional backup container name") ) + parser.add_argument( + '--force', + action='store_true', + default=False, + help=_("Allow to back up an in-use volume") + ) return parser def take_action(self, parsed_args): @@ -58,7 +64,8 @@ class CreateBackup(command.ShowOne): volume_id, container=parsed_args.container, name=parsed_args.name, - description=parsed_args.description + description=parsed_args.description, + force=parsed_args.force, ) backup._info.pop("links", None) return zip(*sorted(six.iteritems(backup._info))) @@ -147,7 +154,7 @@ class RestoreBackup(command.ShowOne): parser.add_argument( "backup", metavar="<backup>", - help=_("Backup to restore (ID only)") + help=_("Backup to restore (name or ID)") ) parser.add_argument( "volume", diff --git a/openstackclient/volume/v2/qos_specs.py b/openstackclient/volume/v2/qos_specs.py index 7ec272b3..5ed1225b 100644 --- a/openstackclient/volume/v2/qos_specs.py +++ b/openstackclient/volume/v2/qos_specs.py @@ -103,13 +103,19 @@ class DeleteQos(command.Command): nargs="+", help=_('QoS specification(s) to delete (name or ID)'), ) + parser.add_argument( + '--force', + action='store_true', + default=False, + help=_("Allow to delete in-use QoS specification(s)") + ) return parser def take_action(self, parsed_args): volume_client = self.app.client_manager.volume for qos in parsed_args.qos_specs: qos_spec = utils.find_resource(volume_client.qos_specs, qos) - volume_client.qos_specs.delete(qos_spec.id) + volume_client.qos_specs.delete(qos_spec.id, parsed_args.force) class DisassociateQos(command.Command): diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index e54395fa..be2388fb 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -457,6 +457,3 @@ class UnsetVolume(command.Command): if parsed_args.image_property: volume_client.volumes.delete_image_metadata( volume.id, parsed_args.image_property) - - if (not parsed_args.image_property and not parsed_args.property): - LOG.error(_("No changes requested")) |
