Cisco APIC ML2 mechanism driver, part 1
This set of changes introduces a mechanism driver for the Cisco APIC. Please see the blueprint for more information. The review is submitted in two parts: - Part 1 (this one) o APIC REST Client o APIC data model and migration script o APIC configurations - Part 2 (dependent on part 1) o APIC mechanism driver o APIC manager Partially implements: blueprint ml2-cisco-apic-mechanism-driver Change-Id: I698b25ca975fed746107ee64f03563ef1a56e0ef
This commit is contained in:
parent
1f4df4e528
commit
a234ecda87
@ -46,3 +46,49 @@
|
||||
# ssh_port=22
|
||||
# username=admin
|
||||
# password=mySecretPassword
|
||||
|
||||
[ml2_cisco_apic]
|
||||
|
||||
# Hostname for the APIC controller
|
||||
# apic_host=1.1.1.1
|
||||
|
||||
# Username for the APIC controller
|
||||
# apic_username=user
|
||||
|
||||
# Password for the APIC controller
|
||||
# apic_password=password
|
||||
|
||||
# Port for the APIC Controller
|
||||
# apic_port=80
|
||||
|
||||
# Names for APIC objects used by Neutron
|
||||
# Note: When deploying multiple clouds against one APIC,
|
||||
# these names must be unique between the clouds.
|
||||
# apic_vmm_domain=openstack
|
||||
# apic_vlan_ns_name=openstack_ns
|
||||
# apic_node_profile=openstack_profile
|
||||
# apic_entity_profile=openstack_entity
|
||||
# apic_function_profile=openstack_function
|
||||
|
||||
# The following flag will cause all the node profiles on the APIC to
|
||||
# be cleared when neutron-server starts. This is typically used only
|
||||
# for test environments that require clean-slate startup conditions.
|
||||
# apic_clear_node_profiles=False
|
||||
|
||||
# Specify your network topology.
|
||||
# This section indicates how your compute nodes are connected to the fabric's
|
||||
# switches and ports. The format is as follows:
|
||||
#
|
||||
# [switch:<swich_id_from_the_apic>]
|
||||
# <compute_host>,<compute_host>=<switchport_the_host(s)_are_connected_to>
|
||||
#
|
||||
# You can have multiple sections, one for each switch in your fabric that is
|
||||
# participating in Openstack. e.g.
|
||||
#
|
||||
# [switch:17]
|
||||
# ubuntu,ubuntu1=1/10
|
||||
# ubuntu2,ubuntu3=1/11
|
||||
#
|
||||
# [switch:18]
|
||||
# ubuntu5,ubuntu6=1/1
|
||||
# ubuntu7,ubuntu8=1/2
|
||||
|
@ -0,0 +1,74 @@
|
||||
# Copyright 2014 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
"""Cisco APIC Mechanism Driver
|
||||
|
||||
Revision ID: 1b837a7125a9
|
||||
Revises: 6be312499f9
|
||||
Create Date: 2014-02-13 09:35:19.147619
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '1b837a7125a9'
|
||||
down_revision = '6be312499f9'
|
||||
|
||||
migration_for_plugins = [
|
||||
'neutron.plugins.ml2.plugin.Ml2Plugin'
|
||||
]
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from neutron.db import migration
|
||||
|
||||
|
||||
def upgrade(active_plugins=None, options=None):
|
||||
if not migration.should_run(active_plugins, migration_for_plugins):
|
||||
return
|
||||
|
||||
op.create_table(
|
||||
'cisco_ml2_apic_epgs',
|
||||
sa.Column('network_id', sa.String(length=255), nullable=False),
|
||||
sa.Column('epg_id', sa.String(length=64), nullable=False),
|
||||
sa.Column('segmentation_id', sa.String(length=64), nullable=False),
|
||||
sa.Column('provider', sa.Boolean(), default=False, nullable=False),
|
||||
sa.PrimaryKeyConstraint('network_id'))
|
||||
|
||||
op.create_table(
|
||||
'cisco_ml2_apic_port_profiles',
|
||||
sa.Column('node_id', sa.String(length=255), nullable=False),
|
||||
sa.Column('profile_id', sa.String(length=64), nullable=False),
|
||||
sa.Column('hpselc_id', sa.String(length=64), nullable=False),
|
||||
sa.Column('module', sa.String(length=10), nullable=False),
|
||||
sa.Column('from_port', sa.Integer(), nullable=False),
|
||||
sa.Column('to_port', sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('node_id'))
|
||||
|
||||
op.create_table(
|
||||
'cisco_ml2_apic_contracts',
|
||||
sa.Column('tenant_id', sa.String(length=255), nullable=False),
|
||||
sa.Column('contract_id', sa.String(length=64), nullable=False),
|
||||
sa.Column('filter_id', sa.String(length=64), nullable=False),
|
||||
sa.PrimaryKeyConstraint('tenant_id'))
|
||||
|
||||
|
||||
def downgrade(active_plugins=None, options=None):
|
||||
if not migration.should_run(active_plugins, migration_for_plugins):
|
||||
return
|
||||
|
||||
op.drop_table('cisco_ml2_apic_contracts')
|
||||
op.drop_table('cisco_ml2_apic_port_profiles')
|
||||
op.drop_table('cisco_ml2_apic_epgs')
|
@ -1 +1 @@
|
||||
6be312499f9
|
||||
1b837a7125a9
|
||||
|
0
neutron/plugins/ml2/drivers/cisco/apic/__init__.py
Normal file
0
neutron/plugins/ml2/drivers/cisco/apic/__init__.py
Normal file
416
neutron/plugins/ml2/drivers/cisco/apic/apic_client.py
Normal file
416
neutron/plugins/ml2/drivers/cisco/apic/apic_client.py
Normal file
@ -0,0 +1,416 @@
|
||||
# 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 collections
|
||||
import time
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
|
||||
from neutron.openstack.common import jsonutils as json
|
||||
from neutron.openstack.common import log as logging
|
||||
from neutron.plugins.ml2.drivers.cisco.apic import exceptions as cexc
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
APIC_CODE_FORBIDDEN = str(requests.codes.forbidden)
|
||||
|
||||
|
||||
# Info about a Managed Object's relative name (RN) and container.
|
||||
class ManagedObjectName(collections.namedtuple(
|
||||
'MoPath', ['container', 'rn_fmt', 'can_create'])):
|
||||
def __new__(cls, container, rn_fmt, can_create=True):
|
||||
return super(ManagedObjectName, cls).__new__(cls, container, rn_fmt,
|
||||
can_create)
|
||||
|
||||
|
||||
class ManagedObjectClass(object):
|
||||
|
||||
"""Information about a Managed Object (MO) class.
|
||||
|
||||
Constructs and keeps track of the distinguished name (DN) and relative
|
||||
name (RN) of a managed object (MO) class. The DN is the RN of the MO
|
||||
appended to the recursive RNs of its containers, i.e.:
|
||||
DN = uni/container-RN/.../container-RN/object-RN
|
||||
|
||||
Also keeps track of whether the MO can be created in the APIC, as some
|
||||
MOs are read-only or used for specifying relationships.
|
||||
"""
|
||||
|
||||
supported_mos = {
|
||||
'fvTenant': ManagedObjectName(None, 'tn-%s'),
|
||||
'fvBD': ManagedObjectName('fvTenant', 'BD-%s'),
|
||||
'fvRsBd': ManagedObjectName('fvAEPg', 'rsbd'),
|
||||
'fvSubnet': ManagedObjectName('fvBD', 'subnet-[%s]'),
|
||||
'fvCtx': ManagedObjectName('fvTenant', 'ctx-%s'),
|
||||
'fvRsCtx': ManagedObjectName('fvBD', 'rsctx'),
|
||||
'fvAp': ManagedObjectName('fvTenant', 'ap-%s'),
|
||||
'fvAEPg': ManagedObjectName('fvAp', 'epg-%s'),
|
||||
'fvRsProv': ManagedObjectName('fvAEPg', 'rsprov-%s'),
|
||||
'fvRsCons': ManagedObjectName('fvAEPg', 'rscons-%s'),
|
||||
'fvRsConsIf': ManagedObjectName('fvAEPg', 'rsconsif-%s'),
|
||||
'fvRsDomAtt': ManagedObjectName('fvAEPg', 'rsdomAtt-[%s]'),
|
||||
'fvRsPathAtt': ManagedObjectName('fvAEPg', 'rspathAtt-[%s]'),
|
||||
|
||||
'vzBrCP': ManagedObjectName('fvTenant', 'brc-%s'),
|
||||
'vzSubj': ManagedObjectName('vzBrCP', 'subj-%s'),
|
||||
'vzFilter': ManagedObjectName('fvTenant', 'flt-%s'),
|
||||
'vzRsFiltAtt': ManagedObjectName('vzSubj', 'rsfiltAtt-%s'),
|
||||
'vzEntry': ManagedObjectName('vzFilter', 'e-%s'),
|
||||
'vzInTerm': ManagedObjectName('vzSubj', 'intmnl'),
|
||||
'vzRsFiltAtt__In': ManagedObjectName('vzInTerm', 'rsfiltAtt-%s'),
|
||||
'vzOutTerm': ManagedObjectName('vzSubj', 'outtmnl'),
|
||||
'vzRsFiltAtt__Out': ManagedObjectName('vzOutTerm', 'rsfiltAtt-%s'),
|
||||
'vzCPIf': ManagedObjectName('fvTenant', 'cif-%s'),
|
||||
'vzRsIf': ManagedObjectName('vzCPIf', 'rsif'),
|
||||
|
||||
'vmmProvP': ManagedObjectName(None, 'vmmp-%s', False),
|
||||
'vmmDomP': ManagedObjectName('vmmProvP', 'dom-%s'),
|
||||
'vmmEpPD': ManagedObjectName('vmmDomP', 'eppd-[%s]'),
|
||||
|
||||
'physDomP': ManagedObjectName(None, 'phys-%s'),
|
||||
|
||||
'infra': ManagedObjectName(None, 'infra'),
|
||||
'infraNodeP': ManagedObjectName('infra', 'nprof-%s'),
|
||||
'infraLeafS': ManagedObjectName('infraNodeP', 'leaves-%s-typ-%s'),
|
||||
'infraNodeBlk': ManagedObjectName('infraLeafS', 'nodeblk-%s'),
|
||||
'infraRsAccPortP': ManagedObjectName('infraNodeP', 'rsaccPortP-[%s]'),
|
||||
'infraAccPortP': ManagedObjectName('infra', 'accportprof-%s'),
|
||||
'infraHPortS': ManagedObjectName('infraAccPortP', 'hports-%s-typ-%s'),
|
||||
'infraPortBlk': ManagedObjectName('infraHPortS', 'portblk-%s'),
|
||||
'infraRsAccBaseGrp': ManagedObjectName('infraHPortS', 'rsaccBaseGrp'),
|
||||
'infraFuncP': ManagedObjectName('infra', 'funcprof'),
|
||||
'infraAccPortGrp': ManagedObjectName('infraFuncP', 'accportgrp-%s'),
|
||||
'infraRsAttEntP': ManagedObjectName('infraAccPortGrp', 'rsattEntP'),
|
||||
'infraAttEntityP': ManagedObjectName('infra', 'attentp-%s'),
|
||||
'infraRsDomP': ManagedObjectName('infraAttEntityP', 'rsdomP-[%s]'),
|
||||
'infraRsVlanNs__phys': ManagedObjectName('physDomP', 'rsvlanNs'),
|
||||
'infraRsVlanNs__vmm': ManagedObjectName('vmmDomP', 'rsvlanNs'),
|
||||
|
||||
'fvnsVlanInstP': ManagedObjectName('infra', 'vlanns-%s-%s'),
|
||||
'fvnsEncapBlk__vlan': ManagedObjectName('fvnsVlanInstP',
|
||||
'from-%s-to-%s'),
|
||||
'fvnsVxlanInstP': ManagedObjectName('infra', 'vxlanns-%s'),
|
||||
'fvnsEncapBlk__vxlan': ManagedObjectName('fvnsVxlanInstP',
|
||||
'from-%s-to-%s'),
|
||||
|
||||
# Read-only
|
||||
'fabricTopology': ManagedObjectName(None, 'topology', False),
|
||||
'fabricPod': ManagedObjectName('fabricTopology', 'pod-%s', False),
|
||||
'fabricPathEpCont': ManagedObjectName('fabricPod', 'paths-%s', False),
|
||||
'fabricPathEp': ManagedObjectName('fabricPathEpCont', 'pathep-%s',
|
||||
False),
|
||||
}
|
||||
|
||||
# Note(Henry): The use of a mutable default argument _inst_cache is
|
||||
# intentional. It persists for the life of MoClass to cache instances.
|
||||
# noinspection PyDefaultArgument
|
||||
def __new__(cls, mo_class, _inst_cache={}):
|
||||
"""Ensure we create only one instance per mo_class."""
|
||||
try:
|
||||
return _inst_cache[mo_class]
|
||||
except KeyError:
|
||||
new_inst = super(ManagedObjectClass, cls).__new__(cls)
|
||||
new_inst.__init__(mo_class)
|
||||
_inst_cache[mo_class] = new_inst
|
||||
return new_inst
|
||||
|
||||
def __init__(self, mo_class):
|
||||
self.klass = mo_class
|
||||
self.klass_name = mo_class.split('__')[0]
|
||||
mo = self.supported_mos[mo_class]
|
||||
self.container = mo.container
|
||||
self.rn_fmt = mo.rn_fmt
|
||||
self.dn_fmt, self.args = self._dn_fmt()
|
||||
self.arg_count = self.dn_fmt.count('%s')
|
||||
rn_has_arg = self.rn_fmt.count('%s')
|
||||
self.can_create = rn_has_arg and mo.can_create
|
||||
|
||||
def _dn_fmt(self):
|
||||
"""Build the distinguished name format using container and RN.
|
||||
|
||||
DN = uni/container-RN/.../container-RN/object-RN
|
||||
|
||||
Also make a list of the required name arguments.
|
||||
Note: Call this method only once at init.
|
||||
"""
|
||||
arg = [self.klass] if '%s' in self.rn_fmt else []
|
||||
if self.container:
|
||||
container = ManagedObjectClass(self.container)
|
||||
dn_fmt = '%s/%s' % (container.dn_fmt, self.rn_fmt)
|
||||
args = container.args + arg
|
||||
return dn_fmt, args
|
||||
return 'uni/%s' % self.rn_fmt, arg
|
||||
|
||||
def dn(self, *args):
|
||||
"""Return the distinguished name for a managed object."""
|
||||
return self.dn_fmt % args
|
||||
|
||||
|
||||
class ApicSession(object):
|
||||
|
||||
"""Manages a session with the APIC."""
|
||||
|
||||
def __init__(self, host, port, usr, pwd, ssl):
|
||||
protocol = ssl and 'https' or 'http'
|
||||
self.api_base = '%s://%s:%s/api' % (protocol, host, port)
|
||||
self.session = requests.Session()
|
||||
self.session_deadline = 0
|
||||
self.session_timeout = 0
|
||||
self.cookie = {}
|
||||
|
||||
# Log in
|
||||
self.authentication = None
|
||||
self.username = None
|
||||
self.password = None
|
||||
if usr and pwd:
|
||||
self.login(usr, pwd)
|
||||
|
||||
@staticmethod
|
||||
def _make_data(key, **attrs):
|
||||
"""Build the body for a msg out of a key and some attributes."""
|
||||
return json.dumps({key: {'attributes': attrs}})
|
||||
|
||||
def _api_url(self, api):
|
||||
"""Create the URL for a generic API."""
|
||||
return '%s/%s.json' % (self.api_base, api)
|
||||
|
||||
def _mo_url(self, mo, *args):
|
||||
"""Create a URL for a MO lookup by DN."""
|
||||
dn = mo.dn(*args)
|
||||
return '%s/mo/%s.json' % (self.api_base, dn)
|
||||
|
||||
def _qry_url(self, mo):
|
||||
"""Create a URL for a query lookup by MO class."""
|
||||
return '%s/class/%s.json' % (self.api_base, mo.klass_name)
|
||||
|
||||
def _check_session(self):
|
||||
"""Check that we are logged in and ensure the session is active."""
|
||||
if not self.authentication:
|
||||
raise cexc.ApicSessionNotLoggedIn
|
||||
if time.time() > self.session_deadline:
|
||||
self.refresh()
|
||||
|
||||
def _send(self, request, url, data=None, refreshed=None):
|
||||
"""Send a request and process the response."""
|
||||
if data is None:
|
||||
response = request(url, cookies=self.cookie)
|
||||
else:
|
||||
response = request(url, data=data, cookies=self.cookie)
|
||||
if response is None:
|
||||
raise cexc.ApicHostNoResponse(url=url)
|
||||
# Every request refreshes the timeout
|
||||
self.session_deadline = time.time() + self.session_timeout
|
||||
if data is None:
|
||||
request_str = url
|
||||
else:
|
||||
request_str = '%s, data=%s' % (url, data)
|
||||
LOG.debug(_("data = %s"), data)
|
||||
# imdata is where the APIC returns the useful information
|
||||
imdata = response.json().get('imdata')
|
||||
LOG.debug(_("Response: %s"), imdata)
|
||||
if response.status_code != requests.codes.ok:
|
||||
try:
|
||||
err_code = imdata[0]['error']['attributes']['code']
|
||||
err_text = imdata[0]['error']['attributes']['text']
|
||||
except (IndexError, KeyError):
|
||||
err_code = '[code for APIC error not found]'
|
||||
err_text = '[text for APIC error not found]'
|
||||
# If invalid token then re-login and retry once
|
||||
if (not refreshed and err_code == APIC_CODE_FORBIDDEN and
|
||||
err_text.lower().startswith('token was invalid')):
|
||||
self.login()
|
||||
return self._send(request, url, data=data, refreshed=True)
|
||||
raise cexc.ApicResponseNotOk(request=request_str,
|
||||
status=response.status_code,
|
||||
reason=response.reason,
|
||||
err_text=err_text, err_code=err_code)
|
||||
return imdata
|
||||
|
||||
# REST requests
|
||||
|
||||
def get_data(self, request):
|
||||
"""Retrieve generic data from the server."""
|
||||
self._check_session()
|
||||
url = self._api_url(request)
|
||||
return self._send(self.session.get, url)
|
||||
|
||||
def get_mo(self, mo, *args):
|
||||
"""Retrieve a managed object by its distinguished name."""
|
||||
self._check_session()
|
||||
url = self._mo_url(mo, *args) + '?query-target=self'
|
||||
return self._send(self.session.get, url)
|
||||
|
||||
def list_mo(self, mo):
|
||||
"""Retrieve the list of managed objects for a class."""
|
||||
self._check_session()
|
||||
url = self._qry_url(mo)
|
||||
return self._send(self.session.get, url)
|
||||
|
||||
def post_data(self, request, data):
|
||||
"""Post generic data to the server."""
|
||||
self._check_session()
|
||||
url = self._api_url(request)
|
||||
return self._send(self.session.post, url, data=data)
|
||||
|
||||
def post_mo(self, mo, *args, **kwargs):
|
||||
"""Post data for a managed object to the server."""
|
||||
self._check_session()
|
||||
url = self._mo_url(mo, *args)
|
||||
data = self._make_data(mo.klass_name, **kwargs)
|
||||
return self._send(self.session.post, url, data=data)
|
||||
|
||||
# Session management
|
||||
|
||||
def _save_cookie(self, request, response):
|
||||
"""Save the session cookie and its expiration time."""
|
||||
imdata = response.json().get('imdata')
|
||||
if response.status_code == requests.codes.ok:
|
||||
attributes = imdata[0]['aaaLogin']['attributes']
|
||||
try:
|
||||
self.cookie = {'APIC-Cookie': attributes['token']}
|
||||
except KeyError:
|
||||
raise cexc.ApicResponseNoCookie(request=request)
|
||||
timeout = int(attributes['refreshTimeoutSeconds'])
|
||||
LOG.debug(_("APIC session will expire in %d seconds"), timeout)
|
||||
# Give ourselves a few seconds to refresh before timing out
|
||||
self.session_timeout = timeout - 5
|
||||
self.session_deadline = time.time() + self.session_timeout
|
||||
else:
|
||||
attributes = imdata[0]['error']['attributes']
|
||||
return attributes
|
||||
|
||||
def login(self, usr=None, pwd=None):
|
||||
"""Log in to controller. Save user name and authentication."""
|
||||
usr = usr or self.username
|
||||
pwd = pwd or self.password
|
||||
name_pwd = self._make_data('aaaUser', name=usr, pwd=pwd)
|
||||
url = self._api_url('aaaLogin')
|
||||
try:
|
||||
response = self.session.post(url, data=name_pwd, timeout=10.0)
|
||||
except requests.exceptions.Timeout:
|
||||
raise cexc.ApicHostNoResponse(url=url)
|
||||
attributes = self._save_cookie('aaaLogin', response)
|
||||
if response.status_code == requests.codes.ok:
|
||||
self.username = usr
|
||||
self.password = pwd
|
||||
self.authentication = attributes
|
||||
else:
|
||||
self.authentication = None
|
||||
raise cexc.ApicResponseNotOk(request=url,
|
||||
status=response.status_code,
|
||||
reason=response.reason,
|
||||
err_text=attributes['text'],
|
||||
err_code=attributes['code'])
|
||||
|
||||
def refresh(self):
|
||||
"""Called when a session has timed out or almost timed out."""
|
||||
url = self._api_url('aaaRefresh')
|
||||
response = self.session.get(url, cookies=self.cookie)
|
||||
attributes = self._save_cookie('aaaRefresh', response)
|
||||
if response.status_code == requests.codes.ok:
|
||||
# We refreshed before the session timed out.
|
||||
self.authentication = attributes
|
||||
else:
|
||||
err_code = attributes['code']
|
||||
err_text = attributes['text']
|
||||
if (err_code == APIC_CODE_FORBIDDEN and
|
||||
err_text.lower().startswith('token was invalid')):
|
||||
# This means the token timed out, so log in again.
|
||||
LOG.debug(_("APIC session timed-out, logging in again."))
|
||||
self.login()
|
||||
else:
|
||||
self.authentication = None
|
||||
raise cexc.ApicResponseNotOk(request=url,
|
||||
status=response.status_code,
|
||||
reason=response.reason,
|
||||
err_text=err_text,
|
||||
err_code=err_code)
|
||||
|
||||
def logout(self):
|
||||
"""End session with controller."""
|
||||
if not self.username:
|
||||
self.authentication = None
|
||||
if self.authentication:
|
||||
data = self._make_data('aaaUser', name=self.username)
|
||||
self.post_data('aaaLogout', data=data)
|
||||
self.authentication = None
|
||||
|
||||
|
||||
class ManagedObjectAccess(object):
|
||||
|
||||
"""CRUD operations on APIC Managed Objects."""
|
||||
|
||||
def __init__(self, session, mo_class):
|
||||
self.session = session
|
||||
self.mo = ManagedObjectClass(mo_class)
|
||||
|
||||
def _create_container(self, *args):
|
||||
"""Recursively create all container objects."""
|
||||
if self.mo.container:
|
||||
container = ManagedObjectAccess(self.session, self.mo.container)
|
||||
if container.mo.can_create:
|
||||
container_args = args[0: container.mo.arg_count]
|
||||
container._create_container(*container_args)
|
||||
container.session.post_mo(container.mo, *container_args)
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
self._create_container(*args)
|
||||
if self.mo.can_create and 'status' not in kwargs:
|
||||
kwargs['status'] = 'created'
|
||||
return self.session.post_mo(self.mo, *args, **kwargs)
|
||||
|
||||
def _mo_attributes(self, obj_data):
|
||||
if (self.mo.klass_name in obj_data and
|
||||
'attributes' in obj_data[self.mo.klass_name]):
|
||||
return obj_data[self.mo.klass_name]['attributes']
|
||||
|
||||
def get(self, *args):
|
||||
"""Return a dict of the MO's attributes, or None."""
|
||||
imdata = self.session.get_mo(self.mo, *args)
|
||||
if imdata:
|
||||
return self._mo_attributes(imdata[0])
|
||||
|
||||
def list_all(self):
|
||||
imdata = self.session.list_mo(self.mo)
|
||||
return filter(None, [self._mo_attributes(obj) for obj in imdata])
|
||||
|
||||
def list_names(self):
|
||||
return [obj['name'] for obj in self.list_all()]
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
return self.session.post_mo(self.mo, *args, **kwargs)
|
||||
|
||||
def delete(self, *args):
|
||||
return self.session.post_mo(self.mo, *args, status='deleted')
|
||||
|
||||
|
||||
class RestClient(ApicSession):
|
||||
|
||||
"""APIC REST client for OpenStack Neutron."""
|
||||
|
||||
def __init__(self, host, port=80, usr=None, pwd=None, ssl=False):
|
||||
"""Establish a session with the APIC."""
|
||||
super(RestClient, self).__init__(host, port, usr, pwd, ssl)
|
||||
|
||||
def __getattr__(self, mo_class):
|
||||
"""Add supported MOs as properties on demand."""
|
||||
if mo_class not in ManagedObjectClass.supported_mos:
|
||||
raise cexc.ApicManagedObjectNotSupported(mo_class=mo_class)
|
||||
self.__dict__[mo_class] = ManagedObjectAccess(self, mo_class)
|
||||
return self.__dict__[mo_class]
|
177
neutron/plugins/ml2/drivers/cisco/apic/apic_model.py
Normal file
177
neutron/plugins/ml2/drivers/cisco/apic/apic_model.py
Normal file
@ -0,0 +1,177 @@
|
||||
# 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 sqlalchemy as sa
|
||||
|
||||
from neutron.db import api as db_api
|
||||
from neutron.db import model_base
|
||||
from neutron.db import models_v2
|
||||
|
||||
|
||||
class NetworkEPG(model_base.BASEV2):
|
||||
|
||||
"""EPG's created on the apic per network."""
|
||||
|
||||
__tablename__ = 'cisco_ml2_apic_epgs'
|
||||
|
||||
network_id = sa.Column(sa.String(255), nullable=False, primary_key=True)
|
||||
epg_id = sa.Column(sa.String(64), nullable=False)
|
||||
segmentation_id = sa.Column(sa.String(64), nullable=False)
|
||||
provider = sa.Column(sa.Boolean, default=False, nullable=False)
|
||||
|
||||
|
||||
class PortProfile(model_base.BASEV2):
|
||||
|
||||
"""Port profiles created on the APIC."""
|
||||
|
||||
__tablename__ = 'cisco_ml2_apic_port_profiles'
|
||||
|
||||
node_id = sa.Column(sa.String(255), nullable=False, primary_key=True)
|
||||
profile_id = sa.Column(sa.String(64), nullable=False)
|
||||
hpselc_id = sa.Column(sa.String(64), nullable=False)
|
||||
module = sa.Column(sa.String(10), nullable=False)
|
||||
from_port = sa.Column(sa.Integer(), nullable=False)
|
||||
to_port = sa.Column(sa.Integer(), nullable=False)
|
||||
|
||||
|
||||
class TenantContract(model_base.BASEV2, models_v2.HasTenant):
|
||||
|
||||
"""Contracts (and Filters) created on the APIC."""
|
||||
|
||||
__tablename__ = 'cisco_ml2_apic_contracts'
|
||||
|
||||
__table_args__ = (sa.PrimaryKeyConstraint('tenant_id'),)
|
||||
contract_id = sa.Column(sa.String(64), nullable=False)
|
||||
filter_id = sa.Column(sa.String(64), nullable=False)
|
||||
|
||||
|
||||
class ApicDbModel(object):
|
||||
|
||||
"""DB Model to manage all APIC DB interactions."""
|
||||
|
||||
def __init__(self):
|
||||
self.session = db_api.get_session()
|
||||
|
||||
def get_port_profile_for_node(self, node_id):
|
||||
"""Returns a port profile for a switch if found in the DB."""
|
||||
return self.session.query(PortProfile).filter_by(
|
||||
node_id=node_id).first()
|
||||
|
||||
def get_profile_for_module_and_ports(self, node_id, profile_id,
|
||||
module, from_port, to_port):
|
||||
"""Returns profile for module and ports.
|
||||
|
||||
Grabs the profile row from the DB for the specified switch,
|
||||
module (linecard) and from/to port combination.
|
||||
"""
|
||||
return self.session.query(PortProfile).filter_by(
|
||||
node_id=node_id,
|
||||
module=module,
|
||||
profile_id=profile_id,
|
||||
from_port=from_port,
|
||||
to_port=to_port).first()
|
||||
|
||||
def get_profile_for_module(self, node_id, profile_id, module):
|
||||
"""Returns the first profile for a switch module from the DB."""
|
||||
return self.session.query(PortProfile).filter_by(
|
||||
node_id=node_id,
|
||||
profile_id=profile_id,
|
||||
module=module).first()
|
||||
|
||||
def add_profile_for_module_and_ports(self, node_id, profile_id,
|
||||
hpselc_id, module,
|
||||
from_port, to_port):
|
||||
"""Adds a profile for switch, module and port range."""
|
||||
row = PortProfile(node_id=node_id, profile_id=profile_id,
|
||||
hpselc_id=hpselc_id, module=module,
|
||||
from_port=from_port, to_port=to_port)
|
||||
self.session.add(row)
|
||||
self.session.flush()
|
||||
|
||||
def get_provider_contract(self):
|
||||
"""Returns provider EPG from the DB if found."""
|
||||
return self.session.query(NetworkEPG).filter_by(
|
||||
provider=True).first()
|
||||
|
||||
def set_provider_contract(self, epg_id):
|
||||
"""Sets an EPG to be a contract provider."""
|
||||
epg = self.session.query(NetworkEPG).filter_by(
|
||||
epg_id=epg_id).first()
|
||||
if epg:
|
||||
epg.provider = True
|
||||
self.session.merge(epg)
|
||||
self.session.flush()
|
||||
|
||||
def unset_provider_contract(self, epg_id):
|
||||
"""Sets an EPG to be a contract consumer."""
|
||||
epg = self.session.query(NetworkEPG).filter_by(
|
||||
epg_id=epg_id).first()
|
||||
if epg:
|
||||
epg.provider = False
|
||||
self.session.merge(epg)
|
||||
self.session.flush()
|
||||
|
||||
def get_an_epg(self, exception):
|
||||
"""Returns an EPG from the DB that does not match the id specified."""
|
||||
return self.session.query(NetworkEPG).filter(
|
||||
NetworkEPG.epg_id != exception).first()
|
||||
|
||||
def get_epg_for_network(self, network_id):
|
||||
"""Returns an EPG for a give neutron network."""
|
||||
return self.session.query(NetworkEPG).filter_by(
|
||||
network_id=network_id).first()
|
||||
|
||||
def write_epg_for_network(self, network_id, epg_uid, segmentation_id='1'):
|
||||
"""Stores EPG details for a network.
|
||||
|
||||
NOTE: Segmentation_id is just a placeholder currently, it will be
|
||||
populated with a proper segment id once segmentation mgmt is
|
||||
moved to the APIC.
|
||||
"""
|
||||
epg = NetworkEPG(network_id=network_id, epg_id=epg_uid,
|
||||
segmentation_id=segmentation_id)
|
||||
self.session.add(epg)
|
||||
self.session.flush()
|
||||
return epg
|
||||
|
||||
def delete_epg(self, epg):
|
||||
"""Deletes an EPG from the DB."""
|
||||
self.session.delete(epg)
|
||||
self.session.flush()
|
||||
|
||||
def get_contract_for_tenant(self, tenant_id):
|
||||
"""Returns the specified tenant's contract."""
|
||||
return self.session.query(TenantContract).filter_by(
|
||||
tenant_id=tenant_id).first()
|
||||
|
||||
def write_contract_for_tenant(self, tenant_id, contract_id, filter_id):
|
||||
"""Stores a new contract for the given tenant."""
|
||||
contract = TenantContract(tenant_id=tenant_id,
|
||||
contract_id=contract_id,
|
||||
filter_id=filter_id)
|
||||
self.session.add(contract)
|
||||
self.session.flush()
|
||||
|
||||
return contract
|
||||
|
||||
def delete_profile_for_node(self, node_id):
|
||||
"""Deletes the port profile for a node."""
|
||||
profile = self.session.query(PortProfile).filter_by(
|
||||
node_id=node_id).first()
|
||||
if profile:
|
||||
self.session.delete(profile)
|
||||
self.session.flush()
|
82
neutron/plugins/ml2/drivers/cisco/apic/config.py
Normal file
82
neutron/plugins/ml2/drivers/cisco/apic/config.py
Normal file
@ -0,0 +1,82 @@
|
||||
# Copyright (c) 2014 OpenStack Foundation
|
||||
# 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.
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
|
||||
apic_opts = [
|
||||
cfg.StrOpt('apic_host',
|
||||
help=_("Host name or IP Address of the APIC controller")),
|
||||
cfg.StrOpt('apic_username',
|
||||
help=_("Username for the APIC controller")),
|
||||
cfg.StrOpt('apic_password',
|
||||
help=_("Password for the APIC controller"), secret=True),
|
||||
cfg.StrOpt('apic_port',
|
||||
help=_("Communication port for the APIC controller")),
|
||||
cfg.StrOpt('apic_vmm_provider', default='VMware',
|
||||
help=_("Name for the VMM domain provider")),
|
||||
cfg.StrOpt('apic_vmm_domain', default='openstack',
|
||||
help=_("Name for the VMM domain to be created for Openstack")),
|
||||
cfg.StrOpt('apic_vlan_ns_name', default='openstack_ns',
|
||||
help=_("Name for the vlan namespace to be used for openstack")),
|
||||
cfg.StrOpt('apic_vlan_range', default='2:4093',
|
||||
help=_("Range of VLAN's to be used for Openstack")),
|
||||
cfg.StrOpt('apic_node_profile', default='openstack_profile',
|
||||
help=_("Name of the node profile to be created")),
|
||||
cfg.StrOpt('apic_entity_profile', default='openstack_entity',
|
||||
help=_("Name of the entity profile to be created")),
|
||||
cfg.StrOpt('apic_function_profile', default='openstack_function',
|
||||
help=_("Name of the function profile to be created")),
|
||||
cfg.BoolOpt('apic_clear_node_profiles', default=False,
|
||||
help=_("Clear the node profiles on the APIC at startup "
|
||||
"(mainly used for testing)")),
|
||||
]
|
||||
|
||||
|
||||
cfg.CONF.register_opts(apic_opts, "ml2_cisco_apic")
|
||||
|
||||
|
||||
def get_switch_and_port_for_host(host_id):
|
||||
for switch, connected in _switch_dict.items():
|
||||
for port, hosts in connected.items():
|
||||
if host_id in hosts:
|
||||
return switch, port
|
||||
|
||||
|
||||
_switch_dict = {}
|
||||
|
||||
|
||||
def create_switch_dictionary():
|
||||
multi_parser = cfg.MultiConfigParser()
|
||||
read_ok = multi_parser.read(cfg.CONF.config_file)
|
||||
|
||||
if len(read_ok) != len(cfg.CONF.config_file):
|
||||
raise cfg.Error(_("Some config files were not parsed properly"))
|
||||
|
||||
for parsed_file in multi_parser.parsed:
|
||||
for parsed_item in parsed_file.keys():
|
||||
if parsed_item.startswith('apic_switch'):
|
||||
switch, switch_id = parsed_item.split(':')
|
||||
if switch.lower() == 'apic_switch':
|
||||
_switch_dict[switch_id] = {}
|
||||
port_cfg = parsed_file[parsed_item].items()
|
||||
for host_list, port in port_cfg:
|
||||
hosts = host_list.split(',')
|
||||
port = port[0]
|
||||
_switch_dict[switch_id][port] = hosts
|
||||
|
||||
return _switch_dict
|
52
neutron/plugins/ml2/drivers/cisco/apic/exceptions.py
Normal file
52
neutron/plugins/ml2/drivers/cisco/apic/exceptions.py
Normal file
@ -0,0 +1,52 @@
|
||||
# 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
|
||||
|
||||
"""Exceptions used by Cisco APIC ML2 mechanism driver."""
|
||||
|
||||
from neutron.common import exceptions
|
||||
|
||||
|
||||
class ApicHostNoResponse(exceptions.NotFound):
|
||||
"""No response from the APIC via the specified URL."""
|
||||
message = _("No response from APIC at %(url)s")
|
||||
|
||||
|
||||
class ApicResponseNotOk(exceptions.NeutronException):
|
||||
"""A response from the APIC was not HTTP OK."""
|
||||
message = _("APIC responded with HTTP status %(status)s: %(reason)s, "
|
||||
"Request: '%(request)s', "
|
||||
"APIC error code %(err_code)s: %(err_text)s")
|
||||
|
||||
|
||||
class ApicResponseNoCookie(exceptions.NeutronException):
|
||||
"""A response from the APIC did not contain an expected cookie."""
|
||||
message = _("APIC failed to provide cookie for %(request)s request")
|
||||
|
||||
|
||||
class ApicSessionNotLoggedIn(exceptions.NotAuthorized):
|
||||
"""Attempted APIC operation while not logged in to APIC."""
|
||||
message = _("Authorized APIC session not established")
|
||||
|
||||
|
||||
class ApicHostNotConfigured(exceptions.NotAuthorized):
|
||||
"""The switch and port for the specified host are not configured."""
|
||||
message = _("The switch and port for host '%(host)s' are not configured")
|
||||
|
||||
|
||||
class ApicManagedObjectNotSupported(exceptions.NeutronException):
|
||||
"""Attempted to use an unsupported Managed Object."""
|
||||
message = _("Managed Object '%(mo_class)s' is not supported")
|
@ -0,0 +1,272 @@
|
||||
# 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
|
||||
import requests
|
||||
import requests.exceptions
|
||||
|
||||
from neutron.plugins.ml2.drivers.cisco.apic import apic_client as apic
|
||||
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 TestCiscoApicClient(base.BaseTestCase, mocked.ControllerMixin):
|
||||
|
||||
def setUp(self):
|
||||
super(TestCiscoApicClient, self).setUp()
|
||||
self.set_up_mocks()
|
||||
self.apic = apic.RestClient(mocked.APIC_HOST)
|
||||
self.addCleanup(mock.patch.stopall)
|
||||
|
||||
def _mock_authenticate(self, timeout=300):
|
||||
self.reset_reponses()
|
||||
self.mock_apic_manager_login_responses(timeout=timeout)
|
||||
self.apic.login(mocked.APIC_USR, mocked.APIC_PWD)
|
||||
|
||||
def test_login_by_instantiation(self):
|
||||
self.reset_reponses()
|
||||
self.mock_apic_manager_login_responses()
|
||||
apic2 = apic.RestClient(mocked.APIC_HOST,
|
||||
usr=mocked.APIC_USR, pwd=mocked.APIC_PWD)
|
||||
self.assertIsNotNone(apic2.authentication)
|
||||
self.assertEqual(apic2.username, mocked.APIC_USR)
|
||||
|
||||
def test_client_session_login_ok(self):
|
||||
self._mock_authenticate()
|
||||
self.assertEqual(
|
||||
self.apic.authentication['userName'], mocked.APIC_USR)
|
||||
self.assertTrue(self.apic.api_base.startswith('http://'))
|
||||
self.assertEqual(self.apic.username, mocked.APIC_USR)
|
||||
self.assertIsNotNone(self.apic.authentication)
|
||||
self.apic = apic.RestClient(mocked.APIC_HOST, mocked.APIC_PORT,
|
||||
ssl=True)
|
||||
self.assertTrue(self.apic.api_base.startswith('https://'))
|
||||
|
||||
def test_client_session_login_fail(self):
|
||||
self.mock_error_post_response(requests.codes.unauthorized,
|
||||
code='599',
|
||||
text=u'Fake error')
|
||||
self.assertRaises(cexc.ApicResponseNotOk, self.apic.login,
|
||||
mocked.APIC_USR, mocked.APIC_PWD)
|
||||
|
||||
def test_client_session_login_timeout(self):
|
||||
self.response['post'].append(requests.exceptions.Timeout)
|
||||
self.assertRaises(cexc.ApicHostNoResponse, self.apic.login,
|
||||
mocked.APIC_USR, mocked.APIC_PWD)
|
||||
|
||||
def test_client_session_logout_ok(self):
|
||||
self.mock_response_for_post('aaaLogout')
|
||||
self.apic.logout()
|
||||
self.assertIsNone(self.apic.authentication)
|
||||
# Multiple signouts should not cause an error
|
||||
self.apic.logout()
|
||||
self.assertIsNone(self.apic.authentication)
|
||||
|
||||
def test_client_session_logout_fail(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_error_post_response(requests.codes.timeout,
|
||||
code='123', text='failed')
|
||||
self.assertRaises(cexc.ApicResponseNotOk, self.apic.logout)
|
||||
|
||||
def test_query_not_logged_in(self):
|
||||
self.apic.authentication = None
|
||||
self.assertRaises(cexc.ApicSessionNotLoggedIn,
|
||||
self.apic.fvTenant.get, mocked.APIC_TENANT)
|
||||
|
||||
def test_query_no_response(self):
|
||||
self._mock_authenticate()
|
||||
requests.Session.get = mock.Mock(return_value=None)
|
||||
self.assertRaises(cexc.ApicHostNoResponse,
|
||||
self.apic.fvTenant.get, mocked.APIC_TENANT)
|
||||
|
||||
def test_query_error_response_no_data(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_error_get_response(requests.codes.bad) # No error attrs.
|
||||
self.assertRaises(cexc.ApicResponseNotOk,
|
||||
self.apic.fvTenant.get, mocked.APIC_TENANT)
|
||||
|
||||
def test_generic_get_data(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_get('topSystem', name='ifc1')
|
||||
top_system = self.apic.get_data('class/topSystem')
|
||||
self.assertIsNotNone(top_system)
|
||||
name = top_system[0]['topSystem']['attributes']['name']
|
||||
self.assertEqual(name, 'ifc1')
|
||||
|
||||
def test_session_timeout_refresh_ok(self):
|
||||
self._mock_authenticate(timeout=-1)
|
||||
# Client will do refresh before getting tenant
|
||||
self.mock_response_for_get('aaaLogin', token='ok',
|
||||
refreshTimeoutSeconds=300)
|
||||
self.mock_response_for_get('fvTenant', name=mocked.APIC_TENANT)
|
||||
tenant = self.apic.fvTenant.get(mocked.APIC_TENANT)
|
||||
self.assertEqual(tenant['name'], mocked.APIC_TENANT)
|
||||
|
||||
def test_session_timeout_refresh_no_cookie(self):
|
||||
self._mock_authenticate(timeout=-1)
|
||||
# Client will do refresh before getting tenant
|
||||
self.mock_response_for_get('aaaLogin', notoken='test')
|
||||
self.assertRaises(cexc.ApicResponseNoCookie,
|
||||
self.apic.fvTenant.get, mocked.APIC_TENANT)
|
||||
|
||||
def test_session_timeout_refresh_error(self):
|
||||
self._mock_authenticate(timeout=-1)
|
||||
self.mock_error_get_response(requests.codes.timeout,
|
||||
code='503', text=u'timed out')
|
||||
self.assertRaises(cexc.ApicResponseNotOk,
|
||||
self.apic.fvTenant.get, mocked.APIC_TENANT)
|
||||
|
||||
def test_session_timeout_refresh_timeout_error(self):
|
||||
self._mock_authenticate(timeout=-1)
|
||||
# Client will try to get refresh, we fake a refresh error.
|
||||
self.mock_error_get_response(requests.codes.bad_request,
|
||||
code='403',
|
||||
text=u'Token was invalid. Expired.')
|
||||
# Client will then try to re-login.
|
||||
self.mock_apic_manager_login_responses()
|
||||
# Finally the client will try to get the tenant.
|
||||
self.mock_response_for_get('fvTenant', name=mocked.APIC_TENANT)
|
||||
tenant = self.apic.fvTenant.get(mocked.APIC_TENANT)
|
||||
self.assertEqual(tenant['name'], mocked.APIC_TENANT)
|
||||
|
||||
def test_lookup_mo_bad_token_retry(self):
|
||||
self._mock_authenticate()
|
||||
# For the first get request we mock a bad token.
|
||||
self.mock_error_get_response(requests.codes.bad_request,
|
||||
code='403',
|
||||
text=u'Token was invalid. Expired.')
|
||||
# Client will then try to re-login.
|
||||
self.mock_apic_manager_login_responses()
|
||||
# Then the client will retry to get the tenant.
|
||||
self.mock_response_for_get('fvTenant', name=mocked.APIC_TENANT)
|
||||
tenant = self.apic.fvTenant.get(mocked.APIC_TENANT)
|
||||
self.assertEqual(tenant['name'], mocked.APIC_TENANT)
|
||||
|
||||
def test_use_unsupported_managed_object(self):
|
||||
self._mock_authenticate()
|
||||
# unittest.assertRaises cannot catch exceptions raised in
|
||||
# __getattr__, so we need to defer the evaluation using lambda.
|
||||
self.assertRaises(cexc.ApicManagedObjectNotSupported,
|
||||
lambda: self.apic.nonexistentObject)
|
||||
|
||||
def test_lookup_nonexistant_mo(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_get('fvTenant')
|
||||
self.assertIsNone(self.apic.fvTenant.get(mocked.APIC_TENANT))
|
||||
|
||||
def test_lookup_existing_mo(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_get('fvTenant', name='infra')
|
||||
tenant = self.apic.fvTenant.get('infra')
|
||||
self.assertEqual(tenant['name'], 'infra')
|
||||
|
||||
def test_list_mos_ok(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_get('fvTenant', name='t1')
|
||||
self.mock_append_to_response('fvTenant', name='t2')
|
||||
tlist = self.apic.fvTenant.list_all()
|
||||
self.assertIsNotNone(tlist)
|
||||
self.assertEqual(len(tlist), 2)
|
||||
self.assertIn({'name': 't1'}, tlist)
|
||||
self.assertIn({'name': 't2'}, tlist)
|
||||
|
||||
def test_list_mo_names_ok(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_get('fvTenant', name='t1')
|
||||
self.mock_append_to_response('fvTenant', name='t2')
|
||||
tnlist = self.apic.fvTenant.list_names()
|
||||
self.assertIsNotNone(tnlist)
|
||||
self.assertEqual(len(tnlist), 2)
|
||||
self.assertIn('t1', tnlist)
|
||||
self.assertIn('t2', tnlist)
|
||||
|
||||
def test_list_mos_split_class_fail(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_get('fvnsEncapBlk', name='Blk1')
|
||||
encap_blks = self.apic.fvnsEncapBlk__vlan.list_all()
|
||||
self.assertEqual(len(encap_blks), 1)
|
||||
|
||||
def test_delete_mo_ok(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_post('fvTenant')
|
||||
self.assertTrue(self.apic.fvTenant.delete(mocked.APIC_TENANT))
|
||||
|
||||
def test_create_mo_ok(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_post('fvTenant', name=mocked.APIC_TENANT)
|
||||
self.mock_response_for_get('fvTenant', name=mocked.APIC_TENANT)
|
||||
self.apic.fvTenant.create(mocked.APIC_TENANT)
|
||||
tenant = self.apic.fvTenant.get(mocked.APIC_TENANT)
|
||||
self.assertEqual(tenant['name'], mocked.APIC_TENANT)
|
||||
|
||||
def test_create_mo_already_exists(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_error_post_response(requests.codes.bad_request,
|
||||
code='103',
|
||||
text=u'Fake 103 error')
|
||||
self.assertRaises(cexc.ApicResponseNotOk,
|
||||
self.apic.vmmProvP.create, mocked.APIC_VMMP)
|
||||
|
||||
def test_create_mo_with_prereq(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_post('fvTenant', name=mocked.APIC_TENANT)
|
||||
self.mock_response_for_post('fvBD', name=mocked.APIC_NETWORK)
|
||||
self.mock_response_for_get('fvBD', name=mocked.APIC_NETWORK)
|
||||
bd_args = mocked.APIC_TENANT, mocked.APIC_NETWORK
|
||||
self.apic.fvBD.create(*bd_args)
|
||||
network = self.apic.fvBD.get(*bd_args)
|
||||
self.assertEqual(network['name'], mocked.APIC_NETWORK)
|
||||
|
||||
def test_create_mo_prereq_exists(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_post('vmmDomP', name=mocked.APIC_DOMAIN)
|
||||
self.mock_response_for_get('vmmDomP', name=mocked.APIC_DOMAIN)
|
||||
self.apic.vmmDomP.create(mocked.APIC_VMMP, mocked.APIC_DOMAIN)
|
||||
dom = self.apic.vmmDomP.get(mocked.APIC_VMMP, mocked.APIC_DOMAIN)
|
||||
self.assertEqual(dom['name'], mocked.APIC_DOMAIN)
|
||||
|
||||
def test_create_mo_fails(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_post('fvTenant', name=mocked.APIC_TENANT)
|
||||
self.mock_error_post_response(requests.codes.bad_request,
|
||||
code='not103',
|
||||
text=u'Fake not103 error')
|
||||
bd_args = mocked.APIC_TENANT, mocked.APIC_NETWORK
|
||||
self.assertRaises(cexc.ApicResponseNotOk,
|
||||
self.apic.fvBD.create, *bd_args)
|
||||
|
||||
def test_update_mo(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_post('fvTenant', name=mocked.APIC_TENANT)
|
||||
self.mock_response_for_get('fvTenant', name=mocked.APIC_TENANT,
|
||||
more='extra')
|
||||
self.apic.fvTenant.update(mocked.APIC_TENANT, more='extra')
|
||||
tenant = self.apic.fvTenant.get(mocked.APIC_TENANT)
|
||||
self.assertEqual(tenant['name'], mocked.APIC_TENANT)
|
||||
self.assertEqual(tenant['more'], 'extra')
|
||||
|
||||
def test_attr_fail_empty_list(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_get('fvTenant') # No attrs for tenant.
|
||||
self.assertIsNone(self.apic.fvTenant.get(mocked.APIC_TENANT))
|
||||
|
||||
def test_attr_fail_other_obj(self):
|
||||
self._mock_authenticate()
|
||||
self.mock_response_for_get('other', name=mocked.APIC_TENANT)
|
||||
self.assertIsNone(self.apic.fvTenant.get(mocked.APIC_TENANT))
|
@ -0,0 +1,225 @@
|
||||
# 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
|
||||
import requests
|
||||
|
||||
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
|
||||
|
||||
|
||||
OK = requests.codes.ok
|
||||
|
||||
APIC_HOST = 'fake.controller.local'
|
||||
APIC_PORT = 7580
|
||||
APIC_USR = 'notadmin'
|
||||
APIC_PWD = 'topsecret'
|
||||
|
||||
APIC_TENANT = 'citizen14'
|
||||
APIC_NETWORK = 'network99'
|
||||
APIC_NETNAME = 'net99name'
|
||||
APIC_SUBNET = '10.3.2.1/24'
|
||||
APIC_L3CTX = 'layer3context'
|
||||
APIC_AP = 'appProfile001'
|
||||
APIC_EPG = 'endPointGroup001'
|
||||
|
||||
APIC_CONTRACT = 'signedContract'
|
||||
APIC_SUBJECT = 'testSubject'
|
||||
APIC_FILTER = 'carbonFilter'
|
||||
APIC_ENTRY = 'forcedEntry'
|
||||
|
||||
APIC_VMMP = 'OpenStack'
|
||||
APIC_DOMAIN = 'cumuloNimbus'
|
||||
APIC_PDOM = 'rainStorm'
|
||||
|
||||
APIC_NODE_PROF = 'red'
|
||||
APIC_LEAF = 'green'
|
||||
APIC_LEAF_TYPE = 'range'
|
||||
APIC_NODE_BLK = 'blue'
|
||||
APIC_PORT_PROF = 'yellow'
|
||||
APIC_PORT_SEL = 'front'
|
||||
APIC_PORT_TYPE = 'range'
|
||||
APIC_PORT_BLK1 = 'block01'
|
||||
APIC_PORT_BLK2 = 'block02'
|
||||
APIC_ACC_PORT_GRP = 'alpha'
|
||||
APIC_FUNC_PROF = 'beta'
|
||||
APIC_ATT_ENT_PROF = 'delta'
|
||||
APIC_VLAN_NAME = 'gamma'
|
||||
APIC_VLAN_MODE = 'dynamic'
|
||||
APIC_VLANID_FROM = 2900
|
||||
APIC_VLANID_TO = 2999
|
||||
APIC_VLAN_FROM = 'vlan-%d' % APIC_VLANID_FROM
|
||||
APIC_VLAN_TO = 'vlan-%d' % APIC_VLANID_TO
|
||||
|
||||
|
||||
class ControllerMixin(object):
|
||||
|
||||
"""Mock the controller for APIC driver and service unit tests."""
|
||||
|
||||
def __init__(self):
|
||||
self.response = None
|
||||
|
||||
def set_up_mocks(self):
|
||||
# The mocked responses from the server are lists used by
|
||||
# mock.side_effect, which means each call to post or get will
|
||||
# return the next item in the list. This allows the test cases
|
||||
# to stage a sequence of responses to method(s) under test.
|
||||
self.response = {'post': [], 'get': []}
|
||||
self.reset_reponses()
|
||||
|
||||
def reset_reponses(self, req=None):
|
||||
# Clear all staged responses.
|
||||
reqs = req and [req] or ['post', 'get'] # Both if none specified.
|
||||
for req in reqs:
|
||||
del self.response[req][:]
|
||||
self.restart_responses(req)
|
||||
|
||||
def restart_responses(self, req):
|
||||
responses = mock.MagicMock(side_effect=self.response[req])
|
||||
if req == 'post':
|
||||
requests.Session.post = responses
|
||||
elif req == 'get':
|
||||
requests.Session.get = responses
|
||||
|
||||
def mock_response_for_post(self, mo, **attrs):
|
||||
attrs['debug_mo'] = mo # useful for debugging
|
||||
self._stage_mocked_response('post', OK, mo, **attrs)
|
||||
|
||||
def mock_response_for_get(self, mo, **attrs):
|
||||
self._stage_mocked_response('get', OK, mo, **attrs)
|
||||
|
||||
def mock_append_to_response(self, mo, **attrs):
|
||||
# Append a MO to the last get response.
|
||||
mo_attrs = attrs and {mo: {'attributes': attrs}} or {}
|
||||
self.response['get'][-1].json.return_value['imdata'].append(mo_attrs)
|
||||
|
||||
def mock_error_post_response(self, status, **attrs):
|
||||
self._stage_mocked_response('post', status, 'error', **attrs)
|
||||
|
||||
def mock_error_get_response(self, status, **attrs):
|
||||
self._stage_mocked_response('get', status, 'error', **attrs)
|
||||
|
||||
def _stage_mocked_response(self, req, mock_status, mo, **attrs):
|
||||
response = mock.MagicMock()
|
||||
response.status_code = mock_status
|
||||
mo_attrs = attrs and [{mo: {'attributes': attrs}}] or []
|
||||
response.json.return_value = {'imdata': mo_attrs}
|
||||
self.response[req].append(response)
|
||||
|
||||
def mock_responses_for_create(self, obj):
|
||||
self._mock_container_responses_for_create(
|
||||
apic.ManagedObjectClass(obj).container)
|
||||
name = '-'.join([obj, 'name']) # useful for debugging
|
||||
self._stage_mocked_response('post', OK, obj, name=name)
|
||||
|
||||
def _mock_container_responses_for_create(self, obj):
|
||||
# Recursively generate responses for creating obj's containers.
|
||||
if obj:
|
||||
mo = apic.ManagedObjectClass(obj)
|
||||
if mo.can_create:
|
||||
if mo.container:
|
||||
self._mock_container_responses_for_create(mo.container)
|
||||
name = '-'.join([obj, 'name']) # useful for debugging
|
||||
self._stage_mocked_response('post', OK, obj, debug_name=name)
|
||||
|
||||
def mock_apic_manager_login_responses(self, timeout=300):
|
||||
# APIC Manager tests are based on authenticated session
|
||||
self.mock_response_for_post('aaaLogin', userName=APIC_USR,
|
||||
token='ok', refreshTimeoutSeconds=timeout)
|
||||
|
||||
def assert_responses_drained(self, req=None):
|
||||
"""Fail if all the expected responses have not been consumed."""
|
||||
request = {'post': self.session.post, 'get': self.session.get}
|
||||
reqs = req and [req] or ['post', 'get'] # Both if none specified.
|
||||
for req in reqs:
|
||||
try:
|
||||
request[req]('some url')
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
# User-friendly error message
|
||||
msg = req + ' response queue not drained'
|
||||
self.fail(msg=msg)
|
||||
|
||||
|
||||
class ConfigMixin(object):
|
||||
|
||||
"""Mock the config for APIC driver and service unit tests."""
|
||||
|
||||
def __init__(self):
|
||||
self.mocked_parser = None
|
||||
|
||||
def set_up_mocks(self):
|
||||
# Mock the configuration file
|
||||
args = ['--config-file', test_api_v2.etcdir('neutron.conf.test')]
|
||||
neutron_config.parse(args=args)
|
||||
|
||||
# Configure the ML2 mechanism drivers and network types
|
||||
ml2_opts = {
|
||||
'mechanism_drivers': ['apic'],
|
||||
'tenant_network_types': ['vlan'],
|
||||
}
|
||||
for opt, val in ml2_opts.items():
|
||||
ml2_config.cfg.CONF.set_override(opt, val, 'ml2')
|
||||
|
||||
# Configure the Cisco APIC mechanism driver
|
||||
apic_test_config = {
|
||||
'apic_host': APIC_HOST,
|
||||
'apic_username': APIC_USR,
|
||||
'apic_password': APIC_PWD,
|
||||
'apic_port': APIC_PORT,
|
||||
'apic_vmm_domain': APIC_DOMAIN,
|
||||
'apic_vlan_ns_name': APIC_VLAN_NAME,
|
||||
'apic_vlan_range': '%d:%d' % (APIC_VLANID_FROM, APIC_VLANID_TO),
|
||||
'apic_node_profile': APIC_NODE_PROF,
|
||||
'apic_entity_profile': APIC_ATT_ENT_PROF,
|
||||
'apic_function_profile': APIC_FUNC_PROF,
|
||||
}
|
||||
for opt, val in apic_test_config.items():
|
||||
cfg.CONF.set_override(opt, val, 'ml2_cisco_apic')
|
||||
|
||||
apic_switch_cfg = {
|
||||
'apic_switch:east01': {'ubuntu1,ubuntu2': ['3/11']},
|
||||
'apic_switch:east02': {'rhel01,rhel02': ['4/21'],
|
||||
'rhel03': ['4/22']},
|
||||
}
|
||||
self.mocked_parser = mock.patch.object(cfg,
|
||||
'MultiConfigParser').start()
|
||||
self.mocked_parser.return_value.read.return_value = [apic_switch_cfg]
|
||||
self.mocked_parser.return_value.parsed = [apic_switch_cfg]
|
||||
|
||||
|
||||
class DbModelMixin(object):
|
||||
|
||||
"""Mock the DB models for the APIC driver and service unit tests."""
|
||||
|
||||
def __init__(self):
|
||||
self.mocked_session = None
|
||||
|
||||
def set_up_mocks(self):
|
||||
self.mocked_session = mock.Mock()
|
||||
get_session = mock.patch('neutron.db.api.get_session').start()
|
||||
get_session.return_value = self.mocked_session
|
||||
|
||||
def mock_db_query_filterby_first_return(self, value):
|
||||
"""Mock db.session.query().filterby().first() to return value."""
|
||||
query = self.mocked_session.query.return_value
|
||||
query.filter_by.return_value.first.return_value = value
|
Loading…
Reference in New Issue
Block a user