f2c5f5856b
It adds a new plugin for SDN-VE, the IBM SDN controller. The plugin supports the core API and the port binding and L3 extensions. Implements: blueprint ibm-sdn-ve-plugin DocImpact Change-Id: I92619a95bca2ae0c37e7fdd39da30119b43d1ad6
388 lines
14 KiB
Python
388 lines
14 KiB
Python
# Copyright 2014 IBM Corp.
|
|
#
|
|
# 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: Mohammad Banikazemi, IBM Corp.
|
|
|
|
|
|
import httplib
|
|
import urllib
|
|
|
|
import httplib2
|
|
from keystoneclient.v2_0 import client as keyclient
|
|
from oslo.config import cfg
|
|
|
|
from neutron.api.v2 import attributes
|
|
from neutron.openstack.common import log as logging
|
|
from neutron.plugins.ibm.common import config # noqa
|
|
from neutron.plugins.ibm.common import constants
|
|
from neutron.wsgi import Serializer
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
SDNVE_VERSION = '2.0'
|
|
SDNVE_ACTION_PREFIX = '/sdnve'
|
|
SDNVE_RETRIES = 0
|
|
SDNVE_RETRIY_INTERVAL = 1
|
|
SDNVE_TENANT_TYPE_OVERLAY = u'DOVE'
|
|
SDNVE_URL = 'https://%s:%s%s'
|
|
|
|
|
|
class RequestHandler(object):
|
|
'''Handles processeing requests to and responses from controller.'''
|
|
|
|
def __init__(self, controller_ips=None, port=None, ssl=None,
|
|
base_url=None, userid=None, password=None,
|
|
timeout=10, formats=None):
|
|
'''Initializes the RequestHandler for communication with controller
|
|
|
|
Following keyword arguments are used; if not specified, default
|
|
values are used.
|
|
:param port: Username for authentication.
|
|
:param timeout: Time out for http requests.
|
|
:param userid: User id for accessing controller.
|
|
:param password: Password for accessing the controlelr.
|
|
:param base_url: The base url for the controller.
|
|
:param controller_ips: List of controller IP addresses.
|
|
:param formats: Supported formats.
|
|
'''
|
|
self.port = port or cfg.CONF.SDNVE.port
|
|
self.timeout = timeout
|
|
self._s_meta = None
|
|
self.connection = None
|
|
self.httpclient = httplib2.Http(
|
|
disable_ssl_certificate_validation=True)
|
|
self.cookie = None
|
|
|
|
userid = userid or cfg.CONF.SDNVE.userid
|
|
password = password or cfg.CONF.SDNVE.password
|
|
if (userid and password):
|
|
self.httpclient.add_credentials(userid, password)
|
|
|
|
self.base_url = base_url or cfg.CONF.SDNVE.base_url
|
|
self.controller_ips = controller_ips or cfg.CONF.SDNVE.controller_ips
|
|
|
|
LOG.info(_("The IP addr of available SDN-VE controllers: %s"),
|
|
self.controller_ips)
|
|
self.controller_ip = self.controller_ips[0]
|
|
LOG.info(_("The SDN-VE controller IP address: %s"),
|
|
self.controller_ip)
|
|
|
|
self.new_controller = False
|
|
self.format = formats or cfg.CONF.SDNVE.format
|
|
|
|
self.version = SDNVE_VERSION
|
|
self.action_prefix = SDNVE_ACTION_PREFIX
|
|
self.retries = SDNVE_RETRIES
|
|
self.retry_interval = SDNVE_RETRIY_INTERVAL
|
|
|
|
def serialize(self, data):
|
|
'''Serializes a dictionary with a single key.'''
|
|
|
|
if isinstance(data, dict):
|
|
return Serializer().serialize(data, self.content_type())
|
|
elif data:
|
|
raise TypeError(_("unable to serialize object type: '%s'") %
|
|
type(data))
|
|
|
|
def deserialize(self, data, status_code):
|
|
'''Deserializes an xml or json string into a dictionary.'''
|
|
|
|
# NOTE(mb): Temporary fix for backend controller requirement
|
|
data = data.replace("router_external", "router:external")
|
|
|
|
if status_code == httplib.NO_CONTENT:
|
|
return data
|
|
try:
|
|
deserialized_data = Serializer(
|
|
metadata=self._s_meta).deserialize(data, self.content_type())
|
|
deserialized_data = deserialized_data['body']
|
|
except Exception:
|
|
deserialized_data = data
|
|
|
|
return deserialized_data
|
|
|
|
def content_type(self, format=None):
|
|
'''Returns the mime-type for either 'xml' or 'json'.'''
|
|
|
|
return 'application/%s' % (format or self.format)
|
|
|
|
def delete(self, url, body=None, headers=None, params=None):
|
|
return self.do_request("DELETE", url, body=body,
|
|
headers=headers, params=params)
|
|
|
|
def get(self, url, body=None, headers=None, params=None):
|
|
return self.do_request("GET", url, body=body,
|
|
headers=headers, params=params)
|
|
|
|
def post(self, url, body=None, headers=None, params=None):
|
|
return self.do_request("POST", url, body=body,
|
|
headers=headers, params=params)
|
|
|
|
def put(self, url, body=None, headers=None, params=None):
|
|
return self.do_request("PUT", url, body=body,
|
|
headers=headers, params=params)
|
|
|
|
def do_request(self, method, url, body=None, headers=None,
|
|
params=None, connection_type=None):
|
|
|
|
status_code = -1
|
|
replybody_deserialized = ''
|
|
|
|
if body:
|
|
body = self.serialize(body)
|
|
|
|
self.headers = headers or {'Content-Type': self.content_type()}
|
|
if self.cookie:
|
|
self.headers['cookie'] = self.cookie
|
|
|
|
if self.controller_ip != self.controller_ips[0]:
|
|
controllers = [self.controller_ip]
|
|
else:
|
|
controllers = []
|
|
controllers.extend(self.controller_ips)
|
|
|
|
for controller_ip in controllers:
|
|
serverurl = SDNVE_URL % (controller_ip, self.port, self.base_url)
|
|
myurl = serverurl + url
|
|
if params and isinstance(params, dict):
|
|
myurl += '?' + urllib.urlencode(params, doseq=1)
|
|
|
|
try:
|
|
LOG.debug(_("Sending request to SDN-VE. url: "
|
|
"%(myurl)s method: %(method)s body: "
|
|
"%(body)s header: %(header)s "),
|
|
{'myurl': myurl, 'method': method,
|
|
'body': body, 'header': self.headers})
|
|
resp, replybody = self.httpclient.request(
|
|
myurl, method=method, body=body, headers=self.headers)
|
|
LOG.debug(("Response recd from SDN-VE. resp: %(resp)s"
|
|
"body: %(body)s"),
|
|
{'resp': resp.status, 'body': replybody})
|
|
status_code = resp.status
|
|
|
|
except Exception as e:
|
|
LOG.error(_("Error: Could not reach server: %(url)s "
|
|
"Exception: %(excp)s."),
|
|
{'url': myurl, 'excp': e})
|
|
self.cookie = None
|
|
continue
|
|
|
|
if status_code not in constants.HTTP_ACCEPTABLE:
|
|
LOG.debug(_("Error message: %(reply)s -- Status: %(status)s"),
|
|
{'reply': replybody, 'status': status_code})
|
|
else:
|
|
LOG.debug(_("Received response status: %s"), status_code)
|
|
|
|
if resp.get('set-cookie'):
|
|
self.cookie = resp['set-cookie']
|
|
replybody_deserialized = self.deserialize(
|
|
replybody,
|
|
status_code)
|
|
LOG.debug(_("Deserialized body: %s"), replybody_deserialized)
|
|
if controller_ip != self.controller_ip:
|
|
# bcast the change of controller
|
|
self.new_controller = True
|
|
self.controller_ip = controller_ip
|
|
|
|
return (status_code, replybody_deserialized)
|
|
|
|
return (httplib.REQUEST_TIMEOUT, 'Could not reach server(s)')
|
|
|
|
|
|
class Client(RequestHandler):
|
|
'''Client for SDNVE controller.'''
|
|
|
|
def __init__(self):
|
|
'''Initialize a new SDNVE client.'''
|
|
super(Client, self).__init__()
|
|
|
|
self.keystoneclient = KeystoneClient()
|
|
|
|
resource_path = {
|
|
'network': "ln/networks/",
|
|
'subnet': "ln/subnets/",
|
|
'port': "ln/ports/",
|
|
'tenant': "ln/tenants/",
|
|
'router': "ln/routers/",
|
|
'floatingip': "ln/floatingips/",
|
|
}
|
|
|
|
def process_request(self, body):
|
|
'''Processes requests according to requirements of controller.'''
|
|
if self.format == 'json':
|
|
body = dict(
|
|
(k.replace(':', '_'), v) for k, v in body.items()
|
|
if attributes.is_attr_set(v))
|
|
|
|
def sdnve_list(self, resource, **params):
|
|
'''Fetches a list of resources.'''
|
|
|
|
res = self.resource_path.get(resource, None)
|
|
if not res:
|
|
LOG.info(_("Bad resource for forming a list request"))
|
|
return 0, ''
|
|
|
|
return self.get(res, params=params)
|
|
|
|
def sdnve_show(self, resource, specific, **params):
|
|
'''Fetches information of a certain resource.'''
|
|
|
|
res = self.resource_path.get(resource, None)
|
|
if not res:
|
|
LOG.info(_("Bad resource for forming a show request"))
|
|
return 0, ''
|
|
|
|
return self.get(res + specific, params=params)
|
|
|
|
def sdnve_create(self, resource, body):
|
|
'''Creates a new resource.'''
|
|
|
|
res = self.resource_path.get(resource, None)
|
|
if not res:
|
|
LOG.info(_("Bad resource for forming a create request"))
|
|
return 0, ''
|
|
|
|
self.process_request(body)
|
|
status, data = self.post(res, body=body)
|
|
return (status, data)
|
|
|
|
def sdnve_update(self, resource, specific, body=None):
|
|
'''Updates a resource.'''
|
|
|
|
res = self.resource_path.get(resource, None)
|
|
if not res:
|
|
LOG.info(_("Bad resource for forming a update request"))
|
|
return 0, ''
|
|
|
|
self.process_request(body)
|
|
return self.put(res + specific, body=body)
|
|
|
|
def sdnve_delete(self, resource, specific):
|
|
'''Deletes the specified resource.'''
|
|
|
|
res = self.resource_path.get(resource, None)
|
|
if not res:
|
|
LOG.info(_("Bad resource for forming a delete request"))
|
|
return 0, ''
|
|
|
|
return self.delete(res + specific)
|
|
|
|
def _tenant_id_conversion(self, osid):
|
|
return osid
|
|
|
|
def sdnve_get_tenant_byid(self, os_tenant_id):
|
|
sdnve_tenant_id = self._tenant_id_conversion(os_tenant_id)
|
|
resp, content = self.sdnve_show('tenant', sdnve_tenant_id)
|
|
if resp in constants.HTTP_ACCEPTABLE:
|
|
tenant_id = content.get('id')
|
|
tenant_type = content.get('network_type')
|
|
if tenant_type == SDNVE_TENANT_TYPE_OVERLAY:
|
|
tenant_type = constants.TENANT_TYPE_OVERLAY
|
|
return tenant_id, tenant_type
|
|
return None, None
|
|
|
|
def sdnve_check_and_create_tenant(self, os_tenant_id, network_type=None):
|
|
|
|
if not os_tenant_id:
|
|
return
|
|
tenant_id, tenant_type = self.sdnve_get_tenant_byid(os_tenant_id)
|
|
if tenant_id:
|
|
if not network_type:
|
|
return tenant_id
|
|
if tenant_type != network_type:
|
|
LOG.info(_("Non matching tenant and network types: "
|
|
"%(ttype)s %(ntype)s"),
|
|
{'ttype': tenant_type, 'ntype': network_type})
|
|
return
|
|
return tenant_id
|
|
|
|
# Have to create a new tenant
|
|
sdnve_tenant_id = self._tenant_id_conversion(os_tenant_id)
|
|
if not network_type:
|
|
network_type = self.keystoneclient.get_tenant_type(os_tenant_id)
|
|
if network_type == constants.TENANT_TYPE_OVERLAY:
|
|
network_type = SDNVE_TENANT_TYPE_OVERLAY
|
|
|
|
pinn_desc = ("Created by SDN-VE Neutron Plugin, OS project name = " +
|
|
self.keystoneclient.get_tenant_name(os_tenant_id))
|
|
|
|
res, content = self.sdnve_create('tenant',
|
|
{'id': sdnve_tenant_id,
|
|
'name': os_tenant_id,
|
|
'network_type': network_type,
|
|
'description': pinn_desc})
|
|
if res not in constants.HTTP_ACCEPTABLE:
|
|
return
|
|
|
|
return sdnve_tenant_id
|
|
|
|
def sdnve_get_controller(self):
|
|
if self.new_controller:
|
|
self.new_controller = False
|
|
return self.controller_ip
|
|
|
|
|
|
class KeystoneClient(object):
|
|
|
|
def __init__(self, username=None, tenant_name=None, password=None,
|
|
auth_url=None):
|
|
|
|
keystone_conf = cfg.CONF.keystone_authtoken
|
|
keystone_auth_url = ('%s://%s:%s/v2.0/' %
|
|
(keystone_conf.auth_protocol,
|
|
keystone_conf.auth_host,
|
|
keystone_conf.auth_port))
|
|
|
|
username = username or keystone_conf.admin_user
|
|
tenant_name = tenant_name or keystone_conf.admin_tenant_name
|
|
password = password or keystone_conf.admin_password
|
|
auth_url = auth_url or keystone_auth_url
|
|
|
|
self.overlay_signature = cfg.CONF.SDNVE.overlay_signature
|
|
self.of_signature = cfg.CONF.SDNVE.of_signature
|
|
self.default_tenant_type = cfg.CONF.SDNVE.default_tenant_type
|
|
|
|
self.client = keyclient.Client(username=username,
|
|
password=password,
|
|
tenant_name=tenant_name,
|
|
auth_url=auth_url)
|
|
|
|
def get_tenant_byid(self, id):
|
|
|
|
try:
|
|
return self.client.tenants.get(id)
|
|
except Exception:
|
|
LOG.exception(_("Did not find tenant: %r"), id)
|
|
|
|
def get_tenant_type(self, id):
|
|
|
|
tenant = self.get_tenant_byid(id)
|
|
if tenant:
|
|
description = tenant.description
|
|
if description:
|
|
if (description.find(self.overlay_signature) >= 0):
|
|
return constants.TENANT_TYPE_OVERLAY
|
|
if (description.find(self.of_signature) >= 0):
|
|
return constants.TENANT_TYPE_OF
|
|
return self.default_tenant_type
|
|
|
|
def get_tenant_name(self, id):
|
|
|
|
tenant = self.get_tenant_byid(id)
|
|
if tenant:
|
|
return tenant.name
|
|
return 'not found'
|