[apic] Nested domain extension for networks

Attributes needed to support nesting of container
domains in OpenStack VMs are being added.

A new extension to the network resource which allows providing
the Kubernetes instance and the list of VLANs is being added. The
extension adds the following information:

apic:nested_domain_name - name of the Kubernetes domain; empty string if no nesting
apic:nested_domain_type - specific string used in APIC
apic:nested_domain_infra_vlan - this is 4093 for Kubernetes/OpenShift
apic:nested_domain_service_vlan -
apic:nested_domain_node_network_vlan -
apic:nested_domain_allowed_vlans - {'vlans_list': <[...,...]>,
                                    'vlan_ranges': <[{'start': <>, 'end': <>},
                                                     {'start': <>, 'end': <>},...]>}

The allowed VLANs specify the VLAN IDs used for tagging
Kubernetes pod and node traffic. The vlan_list can be used
for enumerating non-contiguous ranges, and/or the vlan_ranges
can be used for one one or more contiguos ranges.

Example CLI:
neutron net-create nn1 --apic:nested-domain-name kube \
                       --apic:nested-domain-type k8s \
                       --apic:nested_domain_infra_vlan 4093 \
                       --apic:nested_domain_node_network_vlan 3000 \
                       --apic:nested_domain_service_vlan 1000 \
                       --apic:nested_domain_allowed_vlans \
                       "{'vlans_list': [2, 3], \
                       'vlan_ranges': [{'start': 10, 'end': 12}]}"

Any VMs configured for host a nested domain also require
a "nested_host_vlan" configuration specified in the
"aim_mapping" section. This value is set to 4094 by default
but can be overridden to any VLAN that does not overlap
with any other VLAN used in the system. This VLAN is locally
significant and is only used so that the VM's traffic
intended for the neutron network is not dropped by the Opflex
agent configured flows.

Change-Id: Icb4ca8f4addb0f886450393c44c08d81ebfcea3c
This commit is contained in:
Sumit Naiksatam 2018-04-19 16:10:06 -07:00
parent 36a9f1af25
commit a7903e971d
10 changed files with 515 additions and 15 deletions

View File

@ -0,0 +1,59 @@
# 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.
"""nested domain attributes in cisco_apic extension
Revision ID: 4967af35820f
Revises: 1c564e737f9f
Create Date: 2018-04-19 14:18:11.909757
"""
# revision identifiers, used by Alembic.
revision = '4967af35820f'
down_revision = '1c564e737f9f'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('apic_aim_network_extensions',
sa.Column('nested_domain_name', sa.String(1024),
nullable=True))
op.add_column('apic_aim_network_extensions',
sa.Column('nested_domain_type', sa.String(1024),
nullable=True))
op.add_column('apic_aim_network_extensions',
sa.Column('nested_domain_infra_vlan', sa.Integer,
nullable=True))
op.add_column('apic_aim_network_extensions',
sa.Column('nested_domain_service_vlan', sa.Integer,
nullable=True))
op.add_column('apic_aim_network_extensions',
sa.Column('nested_domain_node_network_vlan', sa.Integer,
nullable=True))
op.create_table(
'apic_aim_network_nested_domain_allowed_vlans',
sa.Column('vlan', sa.Integer, nullable=False),
sa.PrimaryKeyConstraint('vlan'),
sa.Column('network_id', sa.String(36), nullable=False),
sa.ForeignKeyConstraint(['network_id'], ['networks.id'],
name='apic_aim_network_nested_extn_fk_network',
ondelete='CASCADE')
)
def downgrade():
pass

View File

@ -1 +1 @@
1c564e737f9f
4967af35820f

View File

