From: Arvind Somya Date: Thu, 13 Feb 2014 17:57:50 +0000 (-0800) Subject: Cisco APIC ML2 mechanism driver, part 2 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=26cf7e2f9483f993571a573293cfc13b3c6bba58;p=openstack-build%2Fneutron-build.git Cisco APIC ML2 mechanism driver, part 2 This set of changes introduces a mechanism driver for the Cisco APIC. This is the second and final part of a 2 part commit. Please see the blueprint for more information. The review is submitted in two parts: - Part 1 (Posted earlier, required for Part 2) o APIC REST Client o APIC data model and migration script o APIC configurations - Part 2 (this commit) o APIC mechanism driver o APIC manager Partially implements: blueprint ml2-cisco-apic-mechanism-driver Change-Id: I5ed3ac133146635083e2d0093057b43b64f347fe --- diff --git a/neutron/plugins/ml2/drivers/cisco/apic/apic_manager.py b/neutron/plugins/ml2/drivers/cisco/apic/apic_manager.py new file mode 100644 index 000000000..f86aa597d --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/apic/apic_manager.py @@ -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") diff --git a/neutron/plugins/ml2/drivers/cisco/apic/exceptions.py b/neutron/plugins/ml2/drivers/cisco/apic/exceptions.py index 1c478853b..b33abb17d 100644 --- a/neutron/plugins/ml2/drivers/cisco/apic/exceptions.py +++ b/neutron/plugins/ml2/drivers/cisco/apic/exceptions.py @@ -50,3 +50,10 @@ class ApicHostNotConfigured(exceptions.NotAuthorized): class ApicManagedObjectNotSupported(exceptions.NeutronException): """Attempted to use an unsupported Managed Object.""" 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'") diff --git a/neutron/plugins/ml2/drivers/cisco/apic/mechanism_apic.py b/neutron/plugins/ml2/drivers/cisco/apic/mechanism_apic.py new file mode 100644 index 000000000..d5297df68 --- /dev/null +++ b/neutron/plugins/ml2/drivers/cisco/apic/mechanism_apic.py @@ -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) diff --git a/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_common.py b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_common.py index 3c42b98ac..7a8f8e9d5 100644 --- a/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_common.py +++ b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_common.py @@ -23,7 +23,7 @@ from oslo.config import cfg from neutron.common import config as neutron_config from neutron.plugins.ml2 import config as ml2_config 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 @@ -169,7 +169,7 @@ class ConfigMixin(object): def set_up_mocks(self): # 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) # Configure the ML2 mechanism drivers and network types diff --git a/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_manager.py b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_manager.py new file mode 100644 index 000000000..24a2c217d --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_manager.py @@ -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() diff --git a/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_mechanism_driver.py b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_mechanism_driver.py new file mode 100644 index 000000000..6addd4382 --- /dev/null +++ b/neutron/tests/unit/ml2/drivers/cisco/apic/test_cisco_apic_mechanism_driver.py @@ -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 diff --git a/setup.cfg b/setup.cfg index 4308c71be..b44e1b3ea 100644 --- a/setup.cfg +++ b/setup.cfg @@ -150,6 +150,7 @@ neutron.ml2.mechanism_drivers = ncs = neutron.plugins.ml2.drivers.mechanism_ncs:NCSMechanismDriver arista = neutron.plugins.ml2.drivers.mech_arista.mechanism_arista:AristaDriver 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 bigswitch = neutron.plugins.ml2.drivers.mech_bigswitch.driver:BigSwitchMechanismDriver ofagent = neutron.plugins.ml2.drivers.mech_ofagent:OfagentMechanismDriver