From 525b9bbf24f39e9dd423bff18992b17391ae4c23 Mon Sep 17 00:00:00 2001 From: Robert Kukura Date: Mon, 16 Dec 2019 06:43:39 -0500 Subject: [PATCH] [AIM] Add extra provided/consumed contracts to network extension Add apic:extra_provided_contracts and apic:extra_consumed_contracts attributes to the cisco_apic network extension. The named contracts are provided/consumed by the network's default EPG, in addition to any applicable router contracts. At least one subnet on the network must be attached as a router interface for the extra contracts to have any effect on the network's connectivity. Attempting to specify extra contracts for an external network or SVI network results is rejected with an exception. (cherry picked from commit 496f54b84a4c3583bb58e37fca9647bd1d16e30a) (cherry picked from commit 3e16a0c590cae3a7d6f18fac17437dceb3a82b13) Change-Id: I784107f4e7d7d5d39377c583bcd2163c7688eb5b --- .../alembic_migrations/versions/HEAD | 2 +- .../versions/f28141ea1bbf_extra_contracts.py | 44 ++++++ gbpservice/neutron/extensions/cisco_apic.py | 14 ++ .../ml2plus/drivers/apic_aim/exceptions.py | 6 + .../ml2plus/drivers/apic_aim/extension_db.py | 44 +++++- .../drivers/apic_aim/extension_driver.py | 16 ++- .../drivers/apic_aim/mechanism_driver.py | 78 +++++++++-- .../unit/plugins/ml2plus/test_apic_aim.py | 128 +++++++++++++++++- .../grouppolicy/test_aim_validation.py | 5 +- 9 files changed, 313 insertions(+), 24 deletions(-) create mode 100644 gbpservice/neutron/db/migration/alembic_migrations/versions/f28141ea1bbf_extra_contracts.py diff --git a/gbpservice/neutron/db/migration/alembic_migrations/versions/HEAD b/gbpservice/neutron/db/migration/alembic_migrations/versions/HEAD index c34a1200f..1b4974d70 100644 --- a/gbpservice/neutron/db/migration/alembic_migrations/versions/HEAD +++ b/gbpservice/neutron/db/migration/alembic_migrations/versions/HEAD @@ -1 +1 @@ -b76dc22f2e23 +f28141ea1bbf diff --git a/gbpservice/neutron/db/migration/alembic_migrations/versions/f28141ea1bbf_extra_contracts.py b/gbpservice/neutron/db/migration/alembic_migrations/versions/f28141ea1bbf_extra_contracts.py new file mode 100644 index 000000000..e79c90cb6 --- /dev/null +++ b/gbpservice/neutron/db/migration/alembic_migrations/versions/f28141ea1bbf_extra_contracts.py @@ -0,0 +1,44 @@ +# 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. + +"""Table for cisco_apic network extra contracts extension attributes + +Revision ID: f28141ea1bbf +Revises: b76dc22f2e23 +Create Date: 2019-12-20 13:08:03.603312 + +""" + +# revision identifiers, used by Alembic. +revision = 'f28141ea1bbf' +down_revision = 'b76dc22f2e23' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'apic_aim_network_extra_contracts', + sa.Column('network_id', sa.String(36), nullable=False), + sa.Column('contract_name', sa.String(64), nullable=False), + sa.Column('provides', sa.Boolean, nullable=False), + sa.ForeignKeyConstraint(['network_id'], ['networks.id'], + name= + 'apic_aim_network_contract_extn_fk_network', + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('network_id', 'contract_name', 'provides') + ) + + +def downgrade(): + pass diff --git a/gbpservice/neutron/extensions/cisco_apic.py b/gbpservice/neutron/extensions/cisco_apic.py index 1da069cf9..d3ddc1358 100644 --- a/gbpservice/neutron/extensions/cisco_apic.py +++ b/gbpservice/neutron/extensions/cisco_apic.py @@ -43,6 +43,8 @@ NESTED_DOMAIN_INFRA_VLAN = 'apic:nested_domain_infra_vlan' NESTED_DOMAIN_ALLOWED_VLANS = 'apic:nested_domain_allowed_vlans' NESTED_DOMAIN_SERVICE_VLAN = 'apic:nested_domain_service_vlan' NESTED_DOMAIN_NODE_NETWORK_VLAN = 'apic:nested_domain_node_network_vlan' +EXTRA_PROVIDED_CONTRACTS = 'apic:extra_provided_contracts' +EXTRA_CONSUMED_CONTRACTS = 'apic:extra_consumed_contracts' BD = 'BridgeDomain' EPG = 'EndpointGroup' @@ -218,6 +220,18 @@ NET_ATTRIBUTES = { }, 'convert_to': convert_nested_domain_allowed_vlans, }, + EXTRA_PROVIDED_CONTRACTS: { + 'allow_post': True, 'allow_put': True, + 'is_visible': True, 'default': None, + 'convert_to': conv.convert_none_to_empty_list, + 'validate': {'type:list_of_unique_strings': None}, + }, + EXTRA_CONSUMED_CONTRACTS: { + 'allow_post': True, 'allow_put': True, + 'is_visible': True, 'default': None, + 'convert_to': conv.convert_none_to_empty_list, + 'validate': {'type:list_of_unique_strings': None}, + }, } EXT_NET_ATTRIBUTES = { diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/exceptions.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/exceptions.py index 0ed051605..4871f6f6d 100644 --- a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/exceptions.py +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/exceptions.py @@ -101,3 +101,9 @@ class AAPNotAllowedOnDifferentActiveActiveAAPSubnet(exceptions.BadRequest): message = _("Allowed address pair can not be added to this port " "because its subnets %(subnet_ids)s active active AAP mode is " "different than other port's subnets %(other_subnet_ids)s.") + + +class InvalidNetworkForExtraContracts(exceptions.BadRequest): + message = _("Cannot specify apic:extra_provided_contracts or " + "apic:extra_consumed_consumed contracts for an external or " + "SVI network.") diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_db.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_db.py index ef9c6d117..97042b26c 100644 --- a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_db.py +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_db.py @@ -84,6 +84,21 @@ class NetworkExtNestedDomainAllowedVlansDb(model_base.BASEV2): lazy='joined', cascade='delete')) +class NetworkExtExtraContractDb(model_base.BASEV2): + + __tablename__ = 'apic_aim_network_extra_contracts' + + network_id = sa.Column( + sa.String(36), sa.ForeignKey('networks.id', ondelete="CASCADE")) + contract_name = sa.Column(sa.String(64), primary_key=True) + provides = sa.Column(sa.Boolean, primary_key=True) + network = orm.relationship(models_v2.Network, + backref=orm.backref( + 'aim_extension_extra_contract_mapping', + uselist=True, + lazy='joined', cascade='delete')) + + class SubnetExtensionDb(model_base.BASEV2): __tablename__ = 'apic_aim_subnet_extensions' @@ -133,24 +148,33 @@ class ExtensionDbMixin(object): NetworkExtNestedDomainAllowedVlansDb).filter( NetworkExtNestedDomainAllowedVlansDb.network_id.in_( network_ids)).all()) + db_contracts = (session.query(NetworkExtExtraContractDb).filter( + NetworkExtExtraContractDb.network_id.in_(network_ids)).all()) cidrs_by_net_id = {} vlans_by_net_id = {} + contracts_by_net_id = {} for db_cidr in db_cidrs: cidrs_by_net_id.setdefault(db_cidr.network_id, []).append( db_cidr) for db_vlan in db_vlans: vlans_by_net_id.setdefault(db_vlan.network_id, []).append( db_vlan) + for db_contract in db_contracts: + contracts_by_net_id.setdefault(db_contract.network_id, []).append( + db_contract) + result = {} for db_obj in db_objs: net_id = db_obj.network_id result.setdefault(net_id, self.make_network_extn_db_conf_dict( db_obj, cidrs_by_net_id.get(net_id, []), - vlans_by_net_id.get(net_id, []))) + vlans_by_net_id.get(net_id, []), + contracts_by_net_id.get(net_id, []))) return result - def make_network_extn_db_conf_dict(self, ext_db, db_cidrs, db_vlans): + def make_network_extn_db_conf_dict(self, ext_db, db_cidrs, db_vlans, + db_contracts): net_res = {} db_obj = ext_db if db_obj: @@ -174,6 +198,10 @@ class ExtensionDbMixin(object): db_obj['nested_domain_node_network_vlan']) net_res[cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS] = [ c.vlan for c in db_vlans] + net_res[cisco_apic.EXTRA_PROVIDED_CONTRACTS] = [ + c.contract_name for c in db_contracts if c.provides] + net_res[cisco_apic.EXTRA_CONSUMED_CONTRACTS] = [ + c.contract_name for c in db_contracts if not c.provides] if net_res.get(cisco_apic.EXTERNAL_NETWORK): net_res[cisco_apic.EXTERNAL_CIDRS] = [c.cidr for c in db_cidrs] return net_res @@ -229,6 +257,18 @@ class ExtensionDbMixin(object): res_dict[cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS], network_id=network_id) + if cisco_apic.EXTRA_PROVIDED_CONTRACTS in res_dict: + self._update_list_attr( + session, NetworkExtExtraContractDb, 'contract_name', + res_dict[cisco_apic.EXTRA_PROVIDED_CONTRACTS], + network_id=network_id, provides=True) + + if cisco_apic.EXTRA_CONSUMED_CONTRACTS in res_dict: + self._update_list_attr( + session, NetworkExtExtraContractDb, 'contract_name', + res_dict[cisco_apic.EXTRA_CONSUMED_CONTRACTS], + network_id=network_id, provides=False) + def get_network_ids_by_ext_net_dn(self, session, dn, lock_update=False): query = BAKERY(lambda s: s.query( NetworkExtensionDb.network_id)) diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_driver.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_driver.py index c1a121ac0..43bba28e6 100644 --- a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_driver.py +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/extension_driver.py @@ -115,6 +115,10 @@ class ApicExtensionDriver(api_plus.ExtensionDriver, data.get(cisco_apic.NESTED_DOMAIN_SERVICE_VLAN), cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN: data.get(cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN), + cisco_apic.EXTRA_PROVIDED_CONTRACTS: + data.get(cisco_apic.EXTRA_PROVIDED_CONTRACTS), + cisco_apic.EXTRA_CONSUMED_CONTRACTS: + data.get(cisco_apic.EXTRA_CONSUMED_CONTRACTS), } if cisco_apic.VLANS_LIST in (data.get( cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS) or {}): @@ -150,8 +154,7 @@ class ApicExtensionDriver(api_plus.ExtensionDriver, result.update(res_dict) def process_update_network(self, plugin_context, data, result): - # External_cidr, bgp_enable, bgp_type and bgp_asn or - # nested domain attributes could be updated + # Extension attributes that could be updated. update_attrs = [ cisco_apic.EXTERNAL_CIDRS, cisco_apic.BGP, cisco_apic.BGP_TYPE, cisco_apic.BGP_ASN, @@ -159,8 +162,9 @@ class ApicExtensionDriver(api_plus.ExtensionDriver, cisco_apic.NESTED_DOMAIN_INFRA_VLAN, cisco_apic.NESTED_DOMAIN_SERVICE_VLAN, cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN, - cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS] - + cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS, + cisco_apic.EXTRA_PROVIDED_CONTRACTS, + cisco_apic.EXTRA_CONSUMED_CONTRACTS] if not(set(update_attrs) & set(data.keys())): return @@ -176,7 +180,9 @@ class ApicExtensionDriver(api_plus.ExtensionDriver, cisco_apic.NESTED_DOMAIN_NAME, cisco_apic.NESTED_DOMAIN_TYPE, cisco_apic.NESTED_DOMAIN_INFRA_VLAN, cisco_apic.NESTED_DOMAIN_SERVICE_VLAN, - cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN] + cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN, + cisco_apic.EXTRA_PROVIDED_CONTRACTS, + cisco_apic.EXTRA_CONSUMED_CONTRACTS] for e_k in ext_keys: if e_k in data: res_dict.update({e_k: data[e_k]}) diff --git a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py index 88b3d8858..ca696fd00 100644 --- a/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py +++ b/gbpservice/neutron/plugins/ml2plus/drivers/apic_aim/mechanism_driver.py @@ -600,7 +600,15 @@ class ApicMechanismDriver(api_plus.MechanismDriver, session = context._plugin_context.session aim_ctx = aim_context.AimContext(session) - if self._is_external(current): + is_ext = self._is_external(current) + is_svi = self._is_svi(current) + + if ((current[cisco_apic.EXTRA_PROVIDED_CONTRACTS] or + current[cisco_apic.EXTRA_CONSUMED_CONTRACTS]) and + (is_ext or is_svi)): + raise exceptions.InvalidNetworkForExtraContracts() + + if is_ext: l3out, ext_net, ns = self._get_aim_nat_strategy(current) if not ext_net: return # Unmanaged external network @@ -621,7 +629,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver, epg = resource elif isinstance(resource, aim_resource.VRF): vrf = resource - elif self._is_svi(current): + elif is_svi: l3out, ext_net, _ = self._get_aim_external_objects(current) if ext_net: other_nets = set( @@ -714,6 +722,10 @@ class ApicMechanismDriver(api_plus.MechanismDriver, epg.display_name = dname epg.bd_name = bd.name + epg.provided_contract_names = current[ + cisco_apic.EXTRA_PROVIDED_CONTRACTS] + epg.consumed_contract_names = current[ + cisco_apic.EXTRA_CONSUMED_CONTRACTS] self.aim.create(aim_ctx, epg) self._add_network_mapping_and_notify( @@ -732,8 +744,10 @@ class ApicMechanismDriver(api_plus.MechanismDriver, mapping = self._get_network_mapping(session, current['id']) is_ext = self._is_external(current) - # REVISIT: Remove is_ext from condition and add UT for - # updating external network name. + is_svi = self._is_svi(current) + + # Update name if changed. REVISIT: Remove is_ext from + # condition and add UT for updating external network name. if (not is_ext and current['name'] != original['name']): dname = aim_utils.sanitize_display_name(current['name']) @@ -747,6 +761,37 @@ class ApicMechanismDriver(api_plus.MechanismDriver, if l3out: self.aim.update(aim_ctx, l3out, display_name=dname) + # Update extra provided/consumed contracts if changed. + curr_prov = set(current[cisco_apic.EXTRA_PROVIDED_CONTRACTS]) + curr_cons = set(current[cisco_apic.EXTRA_CONSUMED_CONTRACTS]) + orig_prov = set(original[cisco_apic.EXTRA_PROVIDED_CONTRACTS]) + orig_cons = set(original[cisco_apic.EXTRA_CONSUMED_CONTRACTS]) + if (curr_prov != orig_prov or curr_cons != orig_cons): + if is_ext or is_svi: + raise exceptions.InvalidNetworkForExtraContracts() + + added_prov = curr_prov - orig_prov + removed_prov = orig_prov - curr_prov + added_cons = curr_cons - orig_cons + removed_cons = orig_cons - curr_cons + + # REVISIT: AIM needs methods to atomically add/remove + # items to/from lists, as concurrent changes from router + # operations are possible. + epg = self.aim.get(aim_ctx, self._get_network_epg(mapping)) + + if added_prov or removed_prov: + contracts = ((set(epg.provided_contract_names) | added_prov) + - removed_prov) + self.aim.update( + aim_ctx, epg, provided_contract_names=contracts) + + if added_cons or removed_cons: + contracts = ((set(epg.consumed_contract_names) | added_cons) + - removed_cons) + self.aim.update( + aim_ctx, epg, consumed_contract_names=contracts) + if is_ext: _, ext_net, ns = self._get_aim_nat_strategy(current) if ext_net: @@ -762,7 +807,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver, # TODO(amitbose) Propagate name updates to AIM else: # BGP config is supported only for svi networks. - if not self._is_svi(current): + if not is_svi: return # Check for pre-existing l3out SVI. network_db = self.plugin._get_network(context._plugin_context, @@ -979,7 +1024,7 @@ class ApicMechanismDriver(api_plus.MechanismDriver, # Needed because of commit # d8c1e153f88952b7670399715c2f88f1ecf0a94a in Neutron that # put the extension call in Pike+ *before* the precommit - # calls happen in network creation. I believe this is a but + # calls happen in network creation. I believe this is a bug # and should be discussed with the Neutron team. mapping = self._get_network_mapping(session, net_db.id) if mapping: @@ -1009,14 +1054,15 @@ class ApicMechanismDriver(api_plus.MechanismDriver, # Needed because of commit # d8c1e153f88952b7670399715c2f88f1ecf0a94a in Neutron that # put the extension call in Pike+ *before* the precommit - # calls happen in network creation. I believe this is a but + # calls happen in network creation. I believe this is a bug # and should be discussed with the Neutron team. ext_dict = self.get_network_extn_db(session, net_db.id) else: ext_dict = self.make_network_extn_db_conf_dict( net_db.aim_extension_mapping, net_db.aim_extension_cidr_mapping, - net_db.aim_extension_domain_mapping) + net_db.aim_extension_domain_mapping, + net_db.aim_extension_extra_contract_mapping) if cisco_apic.EXTERNAL_NETWORK in ext_dict: dn = ext_dict.pop(cisco_apic.EXTERNAL_NETWORK) a_ext_net = aim_resource.ExternalNetwork.from_dn(dn) @@ -5295,7 +5341,17 @@ class ApicMechanismDriver(api_plus.MechanismDriver, contract = self._map_router( mgr.expected_session, router_db, True) router_contract_names.add(contract.name) - router_contract_names = list(router_contract_names) + + provided_contract_names = ( + router_contract_names | + set([x.contract_name for x in + net_db.aim_extension_extra_contract_mapping + if x.provides])) + consumed_contract_names = ( + router_contract_names | + set([x.contract_name for x in + net_db.aim_extension_extra_contract_mapping + if not x.provides])) # REVISIT: Refactor to share code. dname = aim_utils.sanitize_display_name(net_db.name) @@ -5313,8 +5369,8 @@ class ApicMechanismDriver(api_plus.MechanismDriver, epg.display_name = dname epg.bd_name = bd.name epg.policy_enforcement_pref = 'unenforced' - epg.provided_contract_names = router_contract_names - epg.consumed_contract_names = router_contract_names + epg.provided_contract_names = provided_contract_names + epg.consumed_contract_names = consumed_contract_names epg.openstack_vmm_domain_names = [] epg.physical_domain_names = [] epg.vmm_domains = [] diff --git a/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py b/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py index a2b1633c0..59ad0beb2 100644 --- a/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py +++ b/gbpservice/neutron/tests/unit/plugins/ml2plus/test_apic_aim.py @@ -875,6 +875,10 @@ class TestAimMapping(ApicAimTestCase): aname = self.name_mapper.network(None, net['id']) router_anames = [self.name_mapper.router(None, router['id']) for router in routers or []] + provided_contract_names = set( + router_anames + net['apic:extra_provided_contracts']) + consumed_contract_names = set( + router_anames + net['apic:extra_consumed_contracts']) if routers: if vrf: @@ -945,9 +949,9 @@ class TestAimMapping(ApicAimTestCase): self.assertEqual(aname, aim_epg.name) self.assertEqual(net['name'], aim_epg.display_name) self.assertEqual(aname, aim_epg.bd_name) - self.assertItemsEqual(router_anames, + self.assertItemsEqual(provided_contract_names, aim_epg.provided_contract_names) - self.assertItemsEqual(router_anames, + self.assertItemsEqual(consumed_contract_names, aim_epg.consumed_contract_names) # REVISIT(rkukura): Check openstack_vmm_domain_names and # physical_domain_names? @@ -1294,7 +1298,11 @@ class TestAimMapping(ApicAimTestCase): def test_network_lifecycle(self): # Test create. - net = self._make_network(self.fmt, 'net1', True)['network'] + kwargs = {'apic:extra_provided_contracts': ['ep1', 'ep2'], + 'apic:extra_consumed_contracts': ['ec1', 'ec2']} + net = self._make_network( + self.fmt, 'net1', True, arg_list=tuple(kwargs.keys()), + **kwargs)['network'] net_id = net['id'] self._check_network(net) @@ -1303,7 +1311,10 @@ class TestAimMapping(ApicAimTestCase): self._check_network(net) # Test update. - data = {'network': {'name': 'newnamefornet'}} + data = {'network': + {'name': 'newnamefornet', + 'apic:extra_provided_contracts': ['ep2', 'ep3'], + 'apic:extra_consumed_contracts': ['ec2', 'ec3']}} net = self._update('networks', net_id, data)['network'] self._check_network(net) @@ -1341,6 +1352,56 @@ class TestAimMapping(ApicAimTestCase): self.assertFalse(extn.get_network_extn_db(session, net['id'])) self._check_network_deleted(net) + def _test_invalid_network_exceptions(self, kwargs): + # Verify creating network with extra provided contracts fails. + kwargs['apic:extra_provided_contracts'] = ['ep1'] + resp = self._create_network( + self.fmt, 'net', True, arg_list=tuple(kwargs.keys()), **kwargs) + result = self.deserialize(self.fmt, resp) + self.assertEqual( + 'InvalidNetworkForExtraContracts', + result['NeutronError']['type']) + del kwargs['apic:extra_provided_contracts'] + + # Verify creating network with extra consumed contracts fails. + kwargs['apic:extra_consumed_contracts'] = ['ec1'] + resp = self._create_network( + self.fmt, 'net', True, arg_list=tuple(kwargs.keys()), **kwargs) + result = self.deserialize(self.fmt, resp) + self.assertEqual( + 'InvalidNetworkForExtraContracts', + result['NeutronError']['type']) + del kwargs['apic:extra_consumed_contracts'] + + # Create network without extra provided or consumed contracts. + net_id = self._make_network( + self.fmt, 'net', True, + arg_list=tuple(kwargs.keys()), **kwargs)['network']['id'] + + # Verify setting extra provided contracts on network fails. + result = self._update( + 'networks', net_id, + {'network': {'apic:extra_provided_contracts': ['ep1']}}, + webob.exc.HTTPBadRequest.code) + self.assertEqual( + 'InvalidNetworkForExtraContracts', + result['NeutronError']['type']) + + # Verify setting extra consumed contracts on network fails. + result = self._update( + 'networks', net_id, + {'network': {'apic:extra_consumed_contracts': ['ec1']}}, + webob.exc.HTTPBadRequest.code) + self.assertEqual( + 'InvalidNetworkForExtraContracts', + result['NeutronError']['type']) + + def test_external_network_exceptions(self): + self._test_invalid_network_exceptions({'router:external': True}) + + def test_svi_network_exceptions(self): + self._test_invalid_network_exceptions({'apic:svi': True}) + def test_security_group_lifecycle(self): # Test create sg = self._make_security_group(self.fmt, @@ -3051,6 +3112,14 @@ class TestAimMapping(ApicAimTestCase): gw3C = '10.0.3.3' gw4C = '10.0.4.3' + # Add extra contracts to two of the networks. + self._update( + 'networks', net2, {'network': {'apic:extra_provided_contracts': + ['ep1', 'ep2']}}) + self._update( + 'networks', net3, {'network': {'apic:extra_consumed_contracts': + ['ec1', 'ec2']}}) + # Check initial state with no routing. check_net(net1, sn1, [], [], [gw1A], t1) check_net(net2, sn2, [], [], [gw2A, gw2B], t1) @@ -5087,6 +5156,57 @@ class TestExtensionAttributes(ApicAimTestCase): network_id=net1['id']).all()) self.assertEqual([], db_vlans) + def test_network_with_extra_contracts_lifecycle(self): + session = db_api.get_reader_session() + extn = extn_db.ExtensionDbMixin() + + # Create network with extra contracts. + provided = ['ep1', 'ep2'] + consumed = ['ec1', 'ec2'] + kwargs = {'apic:extra_provided_contracts': provided, + 'apic:extra_consumed_contracts': consumed} + net = self._make_network( + self.fmt, 'net1', True, arg_list=tuple(kwargs.keys()), + **kwargs)['network'] + net_id = net['id'] + self.assertItemsEqual(provided, net['apic:extra_provided_contracts']) + self.assertItemsEqual(consumed, net['apic:extra_consumed_contracts']) + + # Test show. + net = self._show('networks', net_id)['network'] + self.assertItemsEqual(provided, net['apic:extra_provided_contracts']) + self.assertItemsEqual(consumed, net['apic:extra_consumed_contracts']) + + # Test list. + net = self._list( + 'networks', query_params=('id=%s' % net_id))['networks'][0] + self.assertItemsEqual(provided, net['apic:extra_provided_contracts']) + self.assertItemsEqual(consumed, net['apic:extra_consumed_contracts']) + + # Test update extra contracts. + provided = ['ep2', 'ep3'] + consumed = ['ec2', 'ec3'] + net = self._update( + 'networks', net_id, + {'network': + {'apic:extra_provided_contracts': provided, + 'apic:extra_consumed_contracts': consumed}})['network'] + self.assertItemsEqual(provided, net['apic:extra_provided_contracts']) + self.assertItemsEqual(consumed, net['apic:extra_consumed_contracts']) + + # Test show after update. + net = self._show('networks', net_id)['network'] + self.assertItemsEqual(provided, net['apic:extra_provided_contracts']) + self.assertItemsEqual(consumed, net['apic:extra_consumed_contracts']) + + # Test delete. + self._delete('networks', net_id) + self.assertFalse(extn.get_network_extn_db(session, net_id)) + db_contracts = (session.query( + extn_db.NetworkExtExtraContractDb).filter_by( + network_id=net_id).all()) + self.assertEqual([], db_contracts) + def test_external_network_lifecycle(self): session = db_api.get_reader_session() extn = extn_db.ExtensionDbMixin() diff --git a/gbpservice/neutron/tests/unit/services/grouppolicy/test_aim_validation.py b/gbpservice/neutron/tests/unit/services/grouppolicy/test_aim_validation.py index 8acb2e6e2..3db2713bd 100644 --- a/gbpservice/neutron/tests/unit/services/grouppolicy/test_aim_validation.py +++ b/gbpservice/neutron/tests/unit/services/grouppolicy/test_aim_validation.py @@ -431,7 +431,10 @@ class TestNeutronMapping(AimValidationTestCase): def test_unrouted_network(self): # Create network. - net_resp = self._make_network(self.fmt, 'net1', True) + kwargs = {'apic:extra_provided_contracts': ['ep1', 'ep2'], + 'apic:extra_consumed_contracts': ['ec1', 'ec2']} + net_resp = self._make_network( + self.fmt, 'net1', True, arg_list=tuple(kwargs.keys()), **kwargs) net = net_resp['network'] net_id = net['id'] self._validate()