@ -13,11 +13,17 @@
# License for the specific language governing permissions and limitations
# under the License.
import ast
import functools
import six
from neutron_lib.api import converters as conv
from neutron_lib.api.definitions import address_scope as as_def
from neutron_lib.api.definitions import network as net_def
from neutron_lib.api.definitions import subnet as subnet_def
from neutron_lib.api import extensions
from neutron_lib.api import validators as valid
from oslo_log import log as logging
ALIAS = 'cisco-apic'
@ -30,6 +36,12 @@ SVI = 'apic:svi'
BGP = 'apic:bgp_enable'
BGP_ASN = 'apic:bgp_asn'
BGP_TYPE = 'apic:bgp_type'
NESTED_DOMAIN_NAME = 'apic:nested_domain_name'
NESTED_DOMAIN_TYPE = 'apic:nested_domain_type'
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'
BD = 'BridgeDomain'
EPG = 'EndpointGroup'
@ -43,6 +55,103 @@ SYNC_BUILD = 'build'
SYNC_ERROR = 'error'
SYNC_NOT_APPLICABLE = 'N/A'
VLANS_LIST = 'vlans_list'
VLAN_RANGES = 'vlan_ranges'
APIC_MAX_VLAN = 4093
APIC_MIN_VLAN = 1
VLAN_RANGE_START = 'start'
VLAN_RANGE_END = 'end'
LOG = logging.getLogger(__name__)
def _validate_apic_vlan(data, key_specs=None):
if data is None:
return
try:
val = int(data)
if val >= APIC_MIN_VLAN and val <= APIC_MAX_VLAN:
return
msg = _("Invalid value for VLAN: '%s'") % data
LOG.debug(msg)
return msg
except (ValueError, TypeError):
msg = _("Invalid data format for VLAN: '%s'") % data
LOG.debug(msg)
return msg
def _validate_apic_vlan_range(data, key_specs=None):
if data is None:
return
expected_keys = [VLAN_RANGE_START, VLAN_RANGE_END]
msg = valid._verify_dict_keys(expected_keys, data)
if msg:
return msg
for k in expected_keys:
msg = _validate_apic_vlan(data[k])
if msg:
return msg
if int(data[VLAN_RANGE_START]) > int(data[VLAN_RANGE_END]):
msg = _("Invalid start, end for VLAN range %s") % data
return msg
def _validate_dict_or_string(data, key_specs=None):
if data is None:
return
if isinstance(data, str) or isinstance(data, six.string_types):
try:
data = ast.literal_eval(data)
except Exception:
msg = _("Allowed VLANs %s cannot be converted to dict") % data
return msg
return valid.validate_dict_or_none(data, key_specs)
def convert_apic_vlan(value):
if value is None:
return
else:
return int(value)
def convert_nested_domain_allowed_vlans(value):
if value is None:
return
if isinstance(value, str) or isinstance(value, six.string_types):
value = ast.literal_eval(value)
vlans_list = []
if VLANS_LIST in value:
for vlan in value[VLANS_LIST]:
vlans_list.append(convert_apic_vlan(vlan))
if VLAN_RANGES in value:
for vlan_range in value[VLAN_RANGES]:
for vrng in [VLAN_RANGE_START, VLAN_RANGE_END]:
vlan_range[vrng] = convert_apic_vlan(vlan_range[vrng])
vlans_list.extend(range(vlan_range[VLAN_RANGE_START],
vlan_range[VLAN_RANGE_END] + 1))
# eliminate duplicates
vlans_list = list(set(vlans_list))
# sort
vlans_list.sort()
value[VLANS_LIST] = vlans_list
return value
valid.validators['type:apic_vlan'] = _validate_apic_vlan
valid.validators['type:apic_vlan_list'] = functools.partial(
valid._validate_list_of_items, _validate_apic_vlan)
valid.validators['type:apic_vlan_range_list'] = functools.partial(
valid._validate_list_of_items, _validate_apic_vlan_range)
valid.validators['type:dict_or_string'] = _validate_dict_or_string
APIC_ATTRIBUTES = {
DIST_NAMES: {'allow_post': False, 'allow_put': False, 'is_visible': True},
SYNC_STATE: {'allow_post': False, 'allow_put': False, 'is_visible': True}
@ -69,6 +178,45 @@ NET_ATTRIBUTES = {
'is_visible': True, 'default': "0",
'validate': {'type:non_negative': None},
},
NESTED_DOMAIN_NAME: {
'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': '',
'validate': {'type:string': None},
},
NESTED_DOMAIN_TYPE: {
'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': '',
'validate': {'type:string': None},
},
NESTED_DOMAIN_INFRA_VLAN: {
'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': None,
'validate': {'type:apic_vlan': None},
'convert_to': convert_apic_vlan,
},
NESTED_DOMAIN_SERVICE_VLAN: {
'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': None,
'validate': {'type:apic_vlan': None},
'convert_to': convert_apic_vlan,
},
NESTED_DOMAIN_NODE_NETWORK_VLAN: {
'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': None,
'validate': {'type:apic_vlan': None},
'convert_to': convert_apic_vlan,
},
NESTED_DOMAIN_ALLOWED_VLANS: {
'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': None,
'validate': {
'type:dict_or_string': {
VLANS_LIST: {'type:apic_vlan_list': None},
VLAN_RANGES: {'type:apic_vlan_range_list': None},
}
},
'convert_to': convert_nested_domain_allowed_vlans,
},
}
EXT_NET_ATTRIBUTES = {

View File

@ -43,6 +43,11 @@ class NetworkExtensionDb(model_base.BASEV2):
backref=orm.backref(
'aim_extension_mapping', lazy='joined',
uselist=False, cascade='delete'))
nested_domain_name = sa.Column(sa.String(1024), nullable=True)
nested_domain_type = sa.Column(sa.String(1024), nullable=True)
nested_domain_infra_vlan = sa.Column(sa.Integer, nullable=True)
nested_domain_service_vlan = sa.Column(sa.Integer, nullable=True)
nested_domain_node_network_vlan = sa.Column(sa.Integer, nullable=True)
class NetworkExtensionCidrDb(model_base.BASEV2):
@ -55,6 +60,16 @@ class NetworkExtensionCidrDb(model_base.BASEV2):
cidr = sa.Column(sa.String(64), primary_key=True)
class NetworkExtNestedDomainAllowedVlansDb(model_base.BASEV2):
__tablename__ = 'apic_aim_network_nested_domain_allowed_vlans'
# There is a single pool of VLANs for an APIC
vlan = sa.Column(sa.Integer(), primary_key=True)
network_id = sa.Column(
sa.String(36), sa.ForeignKey('networks.id', ondelete="CASCADE"))
class SubnetExtensionDb(model_base.BASEV2):
__tablename__ = 'apic_aim_subnet_extensions'
@ -87,6 +102,9 @@ class ExtensionDbMixin(object):
network_id=network_id).first())
db_cidrs = (session.query(NetworkExtensionCidrDb).filter_by(
network_id=network_id).all())
db_vlans = (session.query(
NetworkExtNestedDomainAllowedVlansDb).filter_by(
network_id=network_id).all())
result = {}
if db_obj:
self._set_if_not_none(result, cisco_apic.EXTERNAL_NETWORK,
@ -97,6 +115,19 @@ class ExtensionDbMixin(object):
result[cisco_apic.BGP] = db_obj['bgp_enable']
result[cisco_apic.BGP_TYPE] = db_obj['bgp_type']
result[cisco_apic.BGP_ASN] = db_obj['bgp_asn']
result[cisco_apic.NESTED_DOMAIN_NAME] = (
db_obj['nested_domain_name'])
result[cisco_apic.NESTED_DOMAIN_TYPE] = (
db_obj['nested_domain_type'])
result[cisco_apic.NESTED_DOMAIN_INFRA_VLAN] = (
db_obj['nested_domain_infra_vlan'])
result[cisco_apic.NESTED_DOMAIN_SERVICE_VLAN] = (
db_obj['nested_domain_service_vlan'])
result[cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN] = (
db_obj['nested_domain_node_network_vlan'])
result[cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS] = (
[c['vlan'] for c in db_vlans])
if result.get(cisco_apic.EXTERNAL_NETWORK):
result[cisco_apic.EXTERNAL_CIDRS] = [c['cidr'] for c in db_cidrs]
@ -120,6 +151,21 @@ class ExtensionDbMixin(object):
db_obj['bgp_type'] = res_dict[cisco_apic.BGP_TYPE]
if cisco_apic.BGP_ASN in res_dict:
db_obj['bgp_asn'] = res_dict[cisco_apic.BGP_ASN]
if cisco_apic.NESTED_DOMAIN_NAME in res_dict:
db_obj['nested_domain_name'] = res_dict[
cisco_apic.NESTED_DOMAIN_NAME]
if cisco_apic.NESTED_DOMAIN_TYPE in res_dict:
db_obj['nested_domain_type'] = res_dict[
cisco_apic.NESTED_DOMAIN_TYPE]
if cisco_apic.NESTED_DOMAIN_INFRA_VLAN in res_dict:
db_obj['nested_domain_infra_vlan'] = res_dict[
cisco_apic.NESTED_DOMAIN_INFRA_VLAN]
if cisco_apic.NESTED_DOMAIN_SERVICE_VLAN in res_dict:
db_obj['nested_domain_service_vlan'] = res_dict[
cisco_apic.NESTED_DOMAIN_SERVICE_VLAN]
if cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN in res_dict:
db_obj['nested_domain_node_network_vlan'] = res_dict[
cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN]
session.add(db_obj)
if cisco_apic.EXTERNAL_CIDRS in res_dict:
@ -127,6 +173,12 @@ class ExtensionDbMixin(object):
res_dict[cisco_apic.EXTERNAL_CIDRS],
network_id=network_id)
if cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS in res_dict:
self._update_list_attr(
session, NetworkExtNestedDomainAllowedVlansDb, 'vlan',
res_dict[cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS],
network_id=network_id)
def get_network_ids_by_ext_net_dn(self, session, dn, lock_update=False):
ids = session.query(NetworkExtensionDb.network_id).filter_by(
external_network_dn=dn)
@ -187,6 +239,8 @@ class ExtensionDbMixin(object):
def _update_list_attr(self, session, db_model, column,
new_values, **filters):
if new_values is None:
return
rows = session.query(db_model).filter_by(**filters).all()
new_values = set(new_values)
for r in rows:

