# 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 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")