neutron/neutron/plugins/ml2/drivers/cisco/apic/apic_client.py

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
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 jsonutils.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]