From 7184e876a5b79ea226c75a267af2dfca7c011f62 Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Thu, 26 Oct 2023 11:50:55 +0200 Subject: [PATCH] Add router default route BFD/ECMP options Add the `--enable-default-route-bfd`, `--disable-default-route-bfd` `--enable-default-route-ecmp` and `--disable-default-route-ecmp` options for `router create` and `router set` commands. Related-Bug: #2002687 Signed-off-by: Frode Nordahl Change-Id: Ia5a196daa87d29445dc5514dcb91544f9d470795 --- openstackclient/network/v2/router.py | 69 +++++++++++++++++++ .../tests/unit/network/v2/test_router.py | 68 ++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 52b7d25fc..51e2aba42 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -231,9 +231,72 @@ def _get_attrs(client_manager, parsed_args): if 'flavor_id' in parsed_args and parsed_args.flavor_id is not None: attrs['flavor_id'] = parsed_args.flavor_id + for attr in ('enable_default_route_bfd', 'enable_default_route_ecmp'): + value = getattr(parsed_args, attr, None) + if value is not None: + attrs[attr] = value + return attrs +def _parser_add_bfd_ecmp_arguments(parser): + """Helper to add BFD and ECMP args for CreateRouter and SetRouter.""" + parser.add_argument( + '--enable-default-route-bfd', + dest='enable_default_route_bfd', + default=None, + action='store_true', + help=_( + "Enable BFD sessions for default routes inferred from " + "the external gateway port subnets for this router." + ), + ) + parser.add_argument( + '--disable-default-route-bfd', + dest='enable_default_route_bfd', + default=None, + action='store_false', + help=_( + "Disable BFD sessions for default routes inferred from " + "the external gateway port subnets for this router." + ), + ) + parser.add_argument( + '--enable-default-route-ecmp', + dest='enable_default_route_ecmp', + default=None, + action='store_true', + help=_( + "Add ECMP default routes if multiple are available via " + "different gateway ports." + ), + ) + parser.add_argument( + '--disable-default-route-ecmp', + dest='enable_default_route_ecmp', + default=None, + action='store_false', + help=_("Add default route only for first gateway port."), + ) + + +def _command_check_bfd_ecmp_supported(attrs, client): + """Helper to check for server side support when bfd/ecmp attrs provided. + + :raises: exceptions.CommandError + """ + if ( + 'enable_default_route_bfd' in attrs + or 'enable_default_route_ecmp' in attrs + ) and not is_multiple_gateways_supported(client): + msg = _( + 'The external-gateway-multihoming extension is not enabled at ' + 'the Neutron side, cannot use --enable-default-route-bfd or ' + '--enable-default-route-ecmp arguments.' + ) + raise exceptions.CommandError(msg) + + class AddPortToRouter(command.Command): _description = _("Add a port to a router") @@ -502,6 +565,7 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): metavar='', help=_("Associate the router to a flavor by ID"), ) + _parser_add_bfd_ecmp_arguments(parser) return parser @@ -527,6 +591,8 @@ class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): if parsed_args.enable_ndp_proxy is not None: attrs['enable_ndp_proxy'] = parsed_args.enable_ndp_proxy + _command_check_bfd_ecmp_supported(attrs, client) + external_gateways = attrs.pop('external_gateways', None) obj = client.create_router(**attrs) # tags cannot be set when created, so tags need to be set later. @@ -943,6 +1009,7 @@ class SetRouter(common.NeutronCommandWithExtraArgs): help=_("Remove QoS policy from router gateway IPs"), ) _tag.add_tag_option_to_parser_for_set(parser, _('router')) + _parser_add_bfd_ecmp_arguments(parser) return parser def take_action(self, parsed_args): @@ -1015,6 +1082,8 @@ class SetRouter(common.NeutronCommandWithExtraArgs): if parsed_args.enable_ndp_proxy is not None: attrs['enable_ndp_proxy'] = parsed_args.enable_ndp_proxy + _command_check_bfd_ecmp_supported(attrs, client) + if attrs: external_gateways = attrs.pop('external_gateways', None) client.update_router(obj, **attrs) diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 33956b3ec..256fd9b70 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -410,6 +410,74 @@ class TestCreateRouter(TestRouter): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) + def test_create_with_enable_default_route_bfd(self): + self.network_client.find_extension = mock.Mock( + return_value=network_fakes.create_one_extension( + attrs={'name': 'external-gateway-multihoming'} + ) + ) + arglist = [self.new_router.name, '--enable-default-route-bfd'] + verifylist = [ + ('name', self.new_router.name), + ('enable_default_route_bfd', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.network_client.create_router.assert_called_once_with( + name=self.new_router.name, + admin_state_up=True, + enable_default_route_bfd=True, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_create_with_enable_default_route_bfd_no_extension(self): + arglist = [self.new_router.name, '--enable-default-route-bfd'] + verifylist = [ + ('name', self.new_router.name), + ('enable_default_route_bfd', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_create_with_enable_default_route_ecmp(self): + self.network_client.find_extension = mock.Mock( + return_value=network_fakes.create_one_extension( + attrs={'name': 'external-gateway-multihoming'} + ) + ) + arglist = [self.new_router.name, '--enable-default-route-ecmp'] + verifylist = [ + ('name', self.new_router.name), + ('enable_default_route_ecmp', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.network_client.create_router.assert_called_once_with( + name=self.new_router.name, + admin_state_up=True, + enable_default_route_ecmp=True, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_create_with_enable_default_route_ecmp_no_extension(self): + arglist = [self.new_router.name, '--enable-default-route-ecmp'] + verifylist = [ + ('name', self.new_router.name), + ('enable_default_route_ecmp', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + class TestDeleteRouter(TestRouter): # The routers to delete.