View File

@ -99,7 +99,23 @@ class ApicExtensionDriver(api_plus.ExtensionDriver,
res_dict = {cisco_apic.SVI: is_svi,
cisco_apic.BGP: is_bgp_enabled,
cisco_apic.BGP_TYPE: bgp_type,
cisco_apic.BGP_ASN: asn}
cisco_apic.BGP_ASN: asn,
cisco_apic.NESTED_DOMAIN_NAME:
data.get(cisco_apic.NESTED_DOMAIN_NAME),
cisco_apic.NESTED_DOMAIN_TYPE:
data.get(cisco_apic.NESTED_DOMAIN_TYPE),
cisco_apic.NESTED_DOMAIN_INFRA_VLAN:
data.get(cisco_apic.NESTED_DOMAIN_INFRA_VLAN),
cisco_apic.NESTED_DOMAIN_SERVICE_VLAN:
data.get(cisco_apic.NESTED_DOMAIN_SERVICE_VLAN),
cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN:
data.get(cisco_apic.NESTED_DOMAIN_NODE_NETWORK_VLAN),
}
if cisco_apic.VLANS_LIST in (data.get(
cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS) or {}):
res_dict.update({cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS:
data.get(cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS)[
cisco_apic.VLANS_LIST]})
self.set_network_extn_db(plugin_context.session, result['id'],
res_dict)
result.update(res_dict)
@ -129,25 +145,43 @@ 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 can be updated.
if (cisco_apic.EXTERNAL_CIDRS not in data and
cisco_apic.BGP not in data and
cisco_apic.BGP_TYPE not in data and
cisco_apic.BGP_ASN not in data):
# External_cidr, bgp_enable, bgp_type and bgp_asn or
# nested domain attributes could be updated
update_attrs = [
cisco_apic.EXTERNAL_CIDRS, cisco_apic.BGP, cisco_apic.BGP_TYPE,
cisco_apic.BGP_ASN,
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_ALLOWED_VLANS]
if not(set(update_attrs) & set(data.keys())):
return
res_dict = {}
if result.get(cisco_apic.DIST_NAMES, {}).get(
cisco_apic.EXTERNAL_NETWORK):
if cisco_apic.EXTERNAL_CIDRS in data:
res_dict = {cisco_apic.EXTERNAL_CIDRS:
data[cisco_apic.EXTERNAL_CIDRS]}
res_dict.update({cisco_apic.EXTERNAL_CIDRS:
data[cisco_apic.EXTERNAL_CIDRS]})
self.validate_bgp_params(data, result)
if cisco_apic.BGP in data:
res_dict.update({cisco_apic.BGP: data[cisco_apic.BGP]})
if cisco_apic.BGP_TYPE in data:
res_dict.update({cisco_apic.BGP_TYPE: data[cisco_apic.BGP_TYPE]})
if cisco_apic.BGP_ASN in data:
res_dict.update({cisco_apic.BGP_ASN: data[cisco_apic.BGP_ASN]})
ext_keys = [cisco_apic.BGP, cisco_apic.BGP_TYPE, cisco_apic.BGP_ASN,
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]
for e_k in ext_keys:
if e_k in data:
res_dict.update({e_k: data[e_k]})
if cisco_apic.VLANS_LIST in (data.get(
cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS) or {}):
res_dict.update({cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS:
data.get(cisco_apic.NESTED_DOMAIN_ALLOWED_VLANS)[
cisco_apic.VLANS_LIST]})
if res_dict:
self.set_network_extn_db(plugin_context.session, result['id'],
res_dict)

