diff options
| author | Bence Romsics <bence.romsics@gmail.com> | 2019-07-31 15:44:27 +0200 |
|---|---|---|
| committer | Bence Romsics <bence.romsics@gmail.com> | 2020-03-30 16:19:05 +0200 |
| commit | dba57c85d5536369d600ae98599c08b921713bd5 (patch) | |
| tree | 16607cbb4c70c312d166ffd91b785b3d3aaf74cf /openstackclient | |
| parent | c5719a12b5b84b2efd989030f17c0eacc9faf7ad (diff) | |
| download | python-openstackclient-dba57c85d5536369d600ae98599c08b921713bd5.tar.gz | |
Add command: router add/remove route --route
Add commands to osc to call the two new API methods introduced by
new Neutron extension: extraroute-atomic.
Bump our openstacksdk requirement to >=0.38.0 which contains
the corresponding sdk change.
The lower-constraints of dogpile.cache and keystoneauth1 are bumped
because of requirements bumps in openstacksdk.
The lower-constraint of decorator is bumped because of problem already
fixed by amotoki here: https://review.opendev.org/701706
Change-Id: Ia9b9c216f1d1161ebedac31594a2c464d77f4ae2
Depends-On: https://review.opendev.org/674324
Partial-Bug: #1826396 (rfe)
Related-Change: https://review.opendev.org/655680 (spec)
Diffstat (limited to 'openstackclient')
| -rw-r--r-- | openstackclient/network/v2/router.py | 97 | ||||
| -rw-r--r-- | openstackclient/tests/functional/network/v2/test_router.py | 43 | ||||
| -rw-r--r-- | openstackclient/tests/unit/network/v2/test_router.py | 140 |
3 files changed, 277 insertions, 3 deletions
diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 464dbbec..81b81f98 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -168,6 +168,93 @@ class AddSubnetToRouter(command.Command): subnet_id=subnet.id) +class AddExtraRoutesToRouter(command.ShowOne): + _description = _("Add extra static routes to a router's routing table.") + + def get_parser(self, prog_name): + parser = super(AddExtraRoutesToRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='<router>', + help=_("Router to which extra static routes " + "will be added (name or ID).") + ) + parser.add_argument( + '--route', + metavar='destination=<subnet>,gateway=<ip-address>', + action=parseractions.MultiKeyValueAction, + dest='routes', + default=[], + required_keys=['destination', 'gateway'], + help=_("Add extra static route to the router. " + "destination: destination subnet (in CIDR notation), " + "gateway: nexthop IP address. " + "Repeat option to add multiple routes. " + "Trying to add a route that's already present " + "(exactly, including destination and nexthop) " + "in the routing table is allowed and is considered " + "a successful operation.") + ) + return parser + + def take_action(self, parsed_args): + if parsed_args.routes is not None: + for route in parsed_args.routes: + route['nexthop'] = route.pop('gateway') + client = self.app.client_manager.network + router_obj = client.add_extra_routes_to_router( + client.find_router(parsed_args.router, ignore_missing=False), + body={'router': {'routes': parsed_args.routes}}) + display_columns, columns = _get_columns(router_obj) + data = utils.get_item_properties( + router_obj, columns, formatters=_formatters) + return (display_columns, data) + + +class RemoveExtraRoutesFromRouter(command.ShowOne): + _description = _( + "Remove extra static routes from a router's routing table.") + + def get_parser(self, prog_name): + parser = super(RemoveExtraRoutesFromRouter, self).get_parser(prog_name) + parser.add_argument( + 'router', + metavar='<router>', + help=_("Router from which extra static routes " + "will be removed (name or ID).") + ) + parser.add_argument( + '--route', + metavar='destination=<subnet>,gateway=<ip-address>', + action=parseractions.MultiKeyValueAction, + dest='routes', + default=[], + required_keys=['destination', 'gateway'], + help=_("Remove extra static route from the router. " + "destination: destination subnet (in CIDR notation), " + "gateway: nexthop IP address. " + "Repeat option to remove multiple routes. " + "Trying to remove a route that's already missing " + "(fully, including destination and nexthop) " + "from the routing table is allowed and is considered " + "a successful operation.") + ) + return parser + + def take_action(self, parsed_args): + if parsed_args.routes is not None: + for route in parsed_args.routes: + route['nexthop'] = route.pop('gateway') + client = self.app.client_manager.network + router_obj = client.remove_extra_routes_from_router( + client.find_router(parsed_args.router, ignore_missing=False), + body={'router': {'routes': parsed_args.routes}}) + display_columns, columns = _get_columns(router_obj) + data = utils.get_item_properties( + router_obj, columns, formatters=_formatters) + return (display_columns, data) + + # TODO(yanxing'an): Use the SDK resource mapped attribute names once the # OSC minimum requirements include SDK 1.0. class CreateRouter(command.ShowOne): @@ -540,17 +627,21 @@ class SetRouter(command.Command): dest='routes', default=None, required_keys=['destination', 'gateway'], - help=_("Routes associated with the router " + help=_("Add routes to the router " "destination: destination subnet (in CIDR notation) " "gateway: nexthop IP address " - "(repeat option to set multiple routes)") + "(repeat option to add multiple routes). " + "This is deprecated in favor of 'router add/remove route' " + "since it is prone to race conditions between concurrent " + "clients when not used together with --no-route to " + "overwrite the current value of 'routes'.") ) parser.add_argument( '--no-route', action='store_true', help=_("Clear routes associated with the router. " "Specify both --route and --no-route to overwrite " - "current value of route.") + "current value of routes.") ) routes_ha = parser.add_mutually_exclusive_group() routes_ha.add_argument( diff --git a/openstackclient/tests/functional/network/v2/test_router.py b/openstackclient/tests/functional/network/v2/test_router.py index 05aad7a0..0769dca6 100644 --- a/openstackclient/tests/functional/network/v2/test_router.py +++ b/openstackclient/tests/functional/network/v2/test_router.py @@ -261,3 +261,46 @@ class RouterTests(common.NetworkTagTests): new_name )) self.assertIsNone(cmd_output["external_gateway_info"]) + + def test_router_add_remove_route(self): + network_name = uuid.uuid4().hex + subnet_name = uuid.uuid4().hex + router_name = uuid.uuid4().hex + + self.openstack('network create %s' % network_name) + self.addCleanup(self.openstack, 'network delete %s' % network_name) + + self.openstack( + 'subnet create %s ' + '--network %s --subnet-range 10.0.0.0/24' % ( + subnet_name, network_name)) + + self.openstack('router create %s' % router_name) + self.addCleanup(self.openstack, 'router delete %s' % router_name) + + self.openstack('router add subnet %s %s' % (router_name, subnet_name)) + self.addCleanup(self.openstack, 'router remove subnet %s %s' % ( + router_name, subnet_name)) + + out1 = json.loads(self.openstack( + 'router add route -f json %s ' + '--route destination=10.0.10.0/24,gateway=10.0.0.10' % + router_name)), + self.assertEqual(1, len(out1[0]['routes'])) + + self.addCleanup( + self.openstack, 'router set %s --no-route' % router_name) + + out2 = json.loads(self.openstack( + 'router add route -f json %s ' + '--route destination=10.0.10.0/24,gateway=10.0.0.10 ' + '--route destination=10.0.11.0/24,gateway=10.0.0.11' % + router_name)), + self.assertEqual(2, len(out2[0]['routes'])) + + out3 = json.loads(self.openstack( + 'router remove route -f json %s ' + '--route destination=10.0.11.0/24,gateway=10.0.0.11 ' + '--route destination=10.0.12.0/24,gateway=10.0.0.12' % + router_name)), + self.assertEqual(1, len(out3[0]['routes'])) diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 38861b0a..09b4957c 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -776,6 +776,146 @@ class TestRemoveSubnetFromRouter(TestRouter): self.assertIsNone(result) +class TestAddExtraRoutesToRouter(TestRouter): + + _router = network_fakes.FakeRouter.create_one_router() + + def setUp(self): + super(TestAddExtraRoutesToRouter, self).setUp() + self.network.add_extra_routes_to_router = mock.Mock( + return_value=self._router) + self.cmd = router.AddExtraRoutesToRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + + def test_add_no_extra_route(self): + arglist = [ + self._router.id, + ] + verifylist = [ + ('router', self._router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.add_extra_routes_to_router.assert_called_with( + self._router, body={'router': {'routes': []}}) + self.assertEqual(2, len(result)) + + def test_add_one_extra_route(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.add_extra_routes_to_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + ]}}) + self.assertEqual(2, len(result)) + + def test_add_multiple_extra_routes(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + '--route', 'destination=dst2,gateway=gw2', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [ + {'destination': 'dst1', 'gateway': 'gw1'}, + {'destination': 'dst2', 'gateway': 'gw2'}, + ]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.add_extra_routes_to_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + {'destination': 'dst2', 'nexthop': 'gw2'}, + ]}}) + self.assertEqual(2, len(result)) + + +class TestRemoveExtraRoutesFromRouter(TestRouter): + + _router = network_fakes.FakeRouter.create_one_router() + + def setUp(self): + super(TestRemoveExtraRoutesFromRouter, self).setUp() + self.network.remove_extra_routes_from_router = mock.Mock( + return_value=self._router) + self.cmd = router.RemoveExtraRoutesFromRouter(self.app, self.namespace) + self.network.find_router = mock.Mock(return_value=self._router) + + def test_remove_no_extra_route(self): + arglist = [ + self._router.id, + ] + verifylist = [ + ('router', self._router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.remove_extra_routes_from_router.assert_called_with( + self._router, body={'router': {'routes': []}}) + self.assertEqual(2, len(result)) + + def test_remove_one_extra_route(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [{'destination': 'dst1', 'gateway': 'gw1'}]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.remove_extra_routes_from_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + ]}}) + self.assertEqual(2, len(result)) + + def test_remove_multiple_extra_routes(self): + arglist = [ + self._router.id, + '--route', 'destination=dst1,gateway=gw1', + '--route', 'destination=dst2,gateway=gw2', + ] + verifylist = [ + ('router', self._router.id), + ('routes', [ + {'destination': 'dst1', 'gateway': 'gw1'}, + {'destination': 'dst2', 'gateway': 'gw2'}, + ]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.remove_extra_routes_from_router.assert_called_with( + self._router, body={'router': {'routes': [ + {'destination': 'dst1', 'nexthop': 'gw1'}, + {'destination': 'dst2', 'nexthop': 'gw2'}, + ]}}) + self.assertEqual(2, len(result)) + + class TestSetRouter(TestRouter): # The router to set. |
