diff --git a/lower-constraints.txt b/lower-constraints.txt index 1fa30674e1..83e19fe60f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -12,11 +12,11 @@ contextlib2==0.4.0 coverage==4.0 cryptography==2.1 debtcollector==1.2.0 -decorator==3.4.0 +decorator==4.4.1 deprecation==1.0 docker-pycreds==0.2.1 docker==2.4.2 -dogpile.cache==0.6.2 +dogpile.cache==0.6.5 eventlet==0.18.2 extras==1.0.0 fasteners==0.7.0 @@ -38,7 +38,7 @@ jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.16.0 +keystoneauth1==3.18.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.1.0 @@ -50,7 +50,7 @@ msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 -openstacksdk==0.36.0 +openstacksdk==0.38.0 os-client-config==1.28.0 os-service-types==1.7.0 os-testr==1.0.0 diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 464dbbec78..81b81f98b5 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='', + help=_("Router to which extra static routes " + "will be added (name or ID).") + ) + parser.add_argument( + '--route', + metavar='destination=,gateway=', + 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='', + help=_("Router from which extra static routes " + "will be removed (name or ID).") + ) + parser.add_argument( + '--route', + metavar='destination=,gateway=', + 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 05aad7a013..0769dca6ff 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 38861b0ad5..09b4957cce 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. diff --git a/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml b/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml new file mode 100644 index 0000000000..33b5ba7a89 --- /dev/null +++ b/releasenotes/notes/router-extraroute-atomic-d6d406ffb15695f2.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Add new commands ``router add route`` and ``router remove route`` to + support new Neutron extension: ``extraroute-atomic`` (see `Neutron RFE + `_). +deprecations: + - | + The use of ``router set --route`` to add extra routes next to already + existing extra routes is deprecated in favor of ``router add route + --route``, because ``router set --route`` if used from multiple clients + concurrently may lead to lost updates. diff --git a/requirements.txt b/requirements.txt index b17b6a5593..f7a12dae85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ six>=1.10.0 # MIT Babel!=2.4.0,>=2.3.4 # BSD cliff!=2.9.0,>=2.8.0 # Apache-2.0 -openstacksdk>=0.36.0 # Apache-2.0 +openstacksdk>=0.38.0 # Apache-2.0 osc-lib>=2.0.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 60caf5db1e..1e5e36b8c6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -480,11 +480,13 @@ openstack.network.v2 = port_unset = openstackclient.network.v2.port:UnsetPort router_add_port = openstackclient.network.v2.router:AddPortToRouter + router_add_route = openstackclient.network.v2.router:AddExtraRoutesToRouter router_add_subnet = openstackclient.network.v2.router:AddSubnetToRouter router_create = openstackclient.network.v2.router:CreateRouter router_delete = openstackclient.network.v2.router:DeleteRouter router_list = openstackclient.network.v2.router:ListRouter router_remove_port = openstackclient.network.v2.router:RemovePortFromRouter + router_remove_route = openstackclient.network.v2.router:RemoveExtraRoutesFromRouter router_remove_subnet = openstackclient.network.v2.router:RemoveSubnetFromRouter router_set = openstackclient.network.v2.router:SetRouter router_show = openstackclient.network.v2.router:ShowRouter