View File

@ -118,8 +118,19 @@ opts = [
help=_('If True, advertise network MTU values if core plugin '
'calculates them. MTU is advertised to running '
'instances via DHCP and RA MTU options.')),
cfg.IntOpt('nested_host_vlan',
default=4094,
help=_("This is a locally siginificant VLAN used to provide "
"connectivity to the OpenStack VM when configured "
"to host the nested domain (Kubernetes/OpenShift). "
"Any traffic originating from the VM and intended "
"to go on the Neutron network, is tagged with this "
"VLAN. The VLAN is stripped by the Opflex installed "
"flows on the integration bridge and the traffic is "
"forwarded on the Neutron network.")),
]
cfg.CONF.register_opts(opts, "aim_mapping")
@ -177,6 +188,8 @@ class AIMMappingDriver(nrd.CommonNeutronBase, aim_rpc.AIMMappingRPCMixin):
LOG.info('Implicit AIM contracts will be created '
'for l3_policies which do not have them.')
self._create_per_l3p_implicit_contracts()
self._nested_host_vlan = (
cfg.CONF.aim_mapping.nested_host_vlan)
@log.log_method_call
def start_rpc_listeners(self):
@ -2409,6 +2422,16 @@ class AIMMappingDriver(nrd.CommonNeutronBase, aim_rpc.AIMMappingRPCMixin):
network = self._get_network(context, port['network_id'])
return network.get('dns_domain')
def _get_nested_domain(self, context, port):
network = self._get_network(context, port['network_id'])
return (network.get('apic:nested_domain_name'),
network.get('apic:nested_domain_type'),
network.get('apic:nested_domain_infra_vlan'),
network.get('apic:nested_domain_service_vlan'),
network.get('apic:nested_domain_node_network_vlan'),
network.get('apic:nested_domain_allowed_vlans'),
self._nested_host_vlan)
def _create_per_l3p_implicit_contracts(self):
admin_context = n_context.get_admin_context()
context = type('', (object,), {})()

