diff --git a/neutronclient/osc/v2/networking_bgpvpn/router_association.py b/neutronclient/osc/v2/networking_bgpvpn/router_association.py index 40664b6dc..c382f1621 100644 --- a/neutronclient/osc/v2/networking_bgpvpn/router_association.py +++ b/neutronclient/osc/v2/networking_bgpvpn/router_association.py @@ -24,8 +24,12 @@ from neutronclient.osc.v2.networking_bgpvpn.resource_association import\ DeleteBgpvpnResAssoc from neutronclient.osc.v2.networking_bgpvpn.resource_association import\ ListBgpvpnResAssoc +from neutronclient.osc.v2.networking_bgpvpn.resource_association import\ + SetBgpvpnResAssoc from neutronclient.osc.v2.networking_bgpvpn.resource_association import\ ShowBgpvpnResAssoc +from neutronclient.osc.v2.networking_bgpvpn.resource_association import\ + UnsetBgpvpnResAssoc class BgpvpnRouterAssoc(object): @@ -38,15 +42,62 @@ class BgpvpnRouterAssoc(object): ('tenant_id', 'Project', column_util.LIST_LONG_ONLY), ('%s_id' % _assoc_res_name, '%s ID' % _assoc_res_name.capitalize(), column_util.LIST_BOTH), + ('advertise_extra_routes', 'Advertise extra routes', + column_util.LIST_LONG_ONLY), ) _formatters = {} + def _get_common_parser(self, parser): + """Adds to parser arguments common to create, set and unset commands. + + :params ArgumentParser parser: argparse object contains all command's + arguments + """ + ADVERTISE_ROUTES = _("Routes will be advertised to the " + "BGP VPN%s") % ( + _(' (default)') if self._action == 'create' + else "") + NOT_ADVERTISE_ROUTES = _("Routes from the router will not be " + "advertised to the BGP VPN") + + group_advertise_extra_routes = parser.add_mutually_exclusive_group() + group_advertise_extra_routes.add_argument( + '--advertise_extra_routes', + action='store_true', + help=NOT_ADVERTISE_ROUTES if self._action == 'unset' + else ADVERTISE_ROUTES, + ) + group_advertise_extra_routes.add_argument( + '--no-advertise_extra_routes', + action='store_true', + help=ADVERTISE_ROUTES if self._action == 'unset' + else NOT_ADVERTISE_ROUTES, + ) + + def _args2body(self, _, args): + attrs = {} + + if args.advertise_extra_routes: + attrs['advertise_extra_routes'] = self._action != 'unset' + elif args.no_advertise_extra_routes: + attrs['advertise_extra_routes'] = self._action == 'unset' + + return {self._resource: attrs} + class CreateBgpvpnRouterAssoc(BgpvpnRouterAssoc, CreateBgpvpnResAssoc): _description = _("Create a BGP VPN router association") pass +class SetBgpvpnRouterAssoc(BgpvpnRouterAssoc, SetBgpvpnResAssoc): + _description = _("Set BGP VPN router association properties") + + +class UnsetBgpvpnRouterAssoc(BgpvpnRouterAssoc, UnsetBgpvpnResAssoc): + _description = _("Unset BGP VPN router association properties") + + class DeleteBgpvpnRouterAssoc(BgpvpnRouterAssoc, DeleteBgpvpnResAssoc): _description = _("Delete a BGP VPN router association(s) for a given BGP " "VPN") diff --git a/neutronclient/tests/unit/osc/v2/networking_bgpvpn/fakes.py b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/fakes.py index 21b9a762b..b3a538f5a 100644 --- a/neutronclient/tests/unit/osc/v2/networking_bgpvpn/fakes.py +++ b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/fakes.py @@ -33,6 +33,12 @@ from neutronclient.osc.v2.networking_bgpvpn.resource_association import\ ShowBgpvpnResAssoc from neutronclient.osc.v2.networking_bgpvpn.resource_association import\ UnsetBgpvpnResAssoc +from neutronclient.osc.v2.networking_bgpvpn.router_association import\ + CreateBgpvpnRouterAssoc +from neutronclient.osc.v2.networking_bgpvpn.router_association import\ + SetBgpvpnRouterAssoc +from neutronclient.osc.v2.networking_bgpvpn.router_association import\ + ShowBgpvpnRouterAssoc from neutronclient.tests.unit.osc.v2 import fakes as test_fakes @@ -139,6 +145,35 @@ class ShowBgpvpnFakeResAssoc(BgpvpnFakeAssoc, ShowBgpvpnResAssoc): pass +class BgpvpnFakeRouterAssoc(object): + _assoc_res_name = 'fake_resource' + _resource = '%s_association' % _assoc_res_name + _resource_plural = '%ss' % _resource + + _attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('tenant_id', 'Project', column_util.LIST_LONG_ONLY), + ('%s_id' % _assoc_res_name, '%s ID' % _assoc_res_name.capitalize(), + column_util.LIST_BOTH), + ('advertise_extra_routes', 'Advertise extra routes', + column_util.LIST_LONG_ONLY), + ) + _formatters = {} + + +class CreateBgpvpnFakeRouterAssoc(BgpvpnFakeRouterAssoc, + CreateBgpvpnRouterAssoc): + pass + + +class SetBgpvpnFakeRouterAssoc(BgpvpnFakeRouterAssoc, SetBgpvpnRouterAssoc): + pass + + +class ShowBgpvpnFakeRouterAssoc(BgpvpnFakeRouterAssoc, ShowBgpvpnRouterAssoc): + pass + + class FakeResource(object): """Fake resource with minimal attributes.""" @@ -177,14 +212,19 @@ class FakeResAssoc(object): """Fake resource association with minimal attributes.""" @staticmethod - def create_one_resource_association(resource): + def create_one_resource_association(resource, attrs=None): """Create a fake resource association.""" + attrs = attrs or {} + res_assoc_attrs = { 'id': 'fake_association_id', 'tenant_id': resource['tenant_id'], 'fake_resource_id': resource['id'], } + + # Overwrite default attributes. + res_assoc_attrs.update(attrs) return copy.deepcopy(res_assoc_attrs) @staticmethod diff --git a/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_router_association.py b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_router_association.py new file mode 100644 index 000000000..fb17bdb67 --- /dev/null +++ b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_router_association.py @@ -0,0 +1,291 @@ +# Copyright (c) 2018 Orange SA. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy +import operator + +import mock +from osc_lib.tests.utils import ParserException +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from neutronclient.tests.unit.osc.v2.networking_bgpvpn import fakes + + +columns_short = tuple(col for col, _, listing_mode + in fakes.BgpvpnFakeRouterAssoc._attr_map + if listing_mode in (column_util.LIST_BOTH, + column_util.LIST_SHORT_ONLY)) +columns_long = tuple(col for col, _, listing_mode + in fakes.BgpvpnFakeRouterAssoc._attr_map + if listing_mode in (column_util.LIST_BOTH, + column_util.LIST_LONG_ONLY)) +headers_short = tuple(head for _, head, listing_mode + in fakes.BgpvpnFakeRouterAssoc._attr_map + if listing_mode in (column_util.LIST_BOTH, + column_util.LIST_SHORT_ONLY)) +headers_long = tuple(head for _, head, listing_mode + in fakes.BgpvpnFakeRouterAssoc._attr_map + if listing_mode in (column_util.LIST_BOTH, + column_util.LIST_LONG_ONLY)) +sorted_attr_map = sorted(fakes.BgpvpnFakeRouterAssoc._attr_map, + key=operator.itemgetter(1)) +sorted_columns = tuple(col for col, _, _ in sorted_attr_map) +sorted_headers = tuple(head for _, head, _ in sorted_attr_map) + + +def _get_data(attrs, columns=sorted_columns): + return osc_utils.get_dict_properties( + attrs, columns, formatters=fakes.BgpvpnFakeAssoc._formatters) + + +class TestCreateRouterAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super(TestCreateRouterAssoc, self).setUp() + self.cmd = fakes.CreateBgpvpnFakeRouterAssoc(self.app, self.namespace) + self.fake_bgpvpn = fakes.FakeBgpvpn.create_one_bgpvpn() + self.fake_router = fakes.FakeResource.create_one_resource() + + def _build_args(self, param=None): + arglist_base = [ + self.fake_bgpvpn['id'], + self.fake_router['id'], + '--project', self.fake_bgpvpn['tenant_id'] + ] + if param is not None: + if isinstance(param, list): + arglist_base.extend(param) + else: + arglist_base.append(param) + return arglist_base + + def _build_verify_list(self, param=None): + verifylist = [ + ('bgpvpn', self.fake_bgpvpn['id']), + ('resource', self.fake_router['id']), + ('project', self.fake_bgpvpn['tenant_id']) + ] + if param is not None: + verifylist.append(param) + return verifylist + + def _exec_create_router_association( + self, fake_res_assoc, arglist, verifylist): + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + cols, data = self.cmd.take_action(parsed_args) + + fake_res_assoc_call = copy.deepcopy(fake_res_assoc) + fake_res_assoc_call.pop('id') + + self.neutronclient.create_bgpvpn_fake_resource_assoc.\ + assert_called_once_with( + self.fake_bgpvpn['id'], + {fakes.BgpvpnFakeRouterAssoc._resource: fake_res_assoc_call}) + return cols, data + + def test_create_router_association(self): + fake_res_assoc = fakes.FakeResAssoc.create_one_resource_association( + self.fake_router) + + self.neutronclient.create_bgpvpn_fake_resource_assoc = mock.Mock( + return_value={ + fakes.BgpvpnFakeRouterAssoc._resource: fake_res_assoc, + 'advertise_extra_routes': True}) + + arglist = self._build_args() + # advertise_extra_routes will be False since none + # of the mutually exclusive args present + verifylist = self._build_verify_list(('advertise_extra_routes', False)) + + self._exec_create_router_association( + fake_res_assoc, arglist, verifylist) + + def test_create_router_association_advertise(self): + fake_res_assoc = fakes.FakeResAssoc.create_one_resource_association( + self.fake_router, + {'advertise_extra_routes': True}) + + self.neutronclient.create_bgpvpn_fake_resource_assoc = mock.Mock( + return_value={ + fakes.BgpvpnFakeRouterAssoc._resource: fake_res_assoc}) + + arglist = self._build_args('--advertise_extra_routes') + verifylist = self._build_verify_list(('advertise_extra_routes', True)) + + cols, data = self._exec_create_router_association( + fake_res_assoc, arglist, verifylist) + self.assertEqual(sorted_headers, cols) + self.assertEqual(_get_data(fake_res_assoc), data) + + def test_create_router_association_no_advertise(self): + fake_res_assoc = fakes.FakeResAssoc.create_one_resource_association( + self.fake_router, + {'advertise_extra_routes': False}) + + self.neutronclient.create_bgpvpn_fake_resource_assoc = mock.Mock( + return_value={ + fakes.BgpvpnFakeRouterAssoc._resource: fake_res_assoc}) + + arglist = self._build_args('--no-advertise_extra_routes') + verifylist = self._build_verify_list(('advertise_extra_routes', False)) + + cols, data = self._exec_create_router_association( + fake_res_assoc, arglist, verifylist) + self.assertEqual(sorted_headers, cols) + self.assertEqual(_get_data(fake_res_assoc), data) + + def test_create_router_association_advertise_fault(self): + arglist = self._build_args( + ['--advertise_extra_routes', '--no-advertise_extra_routes']) + + try: + self._exec_create_router_association(None, arglist, None) + except ParserException as e: + self.assertEqual(format(e), 'Argument parse failed') + + def test_router_association_unknown_arg(self): + arglist = self._build_args('--unknown arg') + + try: + self._exec_create_router_association(None, arglist, None) + except ParserException as e: + self.assertEqual(format(e), 'Argument parse failed') + + +class TestSetRouterAssoc(fakes.TestNeutronClientBgpvpn): + + def setUp(self): + super(TestSetRouterAssoc, self).setUp() + self.cmd = fakes.SetBgpvpnFakeRouterAssoc(self.app, self.namespace) + self.fake_bgpvpn = fakes.FakeBgpvpn.create_one_bgpvpn() + self.fake_router = fakes.FakeResource.create_one_resource() + + def _build_args(self, fake_res_assoc, param=None): + arglist_base = [ + fake_res_assoc['id'], + self.fake_bgpvpn['id'] + ] + if param is not None: + if isinstance(param, list): + arglist_base.extend(param) + else: + arglist_base.append(param) + return arglist_base + + def _build_verify_list(self, fake_res_assoc, param=None): + verifylist = [ + ('resource_association_id', fake_res_assoc['id']), + ('bgpvpn', self.fake_bgpvpn['id']) + ] + if param is not None: + verifylist.append(param) + return verifylist + + def test_set_router_association_no_advertise(self): + fake_res_assoc = fakes.FakeResAssoc.create_one_resource_association( + self.fake_router, + {'advertise_extra_routes': True}) + self.neutronclient.update_bgpvpn_fake_resource_assoc = mock.Mock() + + arglist = self._build_args( + fake_res_assoc, + '--no-advertise_extra_routes') + verifylist = [ + ('resource_association_id', fake_res_assoc['id']), + ('bgpvpn', self.fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + fake_res_assoc_call = copy.deepcopy(fake_res_assoc) + fake_res_assoc_call.pop('id') + + self.neutronclient.update_bgpvpn_fake_resource_assoc.\ + assert_called_once_with( + self.fake_bgpvpn['id'], + fake_res_assoc['id'], + { + fakes.BgpvpnFakeRouterAssoc._resource: { + 'advertise_extra_routes': False + } + }) + self.assertIsNone(result) + + def test_set_router_association_advertise(self): + fake_res_assoc = fakes.FakeResAssoc.create_one_resource_association( + self.fake_router, + {'advertise_extra_routes': False}) + self.neutronclient.update_bgpvpn_fake_resource_assoc = mock.Mock() + + arglist = self._build_args( + fake_res_assoc, + '--advertise_extra_routes') + verifylist = [ + ('resource_association_id', fake_res_assoc['id']), + ('bgpvpn', self.fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + fake_res_assoc_call = copy.deepcopy(fake_res_assoc) + fake_res_assoc_call.pop('id') + + self.neutronclient.update_bgpvpn_fake_resource_assoc.\ + assert_called_once_with( + self.fake_bgpvpn['id'], + fake_res_assoc['id'], + { + fakes.BgpvpnFakeRouterAssoc._resource: { + 'advertise_extra_routes': True + } + }) + self.assertIsNone(result) + + +class TestShowRouterAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super(TestShowRouterAssoc, self).setUp() + self.cmd = fakes.ShowBgpvpnFakeRouterAssoc(self.app, self.namespace) + + def test_show_router_association(self): + fake_bgpvpn = fakes.FakeBgpvpn.create_one_bgpvpn() + fake_res = fakes.FakeResource.create_one_resource() + fake_res_assoc = fakes.FakeResAssoc.create_one_resource_association( + fake_res, + {'advertise_extra_routes': True}) + self.neutronclient.show_bgpvpn_fake_resource_assoc = mock.Mock( + return_value={fakes.BgpvpnFakeAssoc._resource: fake_res_assoc}) + arglist = [ + fake_res_assoc['id'], + fake_bgpvpn['id'], + ] + verifylist = [ + ('resource_association_id', fake_res_assoc['id']), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, data = self.cmd.take_action(parsed_args) + + self.neutronclient.show_bgpvpn_fake_resource_assoc.\ + assert_called_once_with(fake_bgpvpn['id'], fake_res_assoc['id']) + self.assertEqual(sorted_headers, headers) + self.assertEqual(data, _get_data(fake_res_assoc)) diff --git a/releasenotes/notes/support-routes-advertise-9356a38cf3e2fe5a.yaml b/releasenotes/notes/support-routes-advertise-9356a38cf3e2fe5a.yaml new file mode 100644 index 000000000..f1dbd5ed0 --- /dev/null +++ b/releasenotes/notes/support-routes-advertise-9356a38cf3e2fe5a.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add optional flag to control the advertisement in BGPVPNs + of the routes defined on a Router resource + (``bgpvpn-routes-control`` API extension). diff --git a/setup.cfg b/setup.cfg index 41857c7a6..d9938af5a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -123,7 +123,9 @@ openstack.neutronclient.v2 = bgpvpn_router_association_create = neutronclient.osc.v2.networking_bgpvpn.router_association:CreateBgpvpnRouterAssoc bgpvpn_router_association_delete = neutronclient.osc.v2.networking_bgpvpn.router_association:DeleteBgpvpnRouterAssoc bgpvpn_router_association_list = neutronclient.osc.v2.networking_bgpvpn.router_association:ListBgpvpnRouterAssoc + bgpvpn_router_association_set = neutronclient.osc.v2.networking_bgpvpn.router_association:SetBgpvpnRouterAssoc bgpvpn_router_association_show = neutronclient.osc.v2.networking_bgpvpn.router_association:ShowBgpvpnRouterAssoc + bgpvpn_router_association_unset = neutronclient.osc.v2.networking_bgpvpn.router_association:UnsetBgpvpnRouterAssoc bgpvpn_port_association_create = neutronclient.osc.v2.networking_bgpvpn.port_association:CreateBgpvpnPortAssoc bgpvpn_port_association_set = neutronclient.osc.v2.networking_bgpvpn.port_association:SetBgpvpnPortAssoc bgpvpn_port_association_unset = neutronclient.osc.v2.networking_bgpvpn.port_association:UnsetBgpvpnPortAssoc