From 0e61b3ba1c54be7ae7a218513d30800723a26524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89douard=20Thuleau?= Date: Wed, 15 Nov 2017 10:16:00 +0100 Subject: [PATCH] Add commands to support BGP VPN route control new API extension Networking-bgpvpn API extension [0] brings new API calls. That patch adds command to OSC CLI to support them. It does not implement all the API extension, still missing new router attribute 'advertise_extra_routes' and the BGP VPN 'local_pref' attribute. [0] https://developer.openstack.org/api-ref/network/v2/index.html#port-associations Change-Id: I45a160b0caec534d517b103db91dc738c006978b Partially-Implements: blueprint routes-control Depends-On: I263e1ee6cf4e1a91be91a4a78f4a160f64d33cc6 --- doc/source/cli/osc/v2/networking-bgpvpn.rst | 3 + .../osc/v2/networking_bgpvpn/bgpvpn.py | 2 + .../osc/v2/networking_bgpvpn/constants.py | 4 + .../v2/networking_bgpvpn/port_association.py | 315 ++++++++++++++++++ .../networking_bgpvpn/resource_association.py | 79 ++++- .../networking_bgpvpn/router_association.py | 2 +- .../unit/osc/v2/networking_bgpvpn/fakes.py | 25 +- .../osc/v2/networking_bgpvpn/test_bgpvpn.py | 2 + .../test_resource_association.py | 31 ++ neutronclient/v2_0/client.py | 31 ++ ...bgpvpn-route-control-aeda3e698486f73b.yaml | 6 + setup.cfg | 6 + 12 files changed, 499 insertions(+), 7 deletions(-) create mode 100644 neutronclient/osc/v2/networking_bgpvpn/port_association.py create mode 100644 releasenotes/notes/support-bgpvpn-route-control-aeda3e698486f73b.yaml diff --git a/doc/source/cli/osc/v2/networking-bgpvpn.rst b/doc/source/cli/osc/v2/networking-bgpvpn.rst index 371e47287..46ee47e6e 100644 --- a/doc/source/cli/osc/v2/networking-bgpvpn.rst +++ b/doc/source/cli/osc/v2/networking-bgpvpn.rst @@ -32,3 +32,6 @@ Network v2 .. autoprogram-cliff:: openstack.neutronclient.v2 :command: bgpvpn router association * + +.. autoprogram-cliff:: openstack.neutronclient.v2 + :command: bgpvpn port association * diff --git a/neutronclient/osc/v2/networking_bgpvpn/bgpvpn.py b/neutronclient/osc/v2/networking_bgpvpn/bgpvpn.py index 99d9c4ea3..63b429137 100644 --- a/neutronclient/osc/v2/networking_bgpvpn/bgpvpn.py +++ b/neutronclient/osc/v2/networking_bgpvpn/bgpvpn.py @@ -40,6 +40,7 @@ _attr_map = ( nc_osc_utils.LIST_LONG_ONLY), ('networks', 'Associated Networks', nc_osc_utils.LIST_LONG_ONLY), ('routers', 'Associated Routers', nc_osc_utils.LIST_LONG_ONLY), + ('ports', 'Associated Ports', nc_osc_utils.LIST_LONG_ONLY), ('vni', 'VNI', nc_osc_utils.LIST_LONG_ONLY), ) _formatters = { @@ -49,6 +50,7 @@ _formatters = { 'route_distinguishers': format_columns.ListColumn, 'networks': format_columns.ListColumn, 'routers': format_columns.ListColumn, + 'ports': format_columns.ListColumn, } diff --git a/neutronclient/osc/v2/networking_bgpvpn/constants.py b/neutronclient/osc/v2/networking_bgpvpn/constants.py index 775721e9c..7de1329a6 100644 --- a/neutronclient/osc/v2/networking_bgpvpn/constants.py +++ b/neutronclient/osc/v2/networking_bgpvpn/constants.py @@ -24,3 +24,7 @@ NETWORK_ASSOCS = '%ss' % NETWORK_ASSOC ROUTER_RESOURCE_NAME = 'router' ROUTER_ASSOC = '%s_association' % ROUTER_RESOURCE_NAME ROUTER_ASSOCS = '%ss' % ROUTER_ASSOC + +PORT_RESOURCE_NAME = 'port' +PORT_ASSOC = '%s_association' % PORT_RESOURCE_NAME +PORT_ASSOCS = '%ss' % PORT_ASSOC diff --git a/neutronclient/osc/v2/networking_bgpvpn/port_association.py b/neutronclient/osc/v2/networking_bgpvpn/port_association.py new file mode 100644 index 000000000..75d5c0fd9 --- /dev/null +++ b/neutronclient/osc/v2/networking_bgpvpn/port_association.py @@ -0,0 +1,315 @@ +# Copyright (c) 2017 Juniper networks Inc. +# 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 logging + +from osc_lib.cli import format_columns +from osc_lib.cli import parseractions + +from neutronclient._i18n import _ +from neutronclient.osc import utils as nc_osc_utils +from neutronclient.osc.v2.networking_bgpvpn import constants +from neutronclient.osc.v2.networking_bgpvpn import resource_association + +LOG = logging.getLogger(__name__) + + +class BgpvpnPortAssoc(object): + _assoc_res_name = constants.PORT_RESOURCE_NAME + _resource = constants.PORT_ASSOC + _resource_plural = constants.PORT_ASSOCS + + _attr_map = ( + ('id', 'ID', nc_osc_utils.LIST_BOTH), + ('tenant_id', 'Project', nc_osc_utils.LIST_LONG_ONLY), + ('%s_id' % _assoc_res_name, '%s ID' % _assoc_res_name.capitalize(), + nc_osc_utils.LIST_BOTH), + ('prefix_routes', 'Prefix Routes (BGP LOCAL_PREF)', + nc_osc_utils.LIST_LONG_ONLY), + ('bgpvpn_routes', 'BGP VPN Routes (BGP LOCAL_PREF)', + nc_osc_utils.LIST_LONG_ONLY), + ('advertise_fixed_ips', "Advertise Port's Fixed IPs", + nc_osc_utils.LIST_LONG_ONLY), + ) + _formatters = { + 'prefix_routes': format_columns.ListColumn, + 'bgpvpn_routes': format_columns.ListColumn, + } + + def _transform_resource(self, data): + """Transforms BGP VPN port association routes property + + That permits to easily format the command output with ListColumn + formater and separate the two route types. + + {'routes': + [ + { + 'type': 'prefix', + 'local_pref': 100, + 'prefix': '8.8.8.0/27', + }, + { + 'type': 'prefix', + 'local_pref': 42, + 'prefix': '80.50.30.0/28', + }, + { + 'type': 'bgpvpn', + 'local_pref': 50, + 'bgpvpn': '157d72a9-9968-48e7-8087-6c9a9bc7a181', + }, + { + 'type': 'bgpvpn', + 'bgpvpn': 'd5c7aaab-c7e8-48b3-85ca-a115c00d3603', + }, + ], + } + + to + + { + 'prefix_routes': [ + '8.8.8.0/27 (100)', + '80.50.30.0/28 (42)', + ], + 'bgpvpn_routes': [ + '157d72a9-9968-48e7-8087-6c9a9bc7a181 (50)', + 'd5c7aaab-c7e8-48b3-85ca-a115c00d3603', + ], + } + """ + for route in data.get('routes', []): + local_pref = '' + if route.get('local_pref'): + local_pref = ' (%d)' % route.get('local_pref') + if route['type'] == 'prefix': + data.setdefault('prefix_routes', []).append( + '%s%s' % (route['prefix'], local_pref) + ) + elif route['type'] == 'bgpvpn': + data.setdefault('bgpvpn_routes', []).append( + '%s%s' % (route['bgpvpn_id'], local_pref) + ) + else: + LOG.warning("Unknown route type %s (%s).", route['type'], + route) + data.pop('routes', None) + + 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_ROUTE = _("Fixed IPs of the port will be advertised to the " + "BGP VPN%s") % ( + _(' (default)') if self._action == 'create' + else "") + NOT_ADVERTISE_ROUTE = _("Fixed IPs of the port will not be advertised " + "to the BGP VPN") + + LOCAL_PREF_VALUE = _(". Optionally, can control the value of the BGP " + "LOCAL_PREF of the routes that will be " + "advertised") + + ADD_PREFIX_ROUTE = _("Add prefix route in CIDR notation%s") %\ + LOCAL_PREF_VALUE + REMOVE_PREFIX_ROUTE = _("Remove prefix route in CIDR notation") + REPEAT_PREFIX_ROUTE = _("repeat option for multiple prefix routes") + + ADD_BGVPVPN_ROUTE = _("Add BGP VPN route for route leaking%s") %\ + LOCAL_PREF_VALUE + REMOVE_BGPVPN_ROUTE = _("Remove BGP VPN route") + REPEAT_BGPVPN_ROUTE = _("repeat option for multiple BGP VPN routes") + + group_advertise_fixed_ips = parser.add_mutually_exclusive_group() + group_advertise_fixed_ips.add_argument( + '--advertise-fixed-ips', + action='store_true', + help=NOT_ADVERTISE_ROUTE if self._action == 'unset' + else ADVERTISE_ROUTE, + ) + group_advertise_fixed_ips.add_argument( + '--no-advertise-fixed-ips', + action='store_true', + help=ADVERTISE_ROUTE if self._action == 'unset' + else NOT_ADVERTISE_ROUTE, + ) + + if self._action in ['create', 'set']: + parser.add_argument( + '--prefix-route', + metavar="prefix=[,local_pref=]", + dest='prefix_routes', + action=parseractions.MultiKeyValueAction, + required_keys=['prefix'], + optional_keys=['local_pref'], + help="%s (%s)" % (ADD_PREFIX_ROUTE, REPEAT_PREFIX_ROUTE), + ) + parser.add_argument( + '--bgpvpn-route', + metavar="bgpvpn=[,local_pref=]", + dest='bgpvpn_routes', + action=parseractions.MultiKeyValueAction, + required_keys=['bgpvpn'], + optional_keys=['local_pref'], + help="%s (%s)" % (ADD_BGVPVPN_ROUTE, REPEAT_BGPVPN_ROUTE), + ) + else: + parser.add_argument( + '--prefix-route', + metavar="", + dest='prefix_routes', + action='append', + help="%s (%s)" % (REMOVE_PREFIX_ROUTE, REPEAT_PREFIX_ROUTE), + ) + parser.add_argument( + '--bgpvpn-route', + metavar="", + dest='bgpvpn_routes', + action='append', + help="%s (%s)" % (REMOVE_BGPVPN_ROUTE, REPEAT_BGPVPN_ROUTE), + ) + if self._action != 'create': + parser.add_argument( + '--no-prefix-route' if self._action == 'set' else + '--all-prefix-routes', + dest='purge_prefix_route', + action='store_true', + help=_('Empty prefix route list'), + ) + parser.add_argument( + '--no-bgpvpn-route' if self._action == 'set' else + '--all-bgpvpn-routes', + dest='purge_bgpvpn_route', + action='store_true', + help=_('Empty BGP VPN route list'), + ) + + def _args2body(self, bgpvpn_id, args): + client = self.app.client_manager.neutronclient + attrs = {} + + if self._action != 'create': + assoc = client.find_resource_by_id( + self._resource, + args.resource_association_id, + cmd_resource='bgpvpn_%s_assoc' % self._assoc_res_name, + parent_id=bgpvpn_id) + else: + assoc = {'routes': []} + + if args.advertise_fixed_ips: + attrs['advertise_fixed_ips'] = self._action != 'unset' + elif args.no_advertise_fixed_ips: + attrs['advertise_fixed_ips'] = self._action == 'unset' + + prefix_routes = None + if 'purge_prefix_route' in args and args.purge_prefix_route: + prefix_routes = [] + else: + prefix_routes = {r['prefix']: r.get('local_pref') + for r in assoc['routes'] + if r['type'] == 'prefix'} + if args.prefix_routes: + if self._action in ['create', 'set']: + prefix_routes.update({r['prefix']: r.get('local_pref') + for r in args.prefix_routes}) + elif self._action == 'unset': + for prefix in args.prefix_routes: + prefix_routes.pop(prefix, None) + + bgpvpn_routes = None + if 'purge_bgpvpn_route' in args and args.purge_bgpvpn_route: + bgpvpn_routes = [] + else: + bgpvpn_routes = {r['bgpvpn_id']: r.get('local_pref') + for r in assoc['routes'] + if r['type'] == 'bgpvpn'} + if args.bgpvpn_routes: + if self._action == 'unset': + routes = [ + {'bgpvpn': bgpvpn} for bgpvpn in args.bgpvpn_routes + ] + else: + routes = args.bgpvpn_routes + args_bgpvpn_routes = { + client.find_resource(constants.BGPVPN, r['bgpvpn'])['id']: + r.get('local_pref') + for r in routes + } + if self._action in ['create', 'set']: + bgpvpn_routes.update(args_bgpvpn_routes) + elif self._action == 'unset': + for bgpvpn_id in args_bgpvpn_routes: + bgpvpn_routes.pop(bgpvpn_id, None) + + if prefix_routes is not None and not prefix_routes: + attrs.setdefault('routes', []) + elif prefix_routes is not None: + for prefix, local_pref in prefix_routes.items(): + route = { + 'type': 'prefix', + 'prefix': prefix, + } + if local_pref: + route['local_pref'] = int(local_pref) + attrs.setdefault('routes', []).append(route) + if bgpvpn_routes is not None and not bgpvpn_routes: + attrs.setdefault('routes', []) + elif bgpvpn_routes is not None: + for bgpvpn_id, local_pref in bgpvpn_routes.items(): + route = { + 'type': 'bgpvpn', + 'bgpvpn_id': bgpvpn_id, + } + if local_pref: + route['local_pref'] = int(local_pref) + attrs.setdefault('routes', []).append(route) + + return {self._resource: attrs} + + +class CreateBgpvpnPortAssoc(BgpvpnPortAssoc, + resource_association.CreateBgpvpnResAssoc): + _description = _("Create a BGP VPN port association") + + +class SetBgpvpnPortAssoc(BgpvpnPortAssoc, + resource_association.SetBgpvpnResAssoc): + _description = _("Set BGP VPN port association properties") + + +class UnsetBgpvpnPortAssoc(BgpvpnPortAssoc, + resource_association.UnsetBgpvpnResAssoc): + _description = _("Unset BGP VPN port association properties") + + +class DeleteBgpvpnPortAssoc(BgpvpnPortAssoc, + resource_association.DeleteBgpvpnResAssoc): + _description = _("Delete a BGP VPN port association(s) for a given BGP " + "VPN") + + +class ListBgpvpnPortAssoc(BgpvpnPortAssoc, + resource_association.ListBgpvpnResAssoc): + _description = _("List BGP VPN port associations for a given BGP VPN") + + +class ShowBgpvpnPortAssoc(BgpvpnPortAssoc, + resource_association.ShowBgpvpnResAssoc): + _description = _("Show information of a given BGP VPN port association") diff --git a/neutronclient/osc/v2/networking_bgpvpn/resource_association.py b/neutronclient/osc/v2/networking_bgpvpn/resource_association.py index 485803c47..4784100f8 100644 --- a/neutronclient/osc/v2/networking_bgpvpn/resource_association.py +++ b/neutronclient/osc/v2/networking_bgpvpn/resource_association.py @@ -16,6 +16,7 @@ import logging +from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils as osc_utils @@ -29,6 +30,7 @@ LOG = logging.getLogger(__name__) class CreateBgpvpnResAssoc(command.ShowOne): """Create a BGP VPN resource association""" + _action = 'create' def get_parser(self, prog_name): parser = super(CreateBgpvpnResAssoc, self).get_parser(prog_name) @@ -45,6 +47,11 @@ class CreateBgpvpnResAssoc(command.ShowOne): help=(_("%s to associate the BGP VPN (name or ID)") % self._assoc_res_name.capitalize()), ) + + get_common_parser = getattr(self, '_get_common_parser', None) + if callable(get_common_parser): + get_common_parser(parser) + return parser def take_action(self, parsed_args): @@ -66,7 +73,16 @@ class CreateBgpvpnResAssoc(command.ShowOne): parsed_args.project_domain, ).id body[self._resource]['tenant_id'] = project_id + + arg2body = getattr(self, '_args2body', None) + if callable(arg2body): + body[self._resource].update( + arg2body(bgpvpn['id'], parsed_args)[self._resource]) + obj = create_method(bgpvpn['id'], body)[self._resource] + transform = getattr(self, '_transform_resource', None) + if callable(transform): + transform(obj) columns, display_columns = nc_osc_utils.get_columns(obj, self._attr_map) data = osc_utils.get_dict_properties(obj, columns, @@ -74,6 +90,48 @@ class CreateBgpvpnResAssoc(command.ShowOne): return display_columns, data +class SetBgpvpnResAssoc(command.Command): + """Set BGP VPN resource association properties""" + _action = 'set' + + def get_parser(self, prog_name): + parser = super(SetBgpvpnResAssoc, self).get_parser(prog_name) + parser.add_argument( + 'resource_association_id', + metavar="<%s association ID>" % self._assoc_res_name, + help=(_("%s association ID to update") % + self._assoc_res_name.capitalize()), + ) + parser.add_argument( + 'bgpvpn', + metavar="", + help=(_("BGP VPN the %s association belongs to (name or ID)") % + self._assoc_res_name), + ) + + get_common_parser = getattr(self, '_get_common_parser', None) + if callable(get_common_parser): + get_common_parser(parser) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.neutronclient + update_method = getattr( + client, 'update_bgpvpn_%s_assoc' % self._assoc_res_name) + bgpvpn = client.find_resource(constants.BGPVPN, parsed_args.bgpvpn) + arg2body = getattr(self, '_args2body', None) + if callable(arg2body): + body = arg2body(bgpvpn['id'], parsed_args) + update_method(bgpvpn['id'], parsed_args.resource_association_id, + body) + + +class UnsetBgpvpnResAssoc(SetBgpvpnResAssoc): + """Unset BGP VPN resource association properties""" + _action = 'unset' + + class DeleteBgpvpnResAssoc(command.Command): """Remove a BGP VPN resource association(s) for a given BGP VPN""" @@ -89,7 +147,8 @@ class DeleteBgpvpnResAssoc(command.Command): parser.add_argument( 'bgpvpn', metavar="", - help=_("BGP VPN the association belongs to (name or ID)"), + help=(_("BGP VPN the %s association belongs to (name or ID)") % + self._assoc_res_name), ) return parser @@ -137,6 +196,13 @@ class ListBgpvpnResAssoc(command.Lister): action='store_true', help=_("List additional fields in output"), ) + parser.add_argument( + '--property', + metavar="", + help=_("Filter property to apply on returned BGP VPNs (repeat to " + "filter on multiple properties)"), + action=parseractions.KeyValueAction, + ) return parser def take_action(self, parsed_args): @@ -144,8 +210,14 @@ class ListBgpvpnResAssoc(command.Lister): list_method = getattr(client, 'list_bgpvpn_%s_assocs' % self._assoc_res_name) bgpvpn = client.find_resource(constants.BGPVPN, parsed_args.bgpvpn) + params = {} + if parsed_args.property: + params.update(parsed_args.property) objs = list_method(bgpvpn['id'], - retrieve_all=True)[self._resource_plural] + retrieve_all=True, **params)[self._resource_plural] + transform = getattr(self, '_transform_resource', None) + if callable(transform): + [transform(obj) for obj in objs] headers, columns = nc_osc_utils.get_column_definitions( self._attr_map, long_listing=parsed_args.long) return (headers, (osc_utils.get_dict_properties( @@ -181,6 +253,9 @@ class ShowBgpvpnResAssoc(command.ShowOne): cmd_resource='bgpvpn_%s_assoc' % self._assoc_res_name, parent_id=bgpvpn['id']) obj = show_method(bgpvpn['id'], assoc['id'])[self._resource] + transform = getattr(self, '_transform_resource', None) + if callable(transform): + transform(obj) columns, display_columns = nc_osc_utils.get_columns(obj, self._attr_map) data = osc_utils.get_dict_properties(obj, columns, diff --git a/neutronclient/osc/v2/networking_bgpvpn/router_association.py b/neutronclient/osc/v2/networking_bgpvpn/router_association.py index ee788d33c..a51fbe604 100644 --- a/neutronclient/osc/v2/networking_bgpvpn/router_association.py +++ b/neutronclient/osc/v2/networking_bgpvpn/router_association.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Juniper Routerworks Inc. +# Copyright (c) 2016 Juniper networks Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/neutronclient/tests/unit/osc/v2/networking_bgpvpn/fakes.py b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/fakes.py index 3d3ec597e..0103904e3 100644 --- a/neutronclient/tests/unit/osc/v2/networking_bgpvpn/fakes.py +++ b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/fakes.py @@ -26,11 +26,18 @@ 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 from neutronclient.tests.unit.osc.v2 import fakes as test_fakes +_FAKE_PROJECT_ID = 'fake_project_id' + + class TestNeutronClientBgpvpn(test_fakes.TestNeutronClientOSCV2): def setUp(self): @@ -38,11 +45,11 @@ class TestNeutronClientBgpvpn(test_fakes.TestNeutronClientOSCV2): self.neutronclient.find_resource = mock.Mock( side_effect=lambda resource, name_or_id, project_id=None, cmd_resource=None, parent_id=None, fields=None: - {'id': name_or_id}) + {'id': name_or_id, 'tenant_id': _FAKE_PROJECT_ID}) self.neutronclient.find_resource_by_id = mock.Mock( side_effect=lambda resource, resource_id, cmd_resource=None, parent_id=None, fields=None: - {'id': resource_id}) + {'id': resource_id, 'tenant_id': _FAKE_PROJECT_ID}) nc_osc_utils.find_project = mock.Mock( side_effect=lambda _, name_or_id, __: mock.Mock(id=name_or_id)) @@ -59,7 +66,7 @@ class FakeBgpvpn(object): # Set default attributes. bgpvpn_attrs = { 'id': 'fake_bgpvpn_id', - 'tenant_id': 'fake_project_id', + 'tenant_id': _FAKE_PROJECT_ID, 'name': '', 'type': 'l3', 'route_targets': [], @@ -68,11 +75,13 @@ class FakeBgpvpn(object): 'route_distinguishers': [], 'networks': [], 'routers': [], + 'ports': [], 'vni': 100, } # Overwrite default attributes. bgpvpn_attrs.update(attrs) + return copy.deepcopy(bgpvpn_attrs) @staticmethod @@ -108,6 +117,14 @@ class CreateBgpvpnFakeResAssoc(BgpvpnFakeAssoc, CreateBgpvpnResAssoc): pass +class SetBgpvpnFakeResAssoc(BgpvpnFakeAssoc, SetBgpvpnResAssoc): + pass + + +class UnsetBgpvpnFakeResAssoc(BgpvpnFakeAssoc, UnsetBgpvpnResAssoc): + pass + + class DeleteBgpvpnFakeResAssoc(BgpvpnFakeAssoc, DeleteBgpvpnResAssoc): pass @@ -132,7 +149,7 @@ class FakeResource(object): # Set default attributes. res_attrs = { 'id': 'fake_resource_id', - 'tenant_id': 'fake_project_id', + 'tenant_id': _FAKE_PROJECT_ID, } # Overwrite default attributes. diff --git a/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_bgpvpn.py b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_bgpvpn.py index 71f5acccf..bfa710446 100644 --- a/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_bgpvpn.py +++ b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_bgpvpn.py @@ -126,6 +126,8 @@ class TestCreateBgpvpn(fakes.TestNeutronClientBgpvpn): fake_bgpvpn_call.pop('id') fake_bgpvpn_call.pop('networks') fake_bgpvpn_call.pop('routers') + fake_bgpvpn_call.pop('ports') + self.neutronclient.create_bgpvpn.assert_called_once_with( {constants.BGPVPN: fake_bgpvpn_call}) self.assertEqual(sorted_headers, cols) diff --git a/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_resource_association.py b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_resource_association.py index 6978d85cd..49be9f452 100644 --- a/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_resource_association.py +++ b/neutronclient/tests/unit/osc/v2/networking_bgpvpn/test_resource_association.py @@ -81,6 +81,7 @@ class TestCreateResAssoc(fakes.TestNeutronClientBgpvpn): 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( fake_bgpvpn['id'], @@ -89,6 +90,36 @@ class TestCreateResAssoc(fakes.TestNeutronClientBgpvpn): self.assertEqual(_get_data(fake_res_assoc), data) +class TestSetResAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super(TestSetResAssoc, self).setUp() + self.cmd = fakes.SetBgpvpnFakeResAssoc(self.app, self.namespace) + + def test_set_resource_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) + self.neutronclient.update_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) + + result = self.cmd.take_action(parsed_args) + + self.neutronclient.update_bgpvpn_fake_resource_assoc.\ + assert_not_called() + self.assertIsNone(result) + + class TestDeleteResAssoc(fakes.TestNeutronClientBgpvpn): def setUp(self): super(TestDeleteResAssoc, self).setUp() diff --git a/neutronclient/v2_0/client.py b/neutronclient/v2_0/client.py index 412e1b80e..cb84b9fc9 100644 --- a/neutronclient/v2_0/client.py +++ b/neutronclient/v2_0/client.py @@ -651,6 +651,8 @@ class Client(ClientBase): bgpvpn_router_associations_path = "/bgpvpn/bgpvpns/%s/router_associations" bgpvpn_router_association_path =\ "/bgpvpn/bgpvpns/%s/router_associations/%s" + bgpvpn_port_associations_path = "/bgpvpn/bgpvpns/%s/port_associations" + bgpvpn_port_association_path = "/bgpvpn/bgpvpns/%s/port_associations/%s" network_logs_path = "/log/logs" network_log_path = "/log/logs/%s" network_loggables_path = "/log/loggable-resources" @@ -707,6 +709,7 @@ class Client(ClientBase): 'bgpvpns': 'bgpvpn', 'network_associations': 'network_association', 'router_associations': 'router_association', + 'port_associations': 'port_association', 'flow_classifiers': 'flow_classifier', 'port_pairs': 'port_pair', 'port_pair_groups': 'port_pair_group', @@ -2193,6 +2196,34 @@ class Client(ClientBase): return self.delete( self.bgpvpn_router_association_path % (bgpvpn, router_assoc)) + def list_bgpvpn_port_assocs(self, bgpvpn, retrieve_all=True, **_params): + """Fetches a list of port associations for a given BGP VPN.""" + return self.list('port_associations', + self.bgpvpn_port_associations_path % bgpvpn, + retrieve_all, **_params) + + def show_bgpvpn_port_assoc(self, bgpvpn, port_assoc, **_params): + """Fetches information of a certain BGP VPN's port association""" + return self.get( + self.bgpvpn_port_association_path % (bgpvpn, port_assoc), + params=_params) + + def create_bgpvpn_port_assoc(self, bgpvpn, body=None): + """Creates a new BGP VPN port association""" + return self.post(self.bgpvpn_port_associations_path % bgpvpn, + body=body) + + def update_bgpvpn_port_assoc(self, bgpvpn, port_assoc, body=None): + """Updates a BGP VPN port association""" + return self.put( + self.bgpvpn_port_association_path % (bgpvpn, port_assoc), + body=body) + + def delete_bgpvpn_port_assoc(self, bgpvpn, port_assoc): + """Deletes the specified BGP VPN port association""" + return self.delete( + self.bgpvpn_port_association_path % (bgpvpn, port_assoc)) + def create_sfc_port_pair(self, body=None): """Creates a new Port Pair.""" return self.post(self.sfc_port_pairs_path, body=body) diff --git a/releasenotes/notes/support-bgpvpn-route-control-aeda3e698486f73b.yaml b/releasenotes/notes/support-bgpvpn-route-control-aeda3e698486f73b.yaml new file mode 100644 index 000000000..2f7ed1720 --- /dev/null +++ b/releasenotes/notes/support-bgpvpn-route-control-aeda3e698486f73b.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add BGP VPN `port association `_ + support to the CLI, which are introduced for BGP VPN interconnections by the + ``bgpvpn-routes-control`` API extension. diff --git a/setup.cfg b/setup.cfg index eccd2e3b2..67a2a35e4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -124,6 +124,12 @@ openstack.neutronclient.v2 = 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_show = neutronclient.osc.v2.networking_bgpvpn.router_association:ShowBgpvpnRouterAssoc + 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 + bgpvpn_port_association_delete = neutronclient.osc.v2.networking_bgpvpn.port_association:DeleteBgpvpnPortAssoc + bgpvpn_port_association_list = neutronclient.osc.v2.networking_bgpvpn.port_association:ListBgpvpnPortAssoc + bgpvpn_port_association_show = neutronclient.osc.v2.networking_bgpvpn.port_association:ShowBgpvpnPortAssoc network_loggable_resources_list = neutronclient.osc.v2.logging.network_log:ListLoggableResource network_log_create = neutronclient.osc.v2.logging.network_log:CreateNetworkLog