
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
417 lines
17 KiB
Python
417 lines
17 KiB
Python
# 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]
|