View File

@ -246,6 +246,7 @@ class AIMMappingRPCMixin(ha_ip_db.HAIPOwnerDbMixin):
self._add_segmentation_label_details(context, port, details)
self._set_dhcp_lease_time(details)
details.pop('_cache', None)
self._add_nested_domain_details(context, port, details)
LOG.debug("Details for port %s : %s", port['id'], details)
return details
@ -324,6 +325,25 @@ class AIMMappingRPCMixin(ha_ip_db.HAIPOwnerDbMixin):
details['vrf_subnets'] = self._get_vrf_subnets(context, tenant_name,
name, details)
# Child class needs to support:
# - self._get_nested_domain(context, port)
def _add_nested_domain_details(self, context, port, details):
# This method needs to define requirements for this Mixin's child
# classes in order to fill the following result parameters:
# - nested_domain_name;
# - nested_domain_type;
# - nested_domain_infra_vlan;
# - nested_domain_service_vlan;
# - nested_domain_node_network_vlan;
# - nested_domain_allowed_vlans;
(details['nested_domain_name'], details['nested_domain_type'],
details['nested_domain_infra_vlan'],
details['nested_domain_service_vlan'],
details['nested_domain_node_network_vlan'],
details['nested_domain_allowed_vlans'],
details['nested_host_vlan']) = (
self._get_nested_domain(context, port))
# Child class needs to support:
# - self._get_segmentation_labels(context, port, details)
def _add_segmentation_label_details(self, context, port, details):

