Merge "Cisco APIC ML2 mechanism driver, part 2"
This commit is contained in:
commit
e92f6cfc4a
559
neutron/plugins/ml2/drivers/cisco/apic/apic_manager.py
Normal file
559
neutron/plugins/ml2/drivers/cisco/apic/apic_manager.py
Normal file
@ -0,0 +1,559 @@
|
|||||||
|
# Copyright (c) 2014 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.
|
||||||
|
#
|
||||||
|
# @author: Arvind Somya (asomya@cisco.com), Cisco Systems Inc.
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from neutron.openstack.common import excutils
|
||||||
|
from neutron.plugins.ml2.drivers.cisco.apic import apic_client
|
||||||
|
from neutron.plugins.ml2.drivers.cisco.apic import apic_model
|
||||||
|
from neutron.plugins.ml2.drivers.cisco.apic import config
|
||||||
|
from neutron.plugins.ml2.drivers.cisco.apic import exceptions as cexc
|
||||||
|
|
||||||
|
AP_NAME = 'openstack'
|
||||||
|
CONTEXT_ENFORCED = '1'
|
||||||
|
CONTEXT_UNENFORCED = '2'
|
||||||
|
CONTEXT_DEFAULT = 'default'
|
||||||
|
DN_KEY = 'dn'
|
||||||
|
PORT_DN_PATH = 'topology/pod-1/paths-%s/pathep-[eth%s]'
|
||||||
|
SCOPE_GLOBAL = 'global'
|
||||||
|
SCOPE_TENANT = 'tenant'
|
||||||
|
TENANT_COMMON = 'common'
|
||||||
|
|
||||||
|
|
||||||
|
def group_by_ranges(i):
|
||||||
|
"""Group a list of numbers into tuples representing contiguous ranges."""
|
||||||
|
for a, b in itertools.groupby(enumerate(sorted(i)), lambda (x, y): y - x):
|
||||||
|
b = list(b)
|
||||||
|
yield b[0][1], b[-1][1]
|
||||||
|
|
||||||
|
|
||||||
|
class APICManager(object):
|
||||||
|
"""Class to manage APIC translations and workflow.
|
||||||
|
|
||||||
|
This class manages translation from Neutron objects to APIC
|
||||||
|
managed objects and contains workflows to implement these
|
||||||
|
translations.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.db = apic_model.ApicDbModel()
|
||||||
|
|
||||||
|
apic_conf = cfg.CONF.ml2_cisco_apic
|
||||||
|
self.switch_dict = config.create_switch_dictionary()
|
||||||
|
|
||||||
|
# Connect to the the APIC
|
||||||
|
self.apic = apic_client.RestClient(
|
||||||
|
apic_conf.apic_host,
|
||||||
|
apic_conf.apic_port,
|
||||||
|
apic_conf.apic_username,
|
||||||
|
apic_conf.apic_password
|
||||||
|
)
|
||||||
|
|
||||||
|
self.port_profiles = {}
|
||||||
|
self.vmm_domain = None
|
||||||
|
self.phys_domain = None
|
||||||
|
self.vlan_ns = None
|
||||||
|
self.node_profiles = {}
|
||||||
|
self.entity_profile = None
|
||||||
|
self.function_profile = None
|
||||||
|
self.clear_node_profiles = apic_conf.apic_clear_node_profiles
|
||||||
|
|
||||||
|
def ensure_infra_created_on_apic(self):
|
||||||
|
"""Ensure the infrastructure is setup.
|
||||||
|
|
||||||
|
Loop over the switch dictionary from the config and
|
||||||
|
setup profiles for switches, modules and ports
|
||||||
|
"""
|
||||||
|
# Loop over switches
|
||||||
|
for switch in self.switch_dict:
|
||||||
|
# Create a node profile for this switch
|
||||||
|
self.ensure_node_profile_created_for_switch(switch)
|
||||||
|
|
||||||
|
# Check if a port profile exists for this node
|
||||||
|
ppname = self.check_infra_port_profiles(switch)
|
||||||
|
|
||||||
|
# Gather port ranges for this switch
|
||||||
|
modules = self.gather_infra_module_ports(switch)
|
||||||
|
|
||||||
|
# Setup each module and port range
|
||||||
|
for module in modules:
|
||||||
|
profile = self.db.get_profile_for_module(switch, ppname,
|
||||||
|
module)
|
||||||
|
if not profile:
|
||||||
|
# Create host port selector for this module
|
||||||
|
hname = uuid.uuid4()
|
||||||
|
try:
|
||||||
|
self.apic.infraHPortS.create(ppname, hname, 'range')
|
||||||
|
# Add relation to the function profile
|
||||||
|
fpdn = self.function_profile[DN_KEY]
|
||||||
|
self.apic.infraRsAccBaseGrp.create(ppname, hname,
|
||||||
|
'range', tDn=fpdn)
|
||||||
|
modules[module].sort()
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
self.apic.infraHPortS.delete(ppname, hname,
|
||||||
|
'range')
|
||||||
|
else:
|
||||||
|
hname = profile.hpselc_id
|
||||||
|
|
||||||
|
ranges = group_by_ranges(modules[module])
|
||||||
|
# Add this module and ports to the profile
|
||||||
|
for prange in ranges:
|
||||||
|
# Check if this port block is already added to the profile
|
||||||
|
if not self.db.get_profile_for_module_and_ports(
|
||||||
|
switch, ppname, module, prange[0], prange[-1]):
|
||||||
|
# Create port block for this port range
|
||||||
|
pbname = uuid.uuid4()
|
||||||
|
self.apic.infraPortBlk.create(ppname, hname, 'range',
|
||||||
|
pbname, fromCard=module,
|
||||||
|
toCard=module,
|
||||||
|
fromPort=str(prange[0]),
|
||||||
|
toPort=str(prange[-1]))
|
||||||
|
# Add DB row
|
||||||
|
self.db.add_profile_for_module_and_ports(
|
||||||
|
switch, ppname, hname, module,
|
||||||
|
prange[0], prange[-1])
|
||||||
|
|
||||||
|
def check_infra_port_profiles(self, switch):
|
||||||
|
"""Check and create infra port profiles for a node."""
|
||||||
|
sprofile = self.db.get_port_profile_for_node(switch)
|
||||||
|
ppname = None
|
||||||
|
if not sprofile:
|
||||||
|
# Generate uuid for port profile name
|
||||||
|
ppname = uuid.uuid4()
|
||||||
|
try:
|
||||||
|
# Create port profile for this switch
|
||||||
|
pprofile = self.ensure_port_profile_created_on_apic(ppname)
|
||||||
|
# Add port profile to node profile
|
||||||
|
ppdn = pprofile[DN_KEY]
|
||||||
|
self.apic.infraRsAccPortP.create(switch, ppdn)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Delete port profile
|
||||||
|
self.apic.infraAccPortP.delete(ppname)
|
||||||
|
else:
|
||||||
|
ppname = sprofile.profile_id
|
||||||
|
|
||||||
|
return ppname
|
||||||
|
|
||||||
|
def gather_infra_module_ports(self, switch):
|
||||||
|
"""Build modules and ports per module dictionary."""
|
||||||
|
ports = self.switch_dict[switch]
|
||||||
|
# Gather common modules
|
||||||
|
modules = {}
|
||||||
|
for port in ports:
|
||||||
|
module, sw_port = port.split('/')
|
||||||
|
if module not in modules:
|
||||||
|
modules[module] = []
|
||||||
|
modules[module].append(int(sw_port))
|
||||||
|
|
||||||
|
return modules
|
||||||
|
|
||||||
|
def ensure_context_unenforced(self, tenant_id=TENANT_COMMON,
|
||||||
|
name=CONTEXT_DEFAULT):
|
||||||
|
"""Set the specified tenant's context to unenforced."""
|
||||||
|
ctx = self.apic.fvCtx.get(tenant_id, name)
|
||||||
|
if not ctx:
|
||||||
|
self.apic.fvCtx.create(tenant_id, name,
|
||||||
|
pcEnfPref=CONTEXT_UNENFORCED)
|
||||||
|
elif ctx['pcEnfPref'] != CONTEXT_UNENFORCED:
|
||||||
|
self.apic.fvCtx.update(tenant_id, name,
|
||||||
|
pcEnfPref=CONTEXT_UNENFORCED)
|
||||||
|
|
||||||
|
def ensure_context_enforced(self, tenant_id=TENANT_COMMON,
|
||||||
|
name=CONTEXT_DEFAULT):
|
||||||
|
"""Set the specified tenant's context to enforced."""
|
||||||
|
ctx = self.apic.fvCtx.get(tenant_id, name)
|
||||||
|
if not ctx:
|
||||||
|
self.apic.fvCtx.create(tenant_id, name, pcEnfPref=CONTEXT_ENFORCED)
|
||||||
|
elif ctx['pcEnfPref'] != CONTEXT_ENFORCED:
|
||||||
|
self.apic.fvCtx.update(tenant_id, name, pcEnfPref=CONTEXT_ENFORCED)
|
||||||
|
|
||||||
|
def ensure_entity_profile_created_on_apic(self, name):
|
||||||
|
"""Create the infrastructure entity profile."""
|
||||||
|
if self.clear_node_profiles:
|
||||||
|
self.apic.infraAttEntityP.delete(name)
|
||||||
|
self.entity_profile = self.apic.infraAttEntityP.get(name)
|
||||||
|
if not self.entity_profile:
|
||||||
|
try:
|
||||||
|
phys_dn = self.phys_domain[DN_KEY]
|
||||||
|
self.apic.infraAttEntityP.create(name)
|
||||||
|
# Attach phys domain to entity profile
|
||||||
|
self.apic.infraRsDomP.create(name, phys_dn)
|
||||||
|
self.entity_profile = self.apic.infraAttEntityP.get(name)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Delete the created entity profile
|
||||||
|
self.apic.infraAttEntityP.delete(name)
|
||||||
|
|
||||||
|
def ensure_function_profile_created_on_apic(self, name):
|
||||||
|
"""Create the infrastructure function profile."""
|
||||||
|
if self.clear_node_profiles:
|
||||||
|
self.apic.infraAccPortGrp.delete(name)
|
||||||
|
self.function_profile = self.apic.infraAccPortGrp.get(name)
|
||||||
|
if not self.function_profile:
|
||||||
|
try:
|
||||||
|
self.apic.infraAccPortGrp.create(name)
|
||||||
|
# Attach entity profile to function profile
|
||||||
|
entp_dn = self.entity_profile[DN_KEY]
|
||||||
|
self.apic.infraRsAttEntP.create(name, tDn=entp_dn)
|
||||||
|
self.function_profile = self.apic.infraAccPortGrp.get(name)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Delete the created function profile
|
||||||
|
self.apic.infraAccPortGrp.delete(name)
|
||||||
|
|
||||||
|
def ensure_node_profile_created_for_switch(self, switch_id):
|
||||||
|
"""Creates a switch node profile.
|
||||||
|
|
||||||
|
Create a node profile for a switch and add a switch
|
||||||
|
to the leaf node selector
|
||||||
|
"""
|
||||||
|
if self.clear_node_profiles:
|
||||||
|
self.apic.infraNodeP.delete(switch_id)
|
||||||
|
self.db.delete_profile_for_node(switch_id)
|
||||||
|
sobj = self.apic.infraNodeP.get(switch_id)
|
||||||
|
if not sobj:
|
||||||
|
try:
|
||||||
|
# Create Node profile
|
||||||
|
self.apic.infraNodeP.create(switch_id)
|
||||||
|
# Create leaf selector
|
||||||
|
lswitch_id = uuid.uuid4()
|
||||||
|
self.apic.infraLeafS.create(switch_id, lswitch_id, 'range')
|
||||||
|
# Add leaf nodes to the selector
|
||||||
|
name = uuid.uuid4()
|
||||||
|
self.apic.infraNodeBlk.create(switch_id, lswitch_id, 'range',
|
||||||
|
name, from_=switch_id,
|
||||||
|
to_=switch_id)
|
||||||
|
sobj = self.apic.infraNodeP.get(switch_id)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Remove the node profile
|
||||||
|
self.apic.infraNodeP.delete(switch_id)
|
||||||
|
|
||||||
|
self.node_profiles[switch_id] = {
|
||||||
|
'object': sobj
|
||||||
|
}
|
||||||
|
|
||||||
|
def ensure_port_profile_created_on_apic(self, name):
|
||||||
|
"""Create a port profile."""
|
||||||
|
try:
|
||||||
|
self.apic.infraAccPortP.create(name)
|
||||||
|
return self.apic.infraAccPortP.get(name)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
self.apic.infraAccPortP.delete(name)
|
||||||
|
|
||||||
|
def ensure_vmm_domain_created_on_apic(self, vmm_name,
|
||||||
|
vlan_ns=None, vxlan_ns=None):
|
||||||
|
"""Create Virtual Machine Manager domain.
|
||||||
|
|
||||||
|
Creates the VMM domain on the APIC and adds a VLAN or VXLAN
|
||||||
|
namespace to that VMM domain.
|
||||||
|
TODO (asomya): Add VXLAN support
|
||||||
|
"""
|
||||||
|
provider = 'VMware'
|
||||||
|
if self.clear_node_profiles:
|
||||||
|
self.apic.vmmDomP.delete(provider, vmm_name)
|
||||||
|
self.vmm_domain = self.apic.vmmDomP.get(provider, vmm_name)
|
||||||
|
if not self.vmm_domain:
|
||||||
|
try:
|
||||||
|
self.apic.vmmDomP.create(provider, vmm_name)
|
||||||
|
if vlan_ns:
|
||||||
|
vlan_ns_dn = vlan_ns[DN_KEY]
|
||||||
|
self.apic.infraRsVlanNs__vmm.create(provider, vmm_name,
|
||||||
|
tDn=vlan_ns_dn)
|
||||||
|
self.vmm_domain = self.apic.vmmDomP.get(provider, vmm_name)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Delete the VMM domain
|
||||||
|
self.apic.vmmDomP.delete(provider, vmm_name)
|
||||||
|
|
||||||
|
def ensure_phys_domain_created_on_apic(self, phys_name,
|
||||||
|
vlan_ns=None):
|
||||||
|
"""Create Virtual Machine Manager domain.
|
||||||
|
|
||||||
|
Creates the VMM domain on the APIC and adds a VLAN or VXLAN
|
||||||
|
namespace to that VMM domain.
|
||||||
|
TODO (asomya): Add VXLAN support
|
||||||
|
"""
|
||||||
|
if self.clear_node_profiles:
|
||||||
|
self.apic.physDomP.delete(phys_name)
|
||||||
|
self.phys_domain = self.apic.physDomP.get(phys_name)
|
||||||
|
if not self.phys_domain:
|
||||||
|
try:
|
||||||
|
self.apic.physDomP.create(phys_name)
|
||||||
|
if vlan_ns:
|
||||||
|
vlan_ns_dn = vlan_ns[DN_KEY]
|
||||||
|
self.apic.infraRsVlanNs__phys.create(phys_name,
|
||||||
|
tDn=vlan_ns_dn)
|
||||||
|
self.phys_domain = self.apic.physDomP.get(phys_name)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Delete the physical domain
|
||||||
|
self.apic.physDomP.delete(phys_name)
|
||||||
|
|
||||||
|
def ensure_vlan_ns_created_on_apic(self, name, vlan_min, vlan_max):
|
||||||
|
"""Creates a static VLAN namespace with the given vlan range."""
|
||||||
|
ns_args = name, 'static'
|
||||||
|
if self.clear_node_profiles:
|
||||||
|
self.apic.fvnsVlanInstP.delete(name, 'dynamic')
|
||||||
|
self.apic.fvnsVlanInstP.delete(*ns_args)
|
||||||
|
self.vlan_ns = self.apic.fvnsVlanInstP.get(*ns_args)
|
||||||
|
if not self.vlan_ns:
|
||||||
|
try:
|
||||||
|
self.apic.fvnsVlanInstP.create(*ns_args)
|
||||||
|
vlan_min = 'vlan-' + vlan_min
|
||||||
|
vlan_max = 'vlan-' + vlan_max
|
||||||
|
ns_blk_args = name, 'static', vlan_min, vlan_max
|
||||||
|
vlan_encap = self.apic.fvnsEncapBlk__vlan.get(*ns_blk_args)
|
||||||
|
if not vlan_encap:
|
||||||
|
ns_kw_args = {
|
||||||
|
'name': 'encap',
|
||||||
|
'from': vlan_min,
|
||||||
|
'to': vlan_max
|
||||||
|
}
|
||||||
|
self.apic.fvnsEncapBlk__vlan.create(*ns_blk_args,
|
||||||
|
**ns_kw_args)
|
||||||
|
self.vlan_ns = self.apic.fvnsVlanInstP.get(*ns_args)
|
||||||
|
return self.vlan_ns
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Delete the vlan namespace
|
||||||
|
self.apic.fvnsVlanInstP.delete(*ns_args)
|
||||||
|
|
||||||
|
def ensure_tenant_created_on_apic(self, tenant_id):
|
||||||
|
"""Make sure a tenant exists on the APIC."""
|
||||||
|
if not self.apic.fvTenant.get(tenant_id):
|
||||||
|
self.apic.fvTenant.create(tenant_id)
|
||||||
|
|
||||||
|
def ensure_bd_created_on_apic(self, tenant_id, bd_id):
|
||||||
|
"""Creates a Bridge Domain on the APIC."""
|
||||||
|
if not self.apic.fvBD.get(tenant_id, bd_id):
|
||||||
|
try:
|
||||||
|
self.apic.fvBD.create(tenant_id, bd_id)
|
||||||
|
# Add default context to the BD
|
||||||
|
self.ensure_context_enforced()
|
||||||
|
self.apic.fvRsCtx.create(tenant_id, bd_id,
|
||||||
|
tnFvCtxName=CONTEXT_DEFAULT)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Delete the bridge domain
|
||||||
|
self.apic.fvBD.delete(tenant_id, bd_id)
|
||||||
|
|
||||||
|
def delete_bd_on_apic(self, tenant_id, bd_id):
|
||||||
|
"""Deletes a Bridge Domain from the APIC."""
|
||||||
|
self.apic.fvBD.delete(tenant_id, bd_id)
|
||||||
|
|
||||||
|
def ensure_subnet_created_on_apic(self, tenant_id, bd_id, gw_ip):
|
||||||
|
"""Creates a subnet on the APIC
|
||||||
|
|
||||||
|
The gateway ip (gw_ip) should be specified as a CIDR
|
||||||
|
e.g. 10.0.0.1/24
|
||||||
|
"""
|
||||||
|
if not self.apic.fvSubnet.get(tenant_id, bd_id, gw_ip):
|
||||||
|
self.apic.fvSubnet.create(tenant_id, bd_id, gw_ip)
|
||||||
|
|
||||||
|
def ensure_filter_created_on_apic(self, tenant_id, filter_id):
|
||||||
|
"""Create a filter on the APIC."""
|
||||||
|
if not self.apic.vzFilter.get(tenant_id, filter_id):
|
||||||
|
self.apic.vzFilter.create(tenant_id, filter_id)
|
||||||
|
|
||||||
|
def ensure_epg_created_for_network(self, tenant_id, network_id, net_name):
|
||||||
|
"""Creates an End Point Group on the APIC.
|
||||||
|
|
||||||
|
Create a new EPG on the APIC for the network spcified. This information
|
||||||
|
is also tracked in the local DB and associate the bridge domain for the
|
||||||
|
network with the EPG created.
|
||||||
|
"""
|
||||||
|
# Check if an EPG is already present for this network
|
||||||
|
epg = self.db.get_epg_for_network(network_id)
|
||||||
|
if epg:
|
||||||
|
return epg
|
||||||
|
|
||||||
|
# Create a new EPG on the APIC
|
||||||
|
epg_uid = '-'.join([str(net_name), str(uuid.uuid4())])
|
||||||
|
try:
|
||||||
|
self.apic.fvAEPg.create(tenant_id, AP_NAME, epg_uid)
|
||||||
|
|
||||||
|
# Add bd to EPG
|
||||||
|
bd = self.apic.fvBD.get(tenant_id, network_id)
|
||||||
|
bd_name = bd['name']
|
||||||
|
|
||||||
|
# Create fvRsBd
|
||||||
|
self.apic.fvRsBd.create(tenant_id, AP_NAME, epg_uid,
|
||||||
|
tnFvBDName=bd_name)
|
||||||
|
|
||||||
|
# Add EPG to physical domain
|
||||||
|
phys_dn = self.phys_domain[DN_KEY]
|
||||||
|
self.apic.fvRsDomAtt.create(tenant_id, AP_NAME, epg_uid, phys_dn)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Delete the EPG
|
||||||
|
self.apic.fvAEPg.delete(tenant_id, AP_NAME, epg_uid)
|
||||||
|
|
||||||
|
# Stick it in the DB
|
||||||
|
epg = self.db.write_epg_for_network(network_id, epg_uid)
|
||||||
|
|
||||||
|
return epg
|
||||||
|
|
||||||
|
def delete_epg_for_network(self, tenant_id, network_id):
|
||||||
|
"""Deletes the EPG from the APIC and removes it from the DB."""
|
||||||
|
# Check if an EPG is already present for this network
|
||||||
|
epg = self.db.get_epg_for_network(network_id)
|
||||||
|
if not epg:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Delete this epg
|
||||||
|
self.apic.fvAEPg.delete(tenant_id, AP_NAME, epg.epg_id)
|
||||||
|
# Remove DB row
|
||||||
|
self.db.delete_epg(epg)
|
||||||
|
|
||||||
|
def create_tenant_filter(self, tenant_id):
|
||||||
|
"""Creates a tenant filter and a generic entry under it."""
|
||||||
|
fuuid = uuid.uuid4()
|
||||||
|
try:
|
||||||
|
# Create a new tenant filter
|
||||||
|
self.apic.vzFilter.create(tenant_id, fuuid)
|
||||||
|
# Create a new entry
|
||||||
|
euuid = uuid.uuid4()
|
||||||
|
self.apic.vzEntry.create(tenant_id, fuuid, euuid)
|
||||||
|
return fuuid
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
self.apic.vzFilter.delete(tenant_id, fuuid)
|
||||||
|
|
||||||
|
def set_contract_for_epg(self, tenant_id, epg_id,
|
||||||
|
contract_id, provider=False):
|
||||||
|
"""Set the contract for an EPG.
|
||||||
|
|
||||||
|
By default EPGs are consumers to a contract. Set provider flag
|
||||||
|
for a single EPG to act as a contract provider.
|
||||||
|
"""
|
||||||
|
if provider:
|
||||||
|
try:
|
||||||
|
self.apic.fvRsProv.create(tenant_id, AP_NAME,
|
||||||
|
epg_id, contract_id)
|
||||||
|
self.db.set_provider_contract(epg_id)
|
||||||
|
self.make_tenant_contract_global(tenant_id)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
self.make_tenant_contract_local(tenant_id)
|
||||||
|
self.apic.fvRsProv.delete(tenant_id, AP_NAME,
|
||||||
|
epg_id, contract_id)
|
||||||
|
else:
|
||||||
|
self.apic.fvRsCons.create(tenant_id, AP_NAME, epg_id, contract_id)
|
||||||
|
|
||||||
|
def delete_contract_for_epg(self, tenant_id, epg_id,
|
||||||
|
contract_id, provider=False):
|
||||||
|
"""Delete the contract for an End Point Group.
|
||||||
|
|
||||||
|
Check if the EPG was a provider and attempt to grab another contract
|
||||||
|
consumer from the DB and set that as the new contract provider.
|
||||||
|
"""
|
||||||
|
if provider:
|
||||||
|
self.apic.fvRsProv.delete(tenant_id, AP_NAME, epg_id, contract_id)
|
||||||
|
self.db.unset_provider_contract(epg_id)
|
||||||
|
# Pick out another EPG to set as contract provider
|
||||||
|
epg = self.db.get_an_epg(epg_id)
|
||||||
|
self.update_contract_for_epg(tenant_id, epg.epg_id,
|
||||||
|
contract_id, True)
|
||||||
|
else:
|
||||||
|
self.apic.fvRsCons.delete(tenant_id, AP_NAME, epg_id, contract_id)
|
||||||
|
|
||||||
|
def update_contract_for_epg(self, tenant_id, epg_id,
|
||||||
|
contract_id, provider=False):
|
||||||
|
"""Updates the contract for an End Point Group."""
|
||||||
|
self.apic.fvRsCons.delete(tenant_id, AP_NAME, epg_id, contract_id)
|
||||||
|
self.set_contract_for_epg(tenant_id, epg_id, contract_id, provider)
|
||||||
|
|
||||||
|
def create_tenant_contract(self, tenant_id):
|
||||||
|
"""Creates a tenant contract.
|
||||||
|
|
||||||
|
Create a tenant contract if one doesn't exist. Also create a
|
||||||
|
subject, filter and entry and set the filters to allow all
|
||||||
|
protocol traffic on all ports
|
||||||
|
"""
|
||||||
|
contract = self.db.get_contract_for_tenant(tenant_id)
|
||||||
|
if not contract:
|
||||||
|
cuuid = uuid.uuid4()
|
||||||
|
try:
|
||||||
|
# Create contract
|
||||||
|
self.apic.vzBrCP.create(tenant_id, cuuid, scope=SCOPE_TENANT)
|
||||||
|
acontract = self.apic.vzBrCP.get(tenant_id, cuuid)
|
||||||
|
# Create subject
|
||||||
|
suuid = uuid.uuid4()
|
||||||
|
self.apic.vzSubj.create(tenant_id, cuuid, suuid)
|
||||||
|
# Create filter and entry
|
||||||
|
tfilter = self.create_tenant_filter(tenant_id)
|
||||||
|
# Create interm and outterm
|
||||||
|
self.apic.vzInTerm.create(tenant_id, cuuid, suuid)
|
||||||
|
self.apic.vzRsFiltAtt__In.create(tenant_id, cuuid,
|
||||||
|
suuid, tfilter)
|
||||||
|
self.apic.vzOutTerm.create(tenant_id, cuuid, suuid)
|
||||||
|
self.apic.vzRsFiltAtt__Out.create(tenant_id, cuuid,
|
||||||
|
suuid, tfilter)
|
||||||
|
# Create contract interface
|
||||||
|
iuuid = uuid.uuid4()
|
||||||
|
self.apic.vzCPIf.create(tenant_id, iuuid)
|
||||||
|
self.apic.vzRsIf.create(tenant_id, iuuid,
|
||||||
|
tDn=acontract[DN_KEY])
|
||||||
|
# Store contract in DB
|
||||||
|
contract = self.db.write_contract_for_tenant(tenant_id,
|
||||||
|
cuuid, tfilter)
|
||||||
|
except (cexc.ApicResponseNotOk, KeyError):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
# Delete tenant contract
|
||||||
|
self.apic.vzBrCP.delete(tenant_id, cuuid)
|
||||||
|
|
||||||
|
return contract
|
||||||
|
|
||||||
|
def make_tenant_contract_global(self, tenant_id):
|
||||||
|
"""Mark the tenant contract's scope to global."""
|
||||||
|
contract = self.db.get_contract_for_tenant(tenant_id)
|
||||||
|
self.apic.vzBrCP.update(tenant_id, contract.contract_id,
|
||||||
|
scope=SCOPE_GLOBAL)
|
||||||
|
|
||||||
|
def make_tenant_contract_local(self, tenant_id):
|
||||||
|
"""Mark the tenant contract's scope to tenant."""
|
||||||
|
contract = self.db.get_contract_for_tenant(tenant_id)
|
||||||
|
self.apic.vzBrCP.update(tenant_id, contract.contract_id,
|
||||||
|
scope=SCOPE_TENANT)
|
||||||
|
|
||||||
|
def ensure_path_created_for_port(self, tenant_id, network_id,
|
||||||
|
host_id, encap, net_name):
|
||||||
|
"""Create path attribute for an End Point Group."""
|
||||||
|
encap = 'vlan-' + str(encap)
|
||||||
|
epg = self.ensure_epg_created_for_network(tenant_id, network_id,
|
||||||
|
net_name)
|
||||||
|
eid = epg.epg_id
|
||||||
|
|
||||||
|
# Get attached switch and port for this host
|
||||||
|
host_config = config.get_switch_and_port_for_host(host_id)
|
||||||
|
if not host_config:
|
||||||
|
raise cexc.ApicHostNotConfigured(host=host_id)
|
||||||
|
switch, port = host_config
|
||||||
|
pdn = PORT_DN_PATH % (switch, port)
|
||||||
|
|
||||||
|
# Check if exists
|
||||||
|
patt = self.apic.fvRsPathAtt.get(tenant_id, AP_NAME, eid, pdn)
|
||||||
|
if not patt:
|
||||||
|
self.apic.fvRsPathAtt.create(tenant_id, AP_NAME, eid, pdn,
|
||||||
|
encap=encap, mode="regular",
|
||||||
|
instrImedcy="immediate")
|
@ -50,3 +50,10 @@ class ApicHostNotConfigured(exceptions.NotAuthorized):
|
|||||||
class ApicManagedObjectNotSupported(exceptions.NeutronException):
|
class ApicManagedObjectNotSupported(exceptions.NeutronException):
|
||||||
"""Attempted to use an unsupported Managed Object."""
|
"""Attempted to use an unsupported Managed Object."""
|
||||||
message = _("Managed Object '%(mo_class)s' is not supported")
|
message = _("Managed Object '%(mo_class)s' is not supported")
|
||||||
|
|
||||||
|
|
||||||
|
class ApicMultipleVlanRanges(exceptions.NeutronException):
|
||||||
|
"""Multiple VLAN ranges specified."""
|
||||||
|
message = _("Multiple VLAN ranges are not supported in the APIC plugin. "
|
||||||
|
"Please specify a single VLAN range. "
|
||||||
|
"Current config: '%(vlan_ranges)s'")
|
||||||
|
150
neutron/plugins/ml2/drivers/cisco/apic/mechanism_apic.py
Normal file
150
neutron/plugins/ml2/drivers/cisco/apic/mechanism_apic.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Copyright (c) 2014 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.
|
||||||
|
#
|
||||||
|
# @author: Arvind Somya (asomya@cisco.com), Cisco Systems Inc.
|
||||||
|
|
||||||
|
import netaddr
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from neutron.extensions import portbindings
|
||||||
|
from neutron.openstack.common import log
|
||||||
|
from neutron.plugins.common import constants
|
||||||
|
from neutron.plugins.ml2 import driver_api as api
|
||||||
|
from neutron.plugins.ml2.drivers.cisco.apic import apic_manager
|
||||||
|
from neutron.plugins.ml2.drivers.cisco.apic import exceptions as apic_exc
|
||||||
|
|
||||||
|
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class APICMechanismDriver(api.MechanismDriver):
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.apic_manager = apic_manager.APICManager()
|
||||||
|
|
||||||
|
# Create a Phys domain and VLAN namespace
|
||||||
|
# Get vlan ns name
|
||||||
|
ns_name = cfg.CONF.ml2_cisco_apic.apic_vlan_ns_name
|
||||||
|
|
||||||
|
# Grab vlan ranges
|
||||||
|
if len(cfg.CONF.ml2_type_vlan.network_vlan_ranges) != 1:
|
||||||
|
raise apic_exc.ApicMultipleVlanRanges(
|
||||||
|
cfg.CONF.ml2_type_vlan.network_vlan_ranges)
|
||||||
|
vlan_ranges = cfg.CONF.ml2_type_vlan.network_vlan_ranges[0]
|
||||||
|
if ',' in vlan_ranges:
|
||||||
|
raise apic_exc.ApicMultipleVlanRanges(vlan_ranges)
|
||||||
|
(vlan_min, vlan_max) = vlan_ranges.split(':')[-2:]
|
||||||
|
|
||||||
|
# Create VLAN namespace
|
||||||
|
vlan_ns = self.apic_manager.ensure_vlan_ns_created_on_apic(ns_name,
|
||||||
|
vlan_min,
|
||||||
|
vlan_max)
|
||||||
|
phys_name = cfg.CONF.ml2_cisco_apic.apic_vmm_domain
|
||||||
|
# Create Physical domain
|
||||||
|
self.apic_manager.ensure_phys_domain_created_on_apic(phys_name,
|
||||||
|
vlan_ns)
|
||||||
|
|
||||||
|
# Create entity profile
|
||||||
|
ent_name = cfg.CONF.ml2_cisco_apic.apic_entity_profile
|
||||||
|
self.apic_manager.ensure_entity_profile_created_on_apic(ent_name)
|
||||||
|
|
||||||
|
# Create function profile
|
||||||
|
func_name = cfg.CONF.ml2_cisco_apic.apic_function_profile
|
||||||
|
self.apic_manager.ensure_function_profile_created_on_apic(func_name)
|
||||||
|
|
||||||
|
# Create infrastructure on apic
|
||||||
|
self.apic_manager.ensure_infra_created_on_apic()
|
||||||
|
|
||||||
|
def _perform_port_operations(self, context):
|
||||||
|
# Get tenant details from port context
|
||||||
|
tenant_id = context.current['tenant_id']
|
||||||
|
|
||||||
|
# Get network
|
||||||
|
network = context.network.current['id']
|
||||||
|
net_name = context.network.current['name']
|
||||||
|
|
||||||
|
# Get port
|
||||||
|
port = context.current
|
||||||
|
|
||||||
|
# Get segmentation id
|
||||||
|
if not context.bound_segment:
|
||||||
|
LOG.debug(_("Port %s is not bound to a segment"), port)
|
||||||
|
return
|
||||||
|
seg = None
|
||||||
|
if (context.bound_segment.get(api.NETWORK_TYPE) in
|
||||||
|
[constants.TYPE_VLAN]):
|
||||||
|
seg = context.bound_segment.get(api.SEGMENTATION_ID)
|
||||||
|
|
||||||
|
# Check if a compute port
|
||||||
|
if not port['device_owner'].startswith('compute'):
|
||||||
|
# Not a compute port, return
|
||||||
|
return
|
||||||
|
|
||||||
|
host = port.get(portbindings.HOST_ID)
|
||||||
|
# Check host that the dhcp agent is running on
|
||||||
|
filters = {'device_owner': 'network:dhcp',
|
||||||
|
'network_id': network}
|
||||||
|
dhcp_ports = context._plugin.get_ports(context._plugin_context,
|
||||||
|
filters=filters)
|
||||||
|
dhcp_hosts = []
|
||||||
|
for dhcp_port in dhcp_ports:
|
||||||
|
dhcp_hosts.append(dhcp_port.get(portbindings.HOST_ID))
|
||||||
|
|
||||||
|
# Create a static path attachment for this host/epg/switchport combo
|
||||||
|
self.apic_manager.ensure_tenant_created_on_apic(tenant_id)
|
||||||
|
if dhcp_hosts:
|
||||||
|
for dhcp_host in dhcp_hosts:
|
||||||
|
self.apic_manager.ensure_path_created_for_port(tenant_id,
|
||||||
|
network,
|
||||||
|
dhcp_host, seg,
|
||||||
|
net_name)
|
||||||
|
if host not in dhcp_hosts:
|
||||||
|
self.apic_manager.ensure_path_created_for_port(tenant_id, network,
|
||||||
|
host, seg, net_name)
|
||||||
|
|
||||||
|
def create_port_postcommit(self, context):
|
||||||
|
self._perform_port_operations(context)
|
||||||
|
|
||||||
|
def update_port_postcommit(self, context):
|
||||||
|
self._perform_port_operations(context)
|
||||||
|
|
||||||
|
def create_network_postcommit(self, context):
|
||||||
|
net_id = context.current['id']
|
||||||
|
tenant_id = context.current['tenant_id']
|
||||||
|
net_name = context.current['name']
|
||||||
|
|
||||||
|
self.apic_manager.ensure_bd_created_on_apic(tenant_id, net_id)
|
||||||
|
# Create EPG for this network
|
||||||
|
self.apic_manager.ensure_epg_created_for_network(tenant_id, net_id,
|
||||||
|
net_name)
|
||||||
|
|
||||||
|
def delete_network_postcommit(self, context):
|
||||||
|
net_id = context.current['id']
|
||||||
|
tenant_id = context.current['tenant_id']
|
||||||
|
|
||||||
|
self.apic_manager.delete_bd_on_apic(tenant_id, net_id)
|
||||||
|
self.apic_manager.delete_epg_for_network(tenant_id, net_id)
|
||||||
|
|
||||||
|
def create_subnet_postcommit(self, context):
|
||||||
|
tenant_id = context.current['tenant_id']
|
||||||
|
network_id = context.current['network_id']
|
||||||
|
gateway_ip = context.current['gateway_ip']
|
||||||
|
cidr = netaddr.IPNetwork(context.current['cidr'])
|
||||||
|
netmask = str(cidr.prefixlen)
|
||||||
|
gateway_ip = gateway_ip + '/' + netmask
|
||||||
|
|
||||||
|
self.apic_manager.ensure_subnet_created_on_apic(tenant_id, network_id,
|
||||||
|
gateway_ip)
|
@ -23,7 +23,7 @@ from oslo.config import cfg
|
|||||||
from neutron.common import config as neutron_config
|
from neutron.common import config as neutron_config
|
||||||
from neutron.plugins.ml2 import config as ml2_config
|
from neutron.plugins.ml2 import config as ml2_config
|
||||||
from neutron.plugins.ml2.drivers.cisco.apic import apic_client as apic
|
from neutron.plugins.ml2.drivers.cisco.apic import apic_client as apic
|
||||||
from neutron.tests.unit import test_api_v2
|
from neutron.tests import base
|
||||||
|
|
||||||
|
|
||||||
OK = requests.codes.ok
|
OK = requests.codes.ok
|
||||||
@ -169,7 +169,7 @@ class ConfigMixin(object):
|
|||||||
|
|
||||||
def set_up_mocks(self):
|
def set_up_mocks(self):
|
||||||
# Mock the configuration file
|
# Mock the configuration file
|
||||||
args = ['--config-file', test_api_v2.etcdir('neutron.conf.test')]
|
args = ['--config-file', base.etcdir('neutron.conf.test')]
|
||||||
neutron_config.parse(args=args)
|
neutron_config.parse(args=args)
|
||||||
|
|
||||||
# Configure the ML2 mechanism drivers and network types
|
# Configure the ML2 mechanism drivers and network types
|
||||||
|
@ -0,0 +1,698 @@
|
|||||||
|
# Copyright (c) 2014 Cisco Systems
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# @author: Henry Gessau, Cisco Systems
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from webob import exc as wexc
|
||||||
|
|
||||||
|
from neutron.openstack.common import uuidutils
|
||||||
|
|
||||||
|
from neutron.plugins.ml2.drivers.cisco.apic import apic_manager
|
||||||
|
from neutron.plugins.ml2.drivers.cisco.apic import exceptions as cexc
|
||||||
|
from neutron.tests import base
|
||||||
|
from neutron.tests.unit.ml2.drivers.cisco.apic import (
|
||||||
|
test_cisco_apic_common as mocked)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCiscoApicManager(base.BaseTestCase,
|
||||||
|
mocked.ControllerMixin,
|
||||||
|
mocked.ConfigMixin,
|
||||||
|
mocked.DbModelMixin):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCiscoApicManager, self).setUp()
|
||||||
|
mocked.ControllerMixin.set_up_mocks(self)
|
||||||
|
mocked.ConfigMixin.set_up_mocks(self)
|
||||||
|
mocked.DbModelMixin.set_up_mocks(self)
|
||||||
|
|
||||||
|
self.mock_apic_manager_login_responses()
|
||||||
|
self.mgr = apic_manager.APICManager()
|
||||||
|
self.session = self.mgr.apic.session
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.reset_reponses()
|
||||||
|
|
||||||
|
def test_mgr_session_login(self):
|
||||||
|
login = self.mgr.apic.authentication
|
||||||
|
self.assertEqual(login['userName'], mocked.APIC_USR)
|
||||||
|
|
||||||
|
def test_mgr_session_logout(self):
|
||||||
|
self.mock_response_for_post('aaaLogout')
|
||||||
|
self.mgr.apic.logout()
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertIsNone(self.mgr.apic.authentication)
|
||||||
|
|
||||||
|
def test_to_range(self):
|
||||||
|
port_list = [4, 2, 3, 1, 7, 8, 10, 20, 6, 22, 21]
|
||||||
|
expected_ranges = [(1, 4), (6, 8), (10, 10), (20, 22)]
|
||||||
|
port_ranges = [r for r in apic_manager.group_by_ranges(port_list)]
|
||||||
|
self.assertEqual(port_ranges, expected_ranges)
|
||||||
|
|
||||||
|
def test_get_profiles(self):
|
||||||
|
self.mock_db_query_filterby_first_return('faked')
|
||||||
|
self.assertEqual(
|
||||||
|
self.mgr.db.get_port_profile_for_node('node'),
|
||||||
|
'faked'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.mgr.db.get_profile_for_module('node', 'prof', 'module'),
|
||||||
|
'faked'
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.mgr.db.get_profile_for_module_and_ports(
|
||||||
|
'node', 'prof', 'module', 'from', 'to'
|
||||||
|
),
|
||||||
|
'faked'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_add_profile(self):
|
||||||
|
self.mgr.db.add_profile_for_module_and_ports(
|
||||||
|
'node', 'prof', 'hpselc', 'module', 'from', 'to')
|
||||||
|
self.assertTrue(self.mocked_session.add.called)
|
||||||
|
self.assertTrue(self.mocked_session.flush.called)
|
||||||
|
|
||||||
|
def test_ensure_port_profile_created(self):
|
||||||
|
port_name = mocked.APIC_PORT
|
||||||
|
self.mock_responses_for_create('infraAccPortP')
|
||||||
|
self.mock_response_for_get('infraAccPortP', name=port_name)
|
||||||
|
port = self.mgr.ensure_port_profile_created_on_apic(port_name)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertEqual(port['name'], port_name)
|
||||||
|
|
||||||
|
def test_ensure_port_profile_created_exc(self):
|
||||||
|
port_name = mocked.APIC_PORT
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('infraAccPortP')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_port_profile_created_on_apic,
|
||||||
|
port_name)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_node_profile_created_for_switch_old(self):
|
||||||
|
old_switch = mocked.APIC_NODE_PROF
|
||||||
|
self.mock_response_for_get('infraNodeP', name=old_switch)
|
||||||
|
self.mgr.ensure_node_profile_created_for_switch(old_switch)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
old_name = self.mgr.node_profiles[old_switch]['object']['name']
|
||||||
|
self.assertEqual(old_name, old_switch)
|
||||||
|
|
||||||
|
def test_ensure_node_profile_created_for_switch_new(self):
|
||||||
|
new_switch = mocked.APIC_NODE_PROF
|
||||||
|
self.mock_response_for_get('infraNodeP')
|
||||||
|
self.mock_responses_for_create('infraNodeP')
|
||||||
|
self.mock_responses_for_create('infraLeafS')
|
||||||
|
self.mock_responses_for_create('infraNodeBlk')
|
||||||
|
self.mock_response_for_get('infraNodeP', name=new_switch)
|
||||||
|
self.mgr.ensure_node_profile_created_for_switch(new_switch)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
new_name = self.mgr.node_profiles[new_switch]['object']['name']
|
||||||
|
self.assertEqual(new_name, new_switch)
|
||||||
|
|
||||||
|
def test_ensure_node_profile_created_for_switch_new_exc(self):
|
||||||
|
new_switch = mocked.APIC_NODE_PROF
|
||||||
|
self.mock_response_for_get('infraNodeP')
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('infraNodeP')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_node_profile_created_for_switch,
|
||||||
|
new_switch)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_vmm_domain_created_old(self):
|
||||||
|
dom = mocked.APIC_DOMAIN
|
||||||
|
self.mock_response_for_get('vmmDomP', name=dom)
|
||||||
|
self.mgr.ensure_vmm_domain_created_on_apic(dom)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
old_dom = self.mgr.vmm_domain['name']
|
||||||
|
self.assertEqual(old_dom, dom)
|
||||||
|
|
||||||
|
def _mock_new_vmm_dom_responses(self, dom, seg_type=None):
|
||||||
|
vmm = mocked.APIC_VMMP
|
||||||
|
dn = self.mgr.apic.vmmDomP.mo.dn(vmm, dom)
|
||||||
|
self.mock_response_for_get('vmmDomP')
|
||||||
|
self.mock_responses_for_create('vmmDomP')
|
||||||
|
if seg_type:
|
||||||
|
self.mock_responses_for_create(seg_type)
|
||||||
|
self.mock_response_for_get('vmmDomP', name=dom, dn=dn)
|
||||||
|
|
||||||
|
def test_ensure_vmm_domain_created_new_no_vlan_ns(self):
|
||||||
|
dom = mocked.APIC_DOMAIN
|
||||||
|
self._mock_new_vmm_dom_responses(dom)
|
||||||
|
self.mgr.ensure_vmm_domain_created_on_apic(dom)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
new_dom = self.mgr.vmm_domain['name']
|
||||||
|
self.assertEqual(new_dom, dom)
|
||||||
|
|
||||||
|
def test_ensure_vmm_domain_created_new_no_vlan_ns_exc(self):
|
||||||
|
dom = mocked.APIC_DOMAIN
|
||||||
|
self.mock_response_for_get('vmmDomP')
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('vmmDomP')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_vmm_domain_created_on_apic, dom)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_vmm_domain_created_new_with_vlan_ns(self):
|
||||||
|
dom = mocked.APIC_DOMAIN
|
||||||
|
self._mock_new_vmm_dom_responses(dom, seg_type='infraRsVlanNs__vmm')
|
||||||
|
ns = {'dn': 'test_vlan_ns'}
|
||||||
|
self.mgr.ensure_vmm_domain_created_on_apic(dom, vlan_ns=ns)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
new_dom = self.mgr.vmm_domain['name']
|
||||||
|
self.assertEqual(new_dom, dom)
|
||||||
|
|
||||||
|
def test_ensure_vmm_domain_created_new_with_vxlan_ns(self):
|
||||||
|
dom = mocked.APIC_DOMAIN
|
||||||
|
# TODO(Henry): mock seg_type vxlan when vxlan is ready
|
||||||
|
self._mock_new_vmm_dom_responses(dom, seg_type=None)
|
||||||
|
ns = {'dn': 'test_vxlan_ns'}
|
||||||
|
self.mgr.ensure_vmm_domain_created_on_apic(dom, vxlan_ns=ns)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
new_dom = self.mgr.vmm_domain['name']
|
||||||
|
self.assertEqual(new_dom, dom)
|
||||||
|
|
||||||
|
def test_ensure_infra_created_no_infra(self):
|
||||||
|
self.mgr.switch_dict = {}
|
||||||
|
self.mgr.ensure_infra_created_on_apic()
|
||||||
|
|
||||||
|
def _ensure_infra_created_seq1_setup(self):
|
||||||
|
am = 'neutron.plugins.ml2.drivers.cisco.apic.apic_manager.APICManager'
|
||||||
|
np_create_for_switch = mock.patch(
|
||||||
|
am + '.ensure_node_profile_created_for_switch').start()
|
||||||
|
self.mock_db_query_filterby_first_return(None)
|
||||||
|
pp_create_for_switch = mock.patch(
|
||||||
|
am + '.ensure_port_profile_created_on_apic').start()
|
||||||
|
pp_create_for_switch.return_value = {'dn': 'port_profile_dn'}
|
||||||
|
return np_create_for_switch, pp_create_for_switch
|
||||||
|
|
||||||
|
def test_ensure_infra_created_seq1(self):
|
||||||
|
np_create_for_switch, pp_create_for_switch = (
|
||||||
|
self._ensure_infra_created_seq1_setup())
|
||||||
|
|
||||||
|
def _profile_for_module(aswitch, ppn, module):
|
||||||
|
profile = mock.Mock()
|
||||||
|
profile.ppn = ppn
|
||||||
|
profile.hpselc_id = '-'.join([aswitch, module, 'hpselc_id'])
|
||||||
|
return profile
|
||||||
|
|
||||||
|
self.mgr.db.get_profile_for_module = mock.Mock(
|
||||||
|
side_effect=_profile_for_module)
|
||||||
|
self.mgr.db.get_profile_for_module_and_ports = mock.Mock(
|
||||||
|
return_value=None)
|
||||||
|
self.mgr.db.add_profile_for_module_and_ports = mock.Mock()
|
||||||
|
|
||||||
|
num_switches = len(self.mgr.switch_dict)
|
||||||
|
for loop in range(num_switches):
|
||||||
|
self.mock_responses_for_create('infraRsAccPortP')
|
||||||
|
self.mock_responses_for_create('infraPortBlk')
|
||||||
|
|
||||||
|
self.mgr.ensure_infra_created_on_apic()
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertEqual(np_create_for_switch.call_count, num_switches)
|
||||||
|
self.assertEqual(pp_create_for_switch.call_count, num_switches)
|
||||||
|
for switch in self.mgr.switch_dict:
|
||||||
|
np_create_for_switch.assert_any_call(switch)
|
||||||
|
|
||||||
|
def test_ensure_infra_created_seq1_exc(self):
|
||||||
|
np_create_for_switch, __ = self._ensure_infra_created_seq1_setup()
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('infraAccPortP')
|
||||||
|
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_infra_created_on_apic)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertTrue(np_create_for_switch.called)
|
||||||
|
self.assertEqual(np_create_for_switch.call_count, 1)
|
||||||
|
|
||||||
|
def _ensure_infra_created_seq2_setup(self):
|
||||||
|
am = 'neutron.plugins.ml2.drivers.cisco.apic.apic_manager.APICManager'
|
||||||
|
np_create_for_switch = mock.patch(
|
||||||
|
am + '.ensure_node_profile_created_for_switch').start()
|
||||||
|
|
||||||
|
def _profile_for_node(aswitch):
|
||||||
|
profile = mock.Mock()
|
||||||
|
profile.profile_id = '-'.join([aswitch, 'profile_id'])
|
||||||
|
return profile
|
||||||
|
|
||||||
|
self.mgr.db.get_port_profile_for_node = mock.Mock(
|
||||||
|
side_effect=_profile_for_node)
|
||||||
|
self.mgr.db.get_profile_for_module = mock.Mock(
|
||||||
|
return_value=None)
|
||||||
|
self.mgr.function_profile = {'dn': 'dn'}
|
||||||
|
self.mgr.db.get_profile_for_module_and_ports = mock.Mock(
|
||||||
|
return_value=True)
|
||||||
|
|
||||||
|
return np_create_for_switch
|
||||||
|
|
||||||
|
def test_ensure_infra_created_seq2(self):
|
||||||
|
np_create_for_switch = self._ensure_infra_created_seq2_setup()
|
||||||
|
|
||||||
|
num_switches = len(self.mgr.switch_dict)
|
||||||
|
for loop in range(num_switches):
|
||||||
|
self.mock_responses_for_create('infraHPortS')
|
||||||
|
self.mock_responses_for_create('infraRsAccBaseGrp')
|
||||||
|
|
||||||
|
self.mgr.ensure_infra_created_on_apic()
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertEqual(np_create_for_switch.call_count, num_switches)
|
||||||
|
for switch in self.mgr.switch_dict:
|
||||||
|
np_create_for_switch.assert_any_call(switch)
|
||||||
|
|
||||||
|
def test_ensure_infra_created_seq2_exc(self):
|
||||||
|
np_create_for_switch = self._ensure_infra_created_seq2_setup()
|
||||||
|
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('infraHPortS')
|
||||||
|
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_infra_created_on_apic)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertTrue(np_create_for_switch.called)
|
||||||
|
self.assertEqual(np_create_for_switch.call_count, 1)
|
||||||
|
|
||||||
|
def test_ensure_context_unenforced_new_ctx(self):
|
||||||
|
self.mock_response_for_get('fvCtx')
|
||||||
|
self.mock_responses_for_create('fvCtx')
|
||||||
|
self.mgr.ensure_context_unenforced()
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_context_unenforced_pref1(self):
|
||||||
|
self.mock_response_for_get('fvCtx', pcEnfPref='1')
|
||||||
|
self.mock_response_for_post('fvCtx')
|
||||||
|
self.mgr.ensure_context_unenforced()
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_context_unenforced_pref2(self):
|
||||||
|
self.mock_response_for_get('fvCtx', pcEnfPref='2')
|
||||||
|
self.mgr.ensure_context_unenforced()
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def _mock_vmm_dom_prereq(self, dom):
|
||||||
|
self._mock_new_vmm_dom_responses(dom)
|
||||||
|
self.mgr.ensure_vmm_domain_created_on_apic(dom)
|
||||||
|
|
||||||
|
def _mock_new_phys_dom_responses(self, dom, seg_type=None):
|
||||||
|
dn = self.mgr.apic.physDomP.mo.dn(dom)
|
||||||
|
self.mock_response_for_get('physDomP')
|
||||||
|
self.mock_responses_for_create('physDomP')
|
||||||
|
if seg_type:
|
||||||
|
self.mock_responses_for_create(seg_type)
|
||||||
|
self.mock_response_for_get('physDomP', name=dom, dn=dn)
|
||||||
|
|
||||||
|
def _mock_phys_dom_prereq(self, dom):
|
||||||
|
self._mock_new_phys_dom_responses(dom)
|
||||||
|
self.mgr.ensure_phys_domain_created_on_apic(dom)
|
||||||
|
|
||||||
|
def test_ensure_entity_profile_created_old(self):
|
||||||
|
ep = mocked.APIC_ATT_ENT_PROF
|
||||||
|
self.mock_response_for_get('infraAttEntityP', name=ep)
|
||||||
|
self.mgr.ensure_entity_profile_created_on_apic(ep)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def _mock_new_entity_profile(self, exc=None):
|
||||||
|
self.mock_response_for_get('infraAttEntityP')
|
||||||
|
self.mock_responses_for_create('infraAttEntityP')
|
||||||
|
self.mock_responses_for_create('infraRsDomP')
|
||||||
|
if exc:
|
||||||
|
self.mock_error_get_response(exc, code='103', text=u'Fail')
|
||||||
|
else:
|
||||||
|
self.mock_response_for_get('infraAttEntityP')
|
||||||
|
|
||||||
|
def test_ensure_entity_profile_created_new(self):
|
||||||
|
self._mock_phys_dom_prereq(mocked.APIC_PDOM)
|
||||||
|
ep = mocked.APIC_ATT_ENT_PROF
|
||||||
|
self._mock_new_entity_profile()
|
||||||
|
self.mgr.ensure_entity_profile_created_on_apic(ep)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_entity_profile_created_new_exc(self):
|
||||||
|
self._mock_phys_dom_prereq(mocked.APIC_PDOM)
|
||||||
|
ep = mocked.APIC_ATT_ENT_PROF
|
||||||
|
self._mock_new_entity_profile(exc=wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('infraAttEntityP')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_entity_profile_created_on_apic, ep)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def _mock_entity_profile_preqreq(self):
|
||||||
|
self._mock_phys_dom_prereq(mocked.APIC_PDOM)
|
||||||
|
ep = mocked.APIC_ATT_ENT_PROF
|
||||||
|
self._mock_new_entity_profile()
|
||||||
|
self.mgr.ensure_entity_profile_created_on_apic(ep)
|
||||||
|
|
||||||
|
def test_ensure_function_profile_created_old(self):
|
||||||
|
self._mock_entity_profile_preqreq()
|
||||||
|
fp = mocked.APIC_FUNC_PROF
|
||||||
|
self.mock_response_for_get('infraAccPortGrp', name=fp)
|
||||||
|
self.mgr.ensure_function_profile_created_on_apic(fp)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
old_fp = self.mgr.function_profile['name']
|
||||||
|
self.assertEqual(old_fp, fp)
|
||||||
|
|
||||||
|
def _mock_new_function_profile(self, fp):
|
||||||
|
dn = self.mgr.apic.infraAttEntityP.mo.dn(fp)
|
||||||
|
self.mock_responses_for_create('infraAccPortGrp')
|
||||||
|
self.mock_responses_for_create('infraRsAttEntP')
|
||||||
|
self.mock_response_for_get('infraAccPortGrp', name=fp, dn=dn)
|
||||||
|
|
||||||
|
def test_ensure_function_profile_created_new(self):
|
||||||
|
fp = mocked.APIC_FUNC_PROF
|
||||||
|
dn = self.mgr.apic.infraAttEntityP.mo.dn(fp)
|
||||||
|
self.mgr.entity_profile = {'dn': dn}
|
||||||
|
self.mock_response_for_get('infraAccPortGrp')
|
||||||
|
self.mock_responses_for_create('infraAccPortGrp')
|
||||||
|
self.mock_responses_for_create('infraRsAttEntP')
|
||||||
|
self.mock_response_for_get('infraAccPortGrp', name=fp, dn=dn)
|
||||||
|
self.mgr.ensure_function_profile_created_on_apic(fp)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
new_fp = self.mgr.function_profile['name']
|
||||||
|
self.assertEqual(new_fp, fp)
|
||||||
|
|
||||||
|
def test_ensure_function_profile_created_new_exc(self):
|
||||||
|
fp = mocked.APIC_FUNC_PROF
|
||||||
|
dn = self.mgr.apic.infraAttEntityP.mo.dn(fp)
|
||||||
|
self.mgr.entity_profile = {'dn': dn}
|
||||||
|
self.mock_response_for_get('infraAccPortGrp')
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('infraAccPortGrp')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_function_profile_created_on_apic, fp)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_vlan_ns_created_old(self):
|
||||||
|
ns = mocked.APIC_VLAN_NAME
|
||||||
|
mode = mocked.APIC_VLAN_MODE
|
||||||
|
self.mock_response_for_get('fvnsVlanInstP', name=ns, mode=mode)
|
||||||
|
new_ns = self.mgr.ensure_vlan_ns_created_on_apic(ns, '100', '199')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertIsNone(new_ns)
|
||||||
|
|
||||||
|
def _mock_new_vlan_instance(self, ns, vlan_encap=None):
|
||||||
|
self.mock_responses_for_create('fvnsVlanInstP')
|
||||||
|
if vlan_encap:
|
||||||
|
self.mock_response_for_get('fvnsEncapBlk', **vlan_encap)
|
||||||
|
else:
|
||||||
|
self.mock_response_for_get('fvnsEncapBlk')
|
||||||
|
self.mock_responses_for_create('fvnsEncapBlk__vlan')
|
||||||
|
self.mock_response_for_get('fvnsVlanInstP', name=ns)
|
||||||
|
|
||||||
|
def test_ensure_vlan_ns_created_new_no_encap(self):
|
||||||
|
ns = mocked.APIC_VLAN_NAME
|
||||||
|
self.mock_response_for_get('fvnsVlanInstP')
|
||||||
|
self._mock_new_vlan_instance(ns)
|
||||||
|
new_ns = self.mgr.ensure_vlan_ns_created_on_apic(ns, '200', '299')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertEqual(new_ns['name'], ns)
|
||||||
|
|
||||||
|
def test_ensure_vlan_ns_created_new_exc(self):
|
||||||
|
ns = mocked.APIC_VLAN_NAME
|
||||||
|
self.mock_response_for_get('fvnsVlanInstP')
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('fvnsVlanInstP')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_vlan_ns_created_on_apic,
|
||||||
|
ns, '200', '299')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_vlan_ns_created_new_with_encap(self):
|
||||||
|
ns = mocked.APIC_VLAN_NAME
|
||||||
|
self.mock_response_for_get('fvnsVlanInstP')
|
||||||
|
ns_args = {'name': 'encap', 'from': '300', 'to': '399'}
|
||||||
|
self._mock_new_vlan_instance(ns, vlan_encap=ns_args)
|
||||||
|
new_ns = self.mgr.ensure_vlan_ns_created_on_apic(ns, '300', '399')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertEqual(new_ns['name'], ns)
|
||||||
|
|
||||||
|
def test_ensure_tenant_created_on_apic(self):
|
||||||
|
self.mock_response_for_get('fvTenant', name='any')
|
||||||
|
self.mgr.ensure_tenant_created_on_apic('two')
|
||||||
|
self.mock_response_for_get('fvTenant')
|
||||||
|
self.mock_responses_for_create('fvTenant')
|
||||||
|
self.mgr.ensure_tenant_created_on_apic('four')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_bd_created_existing_bd(self):
|
||||||
|
self.mock_response_for_get('fvBD', name='BD')
|
||||||
|
self.mgr.ensure_bd_created_on_apic('t1', 'two')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_bd_created_not_ctx(self):
|
||||||
|
self.mock_response_for_get('fvBD')
|
||||||
|
self.mock_responses_for_create('fvBD')
|
||||||
|
self.mock_response_for_get('fvCtx')
|
||||||
|
self.mock_responses_for_create('fvCtx')
|
||||||
|
self.mock_responses_for_create('fvRsCtx')
|
||||||
|
self.mgr.ensure_bd_created_on_apic('t2', 'three')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_bd_created_exc(self):
|
||||||
|
self.mock_response_for_get('fvBD')
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('fvBD')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_bd_created_on_apic, 't2', 'three')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_bd_created_ctx_pref1(self):
|
||||||
|
self.mock_response_for_get('fvBD')
|
||||||
|
self.mock_responses_for_create('fvBD')
|
||||||
|
self.mock_response_for_get('fvCtx', pcEnfPref='1')
|
||||||
|
self.mock_responses_for_create('fvRsCtx')
|
||||||
|
self.mgr.ensure_bd_created_on_apic('t3', 'four')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_bd_created_ctx_pref2(self):
|
||||||
|
self.mock_response_for_get('fvBD')
|
||||||
|
self.mock_responses_for_create('fvBD')
|
||||||
|
self.mock_response_for_get('fvCtx', pcEnfPref='2')
|
||||||
|
self.mock_response_for_post('fvCtx')
|
||||||
|
self.mock_responses_for_create('fvRsCtx')
|
||||||
|
self.mgr.ensure_bd_created_on_apic('t3', 'four')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_delete_bd(self):
|
||||||
|
self.mock_response_for_post('fvBD')
|
||||||
|
self.mgr.delete_bd_on_apic('t1', 'bd')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_subnet_created(self):
|
||||||
|
self.mock_response_for_get('fvSubnet', name='sn1')
|
||||||
|
self.mgr.ensure_subnet_created_on_apic('t0', 'bd1', '2.2.2.2/8')
|
||||||
|
self.mock_response_for_get('fvSubnet')
|
||||||
|
self.mock_responses_for_create('fvSubnet')
|
||||||
|
self.mgr.ensure_subnet_created_on_apic('t2', 'bd3', '4.4.4.4/16')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_filter_created(self):
|
||||||
|
self.mock_response_for_get('vzFilter', name='f1')
|
||||||
|
self.mgr.ensure_filter_created_on_apic('t1', 'two')
|
||||||
|
self.mock_response_for_get('vzFilter')
|
||||||
|
self.mock_responses_for_create('vzFilter')
|
||||||
|
self.mgr.ensure_filter_created_on_apic('t2', 'four')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_epg_created_for_network_old(self):
|
||||||
|
self.mock_db_query_filterby_first_return('faked')
|
||||||
|
epg = self.mgr.ensure_epg_created_for_network('X', 'Y', 'Z')
|
||||||
|
self.assertEqual(epg, 'faked')
|
||||||
|
|
||||||
|
def test_ensure_epg_created_for_network_new(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
network = mocked.APIC_NETWORK
|
||||||
|
netname = mocked.APIC_NETNAME
|
||||||
|
self._mock_phys_dom_prereq(mocked.APIC_PDOM)
|
||||||
|
self.mock_db_query_filterby_first_return(None)
|
||||||
|
self.mock_responses_for_create('fvAEPg')
|
||||||
|
self.mock_response_for_get('fvBD', name=network)
|
||||||
|
self.mock_responses_for_create('fvRsBd')
|
||||||
|
self.mock_responses_for_create('fvRsDomAtt')
|
||||||
|
new_epg = self.mgr.ensure_epg_created_for_network(tenant,
|
||||||
|
network, netname)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertEqual(new_epg.network_id, network)
|
||||||
|
self.assertTrue(self.mocked_session.add.called)
|
||||||
|
self.assertTrue(self.mocked_session.flush.called)
|
||||||
|
|
||||||
|
def test_ensure_epg_created_for_network_exc(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
network = mocked.APIC_NETWORK
|
||||||
|
netname = mocked.APIC_NETNAME
|
||||||
|
self.mock_db_query_filterby_first_return(None)
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('fvAEPg')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.ensure_epg_created_for_network,
|
||||||
|
tenant, network, netname)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_delete_epg_for_network_no_epg(self):
|
||||||
|
self.mock_db_query_filterby_first_return(None)
|
||||||
|
self.mgr.delete_epg_for_network('tenant', 'network')
|
||||||
|
|
||||||
|
def test_delete_epg_for_network(self):
|
||||||
|
epg = mock.Mock()
|
||||||
|
epg.epg_id = mocked.APIC_EPG
|
||||||
|
self.mock_db_query_filterby_first_return(epg)
|
||||||
|
self.mock_response_for_post('fvAEPg')
|
||||||
|
self.mgr.delete_epg_for_network('tenant', 'network')
|
||||||
|
self.assertTrue(self.mocked_session.delete.called)
|
||||||
|
self.assertTrue(self.mocked_session.flush.called)
|
||||||
|
|
||||||
|
def test_ensure_path_created_for_port(self):
|
||||||
|
epg = mock.Mock()
|
||||||
|
epg.epg_id = 'epg01'
|
||||||
|
eepg = mock.Mock(return_value=epg)
|
||||||
|
apic_manager.APICManager.ensure_epg_created_for_network = eepg
|
||||||
|
self.mock_response_for_get('fvRsPathAtt', tDn='foo')
|
||||||
|
self.mgr.ensure_path_created_for_port('tenant', 'network', 'rhel01',
|
||||||
|
'static', 'netname')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_path_created_for_port_no_path_att(self):
|
||||||
|
epg = mock.Mock()
|
||||||
|
epg.epg_id = 'epg2'
|
||||||
|
eepg = mock.Mock(return_value=epg)
|
||||||
|
self.mgr.ensure_epg_created_for_network = eepg
|
||||||
|
self.mock_response_for_get('fvRsPathAtt')
|
||||||
|
self.mock_responses_for_create('fvRsPathAtt')
|
||||||
|
self.mgr.ensure_path_created_for_port('tenant', 'network', 'ubuntu2',
|
||||||
|
'static', 'netname')
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_ensure_path_created_for_port_unknown_host(self):
|
||||||
|
epg = mock.Mock()
|
||||||
|
epg.epg_id = 'epg3'
|
||||||
|
eepg = mock.Mock(return_value=epg)
|
||||||
|
apic_manager.APICManager.ensure_epg_created_for_network = eepg
|
||||||
|
self.mock_response_for_get('fvRsPathAtt', tDn='foo')
|
||||||
|
self.assertRaises(cexc.ApicHostNotConfigured,
|
||||||
|
self.mgr.ensure_path_created_for_port,
|
||||||
|
'tenant', 'network', 'cirros3', 'static', 'netname')
|
||||||
|
|
||||||
|
def test_create_tenant_filter(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
self.mock_responses_for_create('vzFilter')
|
||||||
|
self.mock_responses_for_create('vzEntry')
|
||||||
|
filter_id = self.mgr.create_tenant_filter(tenant)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertTrue(uuidutils.is_uuid_like(str(filter_id)))
|
||||||
|
|
||||||
|
def test_create_tenant_filter_exc(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('vzFilter')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.create_tenant_filter, tenant)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_set_contract_for_epg_consumer(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
epg = mocked.APIC_EPG
|
||||||
|
contract = mocked.APIC_CONTRACT
|
||||||
|
self.mock_responses_for_create('fvRsCons')
|
||||||
|
self.mgr.set_contract_for_epg(tenant, epg, contract)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_set_contract_for_epg_provider(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
epg = mocked.APIC_EPG
|
||||||
|
contract = mocked.APIC_CONTRACT
|
||||||
|
epg_obj = mock.Mock()
|
||||||
|
epg_obj.epg_id = epg
|
||||||
|
epg_obj.provider = False
|
||||||
|
self.mock_db_query_filterby_first_return(epg_obj)
|
||||||
|
self.mock_responses_for_create('fvRsProv')
|
||||||
|
self.mock_response_for_post('vzBrCP')
|
||||||
|
self.mgr.set_contract_for_epg(tenant, epg, contract, provider=True)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertTrue(self.mocked_session.merge.called)
|
||||||
|
self.assertTrue(self.mocked_session.flush.called)
|
||||||
|
self.assertTrue(epg_obj.provider)
|
||||||
|
|
||||||
|
def test_set_contract_for_epg_provider_exc(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
epg = mocked.APIC_EPG
|
||||||
|
contract = mocked.APIC_CONTRACT
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('vzBrCP')
|
||||||
|
self.mock_response_for_post('fvRsProv')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.set_contract_for_epg,
|
||||||
|
tenant, epg, contract, provider=True)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_delete_contract_for_epg_consumer(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
epg = mocked.APIC_EPG
|
||||||
|
contract = mocked.APIC_CONTRACT
|
||||||
|
self.mock_response_for_post('fvRsCons')
|
||||||
|
self.mgr.delete_contract_for_epg(tenant, epg, contract)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_delete_contract_for_epg_provider(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
epg = mocked.APIC_EPG
|
||||||
|
contract = mocked.APIC_CONTRACT
|
||||||
|
epg_obj = mock.Mock()
|
||||||
|
epg_obj.epg_id = epg + '-other'
|
||||||
|
epg_obj.provider = False
|
||||||
|
self.mock_db_query_filterby_first_return(epg_obj)
|
||||||
|
self.mock_response_for_post('fvRsProv')
|
||||||
|
self.mock_response_for_post('fvRsCons')
|
||||||
|
self.mock_responses_for_create('fvRsProv')
|
||||||
|
self.mock_response_for_post('vzBrCP')
|
||||||
|
self.mgr.delete_contract_for_epg(tenant, epg, contract, provider=True)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertTrue(self.mocked_session.merge.called)
|
||||||
|
self.assertTrue(self.mocked_session.flush.called)
|
||||||
|
self.assertTrue(epg_obj.provider)
|
||||||
|
|
||||||
|
def test_create_tenant_contract_existing(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
contract = mocked.APIC_CONTRACT
|
||||||
|
self.mock_db_query_filterby_first_return(contract)
|
||||||
|
new_contract = self.mgr.create_tenant_contract(tenant)
|
||||||
|
self.assertEqual(new_contract, contract)
|
||||||
|
|
||||||
|
def test_create_tenant_contract_new(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
contract = mocked.APIC_CONTRACT
|
||||||
|
dn = self.mgr.apic.vzBrCP.mo.dn(tenant, contract)
|
||||||
|
self.mock_db_query_filterby_first_return(None)
|
||||||
|
self.mock_responses_for_create('vzBrCP')
|
||||||
|
self.mock_response_for_get('vzBrCP', dn=dn)
|
||||||
|
self.mock_responses_for_create('vzSubj')
|
||||||
|
self.mock_responses_for_create('vzFilter')
|
||||||
|
self.mock_responses_for_create('vzEntry')
|
||||||
|
self.mock_responses_for_create('vzInTerm')
|
||||||
|
self.mock_responses_for_create('vzRsFiltAtt__In')
|
||||||
|
self.mock_responses_for_create('vzOutTerm')
|
||||||
|
self.mock_responses_for_create('vzRsFiltAtt__Out')
|
||||||
|
self.mock_responses_for_create('vzCPIf')
|
||||||
|
self.mock_responses_for_create('vzRsIf')
|
||||||
|
new_contract = self.mgr.create_tenant_contract(tenant)
|
||||||
|
self.assert_responses_drained()
|
||||||
|
self.assertTrue(self.mocked_session.add.called)
|
||||||
|
self.assertTrue(self.mocked_session.flush.called)
|
||||||
|
self.assertEqual(new_contract['tenant_id'], tenant)
|
||||||
|
|
||||||
|
def test_create_tenant_contract_exc(self):
|
||||||
|
tenant = mocked.APIC_TENANT
|
||||||
|
self.mock_db_query_filterby_first_return(None)
|
||||||
|
self.mock_error_post_response(wexc.HTTPBadRequest)
|
||||||
|
self.mock_response_for_post('vzBrCP')
|
||||||
|
self.assertRaises(cexc.ApicResponseNotOk,
|
||||||
|
self.mgr.create_tenant_contract, tenant)
|
||||||
|
self.assert_responses_drained()
|
@ -0,0 +1,226 @@
|
|||||||
|
# Copyright (c) 2014 Cisco Systems
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# @author: Henry Gessau, Cisco Systems
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from neutron.plugins.ml2.drivers.cisco.apic import mechanism_apic as md
|
||||||
|
from neutron.plugins.ml2.drivers import type_vlan # noqa
|
||||||
|
from neutron.tests import base
|
||||||
|
from neutron.tests.unit.ml2.drivers.cisco.apic import (
|
||||||
|
test_cisco_apic_common as mocked)
|
||||||
|
|
||||||
|
|
||||||
|
HOST_ID1 = 'ubuntu'
|
||||||
|
HOST_ID2 = 'rhel'
|
||||||
|
ENCAP = '101'
|
||||||
|
|
||||||
|
SUBNET_GATEWAY = '10.3.2.1'
|
||||||
|
SUBNET_CIDR = '10.3.1.0/24'
|
||||||
|
SUBNET_NETMASK = '24'
|
||||||
|
|
||||||
|
TEST_SEGMENT1 = 'test-segment1'
|
||||||
|
TEST_SEGMENT2 = 'test-segment2'
|
||||||
|
|
||||||
|
|
||||||
|
class TestCiscoApicMechDriver(base.BaseTestCase,
|
||||||
|
mocked.ControllerMixin,
|
||||||
|
mocked.ConfigMixin,
|
||||||
|
mocked.DbModelMixin):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCiscoApicMechDriver, self).setUp()
|
||||||
|
mocked.ControllerMixin.set_up_mocks(self)
|
||||||
|
mocked.ConfigMixin.set_up_mocks(self)
|
||||||
|
mocked.DbModelMixin.set_up_mocks(self)
|
||||||
|
|
||||||
|
self.mock_apic_manager_login_responses()
|
||||||
|
self.driver = md.APICMechanismDriver()
|
||||||
|
self.driver.vif_type = 'test-vif_type'
|
||||||
|
self.driver.cap_port_filter = 'test-cap_port_filter'
|
||||||
|
|
||||||
|
def test_initialize(self):
|
||||||
|
cfg.CONF.set_override('network_vlan_ranges', ['physnet1:100:199'],
|
||||||
|
'ml2_type_vlan')
|
||||||
|
ns = mocked.APIC_VLAN_NAME
|
||||||
|
mode = mocked.APIC_VLAN_MODE
|
||||||
|
self.mock_response_for_get('fvnsVlanInstP', name=ns, mode=mode)
|
||||||
|
self.mock_response_for_get('physDomP', name=mocked.APIC_DOMAIN)
|
||||||
|
self.mock_response_for_get('infraAttEntityP',
|
||||||
|
name=mocked.APIC_ATT_ENT_PROF)
|
||||||
|
self.mock_response_for_get('infraAccPortGrp',
|
||||||
|
name=mocked.APIC_ACC_PORT_GRP)
|
||||||
|
mock.patch('neutron.plugins.ml2.drivers.cisco.apic.apic_manager.'
|
||||||
|
'APICManager.ensure_infra_created_on_apic').start()
|
||||||
|
self.driver.initialize()
|
||||||
|
self.session = self.driver.apic_manager.apic.session
|
||||||
|
self.assert_responses_drained()
|
||||||
|
|
||||||
|
def test_update_port_postcommit(self):
|
||||||
|
net_ctx = self._get_network_context(mocked.APIC_TENANT,
|
||||||
|
mocked.APIC_NETWORK,
|
||||||
|
TEST_SEGMENT1)
|
||||||
|
port_ctx = self._get_port_context(mocked.APIC_TENANT,
|
||||||
|
mocked.APIC_NETWORK,
|
||||||
|
'vm1', net_ctx, HOST_ID1)
|
||||||
|
mgr = self.driver.apic_manager = mock.Mock()
|
||||||
|
self.driver.update_port_postcommit(port_ctx)
|
||||||
|
mgr.ensure_tenant_created_on_apic.assert_called_once_with(
|
||||||
|
mocked.APIC_TENANT)
|
||||||
|
mgr.ensure_path_created_for_port.assert_called_once_with(
|
||||||
|
mocked.APIC_TENANT, mocked.APIC_NETWORK, HOST_ID1,
|
||||||
|
ENCAP, mocked.APIC_NETWORK + '-name')
|
||||||
|
|
||||||
|
def test_create_network_postcommit(self):
|
||||||
|
ctx = self._get_network_context(mocked.APIC_TENANT,
|
||||||
|
mocked.APIC_NETWORK,
|
||||||
|
TEST_SEGMENT1)
|
||||||
|
mgr = self.driver.apic_manager = mock.Mock()
|
||||||
|
self.driver.create_network_postcommit(ctx)
|
||||||
|
mgr.ensure_bd_created_on_apic.assert_called_once_with(
|
||||||
|
mocked.APIC_TENANT, mocked.APIC_NETWORK)
|
||||||
|
mgr.ensure_epg_created_for_network.assert_called_once_with(
|
||||||
|
mocked.APIC_TENANT, mocked.APIC_NETWORK,
|
||||||
|
mocked.APIC_NETWORK + '-name')
|
||||||
|
|
||||||
|
def test_delete_network_postcommit(self):
|
||||||
|
ctx = self._get_network_context(mocked.APIC_TENANT,
|
||||||
|
mocked.APIC_NETWORK,
|
||||||
|
TEST_SEGMENT1)
|
||||||
|
mgr = self.driver.apic_manager = mock.Mock()
|
||||||
|
self.driver.delete_network_postcommit(ctx)
|
||||||
|
mgr.delete_bd_on_apic.assert_called_once_with(
|
||||||
|
mocked.APIC_TENANT, mocked.APIC_NETWORK)
|
||||||
|
mgr.delete_epg_for_network.assert_called_once_with(
|
||||||
|
mocked.APIC_TENANT, mocked.APIC_NETWORK)
|
||||||
|
|
||||||
|
def test_create_subnet_postcommit(self):
|
||||||
|
net_ctx = self._get_network_context(mocked.APIC_TENANT,
|
||||||
|
mocked.APIC_NETWORK,
|
||||||
|
TEST_SEGMENT1)
|
||||||
|
subnet_ctx = self._get_subnet_context(SUBNET_GATEWAY,
|
||||||
|
SUBNET_CIDR,
|
||||||
|
net_ctx)
|
||||||
|
mgr = self.driver.apic_manager = mock.Mock()
|
||||||
|
self.driver.create_subnet_postcommit(subnet_ctx)
|
||||||
|
mgr.ensure_subnet_created_on_apic.assert_called_once_with(
|
||||||
|
mocked.APIC_TENANT, mocked.APIC_NETWORK,
|
||||||
|
'%s/%s' % (SUBNET_GATEWAY, SUBNET_NETMASK))
|
||||||
|
|
||||||
|
def _get_network_context(self, tenant_id, net_id, seg_id=None,
|
||||||
|
seg_type='vlan'):
|
||||||
|
network = {'id': net_id,
|
||||||
|
'name': net_id + '-name',
|
||||||
|
'tenant_id': tenant_id,
|
||||||
|
'provider:segmentation_id': seg_id}
|
||||||
|
if seg_id:
|
||||||
|
network_segments = [{'id': seg_id,
|
||||||
|
'segmentation_id': ENCAP,
|
||||||
|
'network_type': seg_type,
|
||||||
|
'physical_network': 'physnet1'}]
|
||||||
|
else:
|
||||||
|
network_segments = []
|
||||||
|
return FakeNetworkContext(network, network_segments)
|
||||||
|
|
||||||
|
def _get_subnet_context(self, gateway_ip, cidr, network):
|
||||||
|
subnet = {'tenant_id': network.current['tenant_id'],
|
||||||
|
'network_id': network.current['id'],
|
||||||
|
'id': '[%s/%s]' % (gateway_ip, cidr),
|
||||||
|
'gateway_ip': gateway_ip,
|
||||||
|
'cidr': cidr}
|
||||||
|
return FakeSubnetContext(subnet, network)
|
||||||
|
|
||||||
|
def _get_port_context(self, tenant_id, net_id, vm_id, network, host):
|
||||||
|
port = {'device_id': vm_id,
|
||||||
|
'device_owner': 'compute',
|
||||||
|
'binding:host_id': host,
|
||||||
|
'tenant_id': tenant_id,
|
||||||
|
'id': mocked.APIC_PORT,
|
||||||
|
'name': mocked.APIC_PORT,
|
||||||
|
'network_id': net_id}
|
||||||
|
return FakePortContext(port, network)
|
||||||
|
|
||||||
|
|
||||||
|
class FakeNetworkContext(object):
|
||||||
|
"""To generate network context for testing purposes only."""
|
||||||
|
|
||||||
|
def __init__(self, network, segments):
|
||||||
|
self._network = network
|
||||||
|
self._segments = segments
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self):
|
||||||
|
return self._network
|
||||||
|
|
||||||
|
@property
|
||||||
|
def network_segments(self):
|
||||||
|
return self._segments
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSubnetContext(object):
|
||||||
|
"""To generate subnet context for testing purposes only."""
|
||||||
|
|
||||||
|
def __init__(self, subnet, network):
|
||||||
|
self._subnet = subnet
|
||||||
|
self._network = network
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self):
|
||||||
|
return self._subnet
|
||||||
|
|
||||||
|
@property
|
||||||
|
def network(self):
|
||||||
|
return self._network
|
||||||
|
|
||||||
|
|
||||||
|
class FakePortContext(object):
|
||||||
|
"""To generate port context for testing purposes only."""
|
||||||
|
|
||||||
|
def __init__(self, port, network):
|
||||||
|
self._fake_plugin = mock.Mock()
|
||||||
|
self._fake_plugin.get_ports.return_value = []
|
||||||
|
self._fake_plugin_context = None
|
||||||
|
self._port = port
|
||||||
|
self._network = network
|
||||||
|
if network.network_segments:
|
||||||
|
self._bound_segment = network.network_segments[0]
|
||||||
|
else:
|
||||||
|
self._bound_segment = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self):
|
||||||
|
return self._port
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _plugin(self):
|
||||||
|
return self._fake_plugin
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _plugin_context(self):
|
||||||
|
return self._fake_plugin_context
|
||||||
|
|
||||||
|
@property
|
||||||
|
def network(self):
|
||||||
|
return self._network
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bound_segment(self):
|
||||||
|
return self._bound_segment
|
||||||
|
|
||||||
|
def set_binding(self, segment_id, vif_type, cap_port_filter):
|
||||||
|
pass
|
@ -150,6 +150,7 @@ neutron.ml2.mechanism_drivers =
|
|||||||
ncs = neutron.plugins.ml2.drivers.mechanism_ncs:NCSMechanismDriver
|
ncs = neutron.plugins.ml2.drivers.mechanism_ncs:NCSMechanismDriver
|
||||||
arista = neutron.plugins.ml2.drivers.mech_arista.mechanism_arista:AristaDriver
|
arista = neutron.plugins.ml2.drivers.mech_arista.mechanism_arista:AristaDriver
|
||||||
cisco_nexus = neutron.plugins.ml2.drivers.cisco.nexus.mech_cisco_nexus:CiscoNexusMechanismDriver
|
cisco_nexus = neutron.plugins.ml2.drivers.cisco.nexus.mech_cisco_nexus:CiscoNexusMechanismDriver
|
||||||
|
cisco_apic = neutron.plugins.ml2.drivers.cisco.apic.mechanism_apic:APICMechanismDriver
|
||||||
l2population = neutron.plugins.ml2.drivers.l2pop.mech_driver:L2populationMechanismDriver
|
l2population = neutron.plugins.ml2.drivers.l2pop.mech_driver:L2populationMechanismDriver
|
||||||
bigswitch = neutron.plugins.ml2.drivers.mech_bigswitch.driver:BigSwitchMechanismDriver
|
bigswitch = neutron.plugins.ml2.drivers.mech_bigswitch.driver:BigSwitchMechanismDriver
|
||||||
ofagent = neutron.plugins.ml2.drivers.mech_ofagent:OfagentMechanismDriver
|
ofagent = neutron.plugins.ml2.drivers.mech_ofagent:OfagentMechanismDriver
|
||||||
|
Loading…
Reference in New Issue
Block a user