View File

@ -4401,6 +4401,61 @@ class TestExtensionAttributes(ApicAimTestCase):
self._delete('networks', net2['id'])
self.assertFalse(extn.get_network_extn_db(session, net2['id']))
def test_network_with_nested_domain_lifecycle(self):
session = db_api.get_session()
extn = extn_db.ExtensionDbMixin()
vlan_dict = {'vlans_list': ['2', '3', '4', '3'],
'vlan_ranges': [{'start': '6', 'end': '9'},
{'start': '11', 'end': '14'}]}
expt_vlans = [2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14]
kwargs = {'apic:nested_domain_name': 'myk8s',
'apic:nested_domain_type': 'k8s',
'apic:nested_domain_infra_vlan': '4093',
'apic:nested_domain_service_vlan': '1000',
'apic:nested_domain_node_network_vlan': '1001',
'apic:nested_domain_allowed_vlans': vlan_dict,
}
net1 = self._make_network(self.fmt, 'net1', True,
arg_list=tuple(kwargs.keys()),
**kwargs)['network']
self.assertEqual('myk8s', net1['apic:nested_domain_name'])
self.assertEqual('k8s', net1['apic:nested_domain_type'])
self.assertEqual(4093, net1['apic:nested_domain_infra_vlan'])
self.assertEqual(1000, net1['apic:nested_domain_service_vlan'])
self.assertEqual(1001, net1['apic:nested_domain_node_network_vlan'])
self.assertItemsEqual(expt_vlans,
net1['apic:nested_domain_allowed_vlans'])
vlan_dict = {'vlans_list': ['2', '3', '4', '3']}
expt_vlans = [2, 3, 4]
data = {'network': {'apic:nested_domain_name': 'new-myk8s',
'apic:nested_domain_type': 'new-k8s',
'apic:nested_domain_infra_vlan': '1093',
'apic:nested_domain_service_vlan': '2000',
'apic:nested_domain_node_network_vlan': '2001',
'apic:nested_domain_allowed_vlans': vlan_dict}}
req = self.new_update_request('networks', data, net1['id'],
self.fmt)
resp = req.get_response(self.api)
self.assertEqual(resp.status_code, 200)
net1 = self.deserialize(self.fmt, resp)['network']
self.assertEqual('new-myk8s', net1['apic:nested_domain_name'])
self.assertEqual('new-k8s', net1['apic:nested_domain_type'])
self.assertEqual(1093, net1['apic:nested_domain_infra_vlan'])
self.assertEqual(2000, net1['apic:nested_domain_service_vlan'])
self.assertEqual(2001, net1['apic:nested_domain_node_network_vlan'])
self.assertItemsEqual(expt_vlans,
net1['apic:nested_domain_allowed_vlans'])
# Test delete.
self._delete('networks', net1['id'])
self.assertFalse(extn.get_network_extn_db(session, net1['id']))
db_vlans = (session.query(
extn_db.NetworkExtNestedDomainAllowedVlansDb).filter_by(
network_id=net1['id']).all())
self.assertEqual([], db_vlans)
def test_external_network_lifecycle(self):
session = db_api.get_reader_session()
extn = extn_db.ExtensionDbMixin()

View File

@ -0,0 +1,59 @@
# Copyright (c) 2018 Cisco Systems 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.
from neutron.tests import base
from gbpservice.neutron.extensions import cisco_apic as apic_ext
class TestAttributeValidators(base.BaseTestCase):
def test_validate_apic_vlan(self):
self.assertIsNone(apic_ext._validate_apic_vlan(None))
self.assertIsNone(apic_ext._validate_apic_vlan('10'))
self.assertIsNone(apic_ext._validate_apic_vlan(10))
class TestAttributeConverters(base.BaseTestCase):
def test_convert_apic_vlan(self):
self.assertIsInstance(apic_ext.convert_apic_vlan('2'), int)
self.assertIsInstance(apic_ext.convert_apic_vlan(2), int)
self.assertIsNone(apic_ext.convert_apic_vlan(None))
def test_convert_nested_domain_allowed_vlans(self):
test_dict_str = "{'vlans_list': [2, 3, 4], " + (
"'vlan_ranges': [{'start': 6, 'end': 9}, ") + (
"{'start': 11, 'end': 14}]}")
expt_list = [2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14]
self.assertItemsEqual(
apic_ext.convert_nested_domain_allowed_vlans(
test_dict_str)['vlans_list'], expt_list)
test_dict = {'vlans_list': [2, 3, 4],
'vlan_ranges': [{'start': 6, 'end': 9},
{'start': 11, 'end': 14}]}
expt_list = [2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14]
self.assertItemsEqual(
apic_ext.convert_nested_domain_allowed_vlans(
test_dict)['vlans_list'], expt_list)
test_dict = {'vlans_list': ['2', '3', '4', '3'],
'vlan_ranges': [{'start': '6', 'end': '9'},
{'start': '11', 'end': '14'},
{'start': '6', 'end': '9'}]}
expt_list = [2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14]
self.assertItemsEqual(
apic_ext.convert_nested_domain_allowed_vlans(
test_dict)['vlans_list'], expt_list)
self.assertIsNone(apic_ext.convert_nested_domain_allowed_vlans(None))

View File

@ -5273,6 +5273,54 @@ class TestNetworkServicePolicy(AIMBaseTestCase):
netaddr.IPAddress(allocation_pool_after_nsp[0].get('end')) + 1)
class TestNestedDomain(AIMBaseTestCase):
def setUp(self, **kwargs):
super(TestNestedDomain, self).setUp(**kwargs)
self.plugin = self._plugin
def _test_get_nested_domain_details(self, vlans_arg):
self._register_agent('host1', test_aim_md.AGENT_CONF_OPFLEX)
expt_vlans = [2, 3, 4, 6, 7, 8, 9, 11, 12, 13, 14]
kwargs = {'apic:nested_domain_name': 'myk8s',
'apic:nested_domain_type': 'k8s',
'apic:nested_domain_infra_vlan': '4093',
'apic:nested_domain_service_vlan': '1000',
'apic:nested_domain_node_network_vlan': '1001',
'apic:nested_domain_allowed_vlans': vlans_arg,
}
net = self._make_network(self.fmt, 'net1', True,
arg_list=tuple(kwargs.keys()), **kwargs)
self._make_subnet(self.fmt, net, '10.0.1.1', '10.0.1.0/24')
p1 = self._make_port(self.fmt, net['network']['id'],
device_owner='compute:')['port']
p1 = self._bind_port_to_host(p1['id'], 'host1')['port']
details = self.driver.get_gbp_details(
self._neutron_admin_context, device='tap%s' % p1['id'],
host='h1')
self.assertEqual('myk8s', details['nested_domain_name'])
self.assertEqual('k8s', details['nested_domain_type'])
self.assertEqual(4093, details['nested_domain_infra_vlan'])
self.assertEqual(1000, details['nested_domain_service_vlan'])
self.assertEqual(1001, details['nested_domain_node_network_vlan'])
self.assertItemsEqual(expt_vlans,
details['nested_domain_allowed_vlans'])
self.assertEqual(4094, details['nested_host_vlan'])
def test_get_nested_domain_details_vlan_dict_input(self):
vlan_dict = {'vlans_list': ['2', '3', '4', '3'],
'vlan_ranges': [{'start': '6', 'end': '9'},
{'start': '11', 'end': '14'}]}
self._test_get_nested_domain_details(vlan_dict)
def test_get_nested_domain_details_vlan_string_input(self):
vlan_str = "{'vlans_list': ['2', '3', '4', '3'], " + (
"'vlan_ranges': [{'start': '6', 'end': '9'}, ") + (
"{'start': '11', 'end': '14'}]}")
self._test_get_nested_domain_details(vlan_str)
class TestNeutronPortOperation(AIMBaseTestCase):
def setUp(self, **kwargs):