From 4f824cf4fb9b824a6cd2d037f52717ec8bc307b8 Mon Sep 17 00:00:00 2001 From: Sumit Naiksatam Date: Tue, 23 Oct 2012 01:37:28 -0700 Subject: [PATCH] RESTProxy Plugin for Floodlight and BigSwitch blueprint restproxy-plugin Implements blueprint restproxy-plugin. This Quantum plugin translates Quantum function calls to authenticated REST requests to a set of redundant external network controllers. Change-Id: Idbde6a7403c459503e8e0ee8b25ddaa8af5be36e --- etc/quantum/plugins/bigswitch/restproxy.ini | 30 + quantum/plugins/bigswitch/README | 14 + quantum/plugins/bigswitch/__init__.py | 16 + quantum/plugins/bigswitch/plugin.py | 738 ++++++++++++++++++ quantum/plugins/bigswitch/tests/__init__.py | 16 + .../plugins/bigswitch/tests/test_server.py | 186 +++++ quantum/plugins/bigswitch/version.py | 57 ++ quantum/tests/unit/bigswitch/__init__.py | 16 + .../unit/bigswitch/etc/restproxy.ini.test | 26 + .../unit/bigswitch/test_restproxy_plugin.py | 112 +++ 10 files changed, 1211 insertions(+) create mode 100644 etc/quantum/plugins/bigswitch/restproxy.ini create mode 100644 quantum/plugins/bigswitch/README create mode 100644 quantum/plugins/bigswitch/__init__.py create mode 100644 quantum/plugins/bigswitch/plugin.py create mode 100644 quantum/plugins/bigswitch/tests/__init__.py create mode 100644 quantum/plugins/bigswitch/tests/test_server.py create mode 100644 quantum/plugins/bigswitch/version.py create mode 100644 quantum/tests/unit/bigswitch/__init__.py create mode 100644 quantum/tests/unit/bigswitch/etc/restproxy.ini.test create mode 100644 quantum/tests/unit/bigswitch/test_restproxy_plugin.py diff --git a/etc/quantum/plugins/bigswitch/restproxy.ini b/etc/quantum/plugins/bigswitch/restproxy.ini new file mode 100644 index 000000000..bb8524306 --- /dev/null +++ b/etc/quantum/plugins/bigswitch/restproxy.ini @@ -0,0 +1,30 @@ +# Config file for quantum-proxy-plugin. + +[DATABASE] +# This line MUST be changed to actually run the plugin. +# Example: +# sql_connection = mysql://root:pass@127.0.0.1:3306/restproxy_quantum +# Replace 127.0.0.1 above with the IP address of the database used by the +# main quantum server. (Leave it as is if the database runs on this host.) +sql_connection = sqlite:// +# Database reconnection retry times - in event connectivity is lost +# set to -1 implies an infinite retry count +# sql_max_retries = 10 +# Database reconnection interval in seconds - in event connectivity is lost +reconnect_interval = 2 + +[RESTPROXY] +# All configuration for this plugin is in section '[restproxy]' +# +# The following parameters are supported: +# servers : [,]* (Error if not set) +# serverauth : (default: no auth) +# serverssl : True | False (default: False) +# syncdata : True | False (default: False) +# servertimeout : 10 (default: 10 seconds) +# +servers=localhost:8080 +#serverauth=username:password +#serverssl=True +#syncdata=True +#servertimeout=10 diff --git a/quantum/plugins/bigswitch/README b/quantum/plugins/bigswitch/README new file mode 100644 index 000000000..ecdca1322 --- /dev/null +++ b/quantum/plugins/bigswitch/README @@ -0,0 +1,14 @@ +# Quantum REST Proxy Plug-in for Big Switch and FloodLight Controllers + +This module provides a generic quantum plugin 'QuantumRestProxy' that +translates quantum function calls to authenticated REST requests (JSON supported) +to a set of redundant external network controllers. + +It also keeps a local persistent store of quantum state that has been +setup using that API. + +Currently the FloodLight Openflow Controller or the Big Switch Networks Controller +can be configured as external network controllers for this plugin. + +For more details on this plugin, please refer to the following link: +http://www.openflowhub.org/display/floodlightcontroller/Quantum+REST+Proxy+Plugin diff --git a/quantum/plugins/bigswitch/__init__.py b/quantum/plugins/bigswitch/__init__.py new file mode 100644 index 000000000..2a2421616 --- /dev/null +++ b/quantum/plugins/bigswitch/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Big Switch Networks, 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. +# diff --git a/quantum/plugins/bigswitch/plugin.py b/quantum/plugins/bigswitch/plugin.py new file mode 100644 index 000000000..50e1ab8e0 --- /dev/null +++ b/quantum/plugins/bigswitch/plugin.py @@ -0,0 +1,738 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 Big Switch Networks, 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: Mandeep Dhami, Big Switch Networks, Inc. +# @author: Sumit Naiksatam, sumitnaiksatam@gmail.com, Big Switch Networks, Inc. + +""" +Quantum REST Proxy Plug-in for Big Switch and FloodLight Controllers + +QuantumRestProxy provides a generic quantum plugin that translates all plugin +function calls to equivalent authenticated REST calls to a set of redundant +external network controllers. It also keeps persistent store for all quantum +state to allow for re-sync of the external controller(s), if required. + +The local state on the plugin also allows for local response and fast-fail +semantics where it can be determined based on the local persistent store. + +Network controller specific code is decoupled from this plugin and expected +to reside on the controller itself (via the REST interface). + +This allows for: + - independent authentication and redundancy schemes between quantum and the + network controller + - independent upgrade/development cycles between quantum and the controller + as it limits the proxy code upgrade requirement to quantum release cycle + and the controller specific code upgrade requirement to controller code + - ability to sync the controller with quantum for independent recovery/reset + +External REST API used by proxy is the same API as defined for quantum (JSON +subset) with some additional parameters (gateway on network-create and macaddr +on port-attach) on an additional PUT to do a bulk dump of all persistent data. +""" + +import base64 +import httplib +import json +import socket + +from quantum.common import exceptions +from quantum.common import topics +from quantum import context as qcontext +from quantum.db import api as db +from quantum.db import db_base_plugin_v2 +from quantum.db import dhcp_rpc_base +from quantum.db import models_v2 +from quantum.openstack.common import cfg +from quantum.openstack.common import context as glbcontext +from quantum.openstack.common import log as logging +from quantum.openstack.common import rpc +from quantum.openstack.common.rpc import dispatcher +from quantum.plugins.bigswitch.version import version_string_with_vcs + + +LOG = logging.getLogger(__name__) + + +database_opts = [ + cfg.StrOpt('sql_connection', default='sqlite://'), + cfg.IntOpt('sql_max_retries', default=-1), + cfg.IntOpt('reconnect_interval', default=2), +] + + +restproxy_opts = [ + cfg.StrOpt('servers', default='localhost:8800'), + cfg.StrOpt('serverauth', default='username:password'), + cfg.BoolOpt('serverssl', default=False), + cfg.BoolOpt('syncdata', default=False), + cfg.IntOpt('servertimeout', default=10), +] + + +cfg.CONF.register_opts(database_opts, "DATABASE") +cfg.CONF.register_opts(restproxy_opts, "RESTPROXY") + + +# The following are used to invoke the API on the external controller +NET_RESOURCE_PATH = "/tenants/%s/networks" +PORT_RESOURCE_PATH = "/tenants/%s/networks/%s/ports" +NETWORKS_PATH = "/tenants/%s/networks/%s" +PORTS_PATH = "/tenants/%s/networks/%s/ports/%s" +ATTACHMENT_PATH = "/tenants/%s/networks/%s/ports/%s/attachment" +SUCCESS_CODES = range(200, 207) +FAILURE_CODES = [0, 301, 302, 303, 400, 401, 403, 404, 500, 501, 502, 503, + 504, 505] +SYNTAX_ERROR_MESSAGE = 'Syntax error in server config file, aborting plugin' + + +class RemoteRestError(exceptions.QuantumException): + def __init__(self, message): + if message is None: + message = "None" + self.message = _("Error in REST call to remote network " + "controller") + ": " + message + super(RemoteRestError, self).__init__() + + +class ServerProxy(object): + """REST server proxy to a network controller.""" + + def __init__(self, server, port, ssl, auth, timeout, base_uri, name): + self.server = server + self.port = port + self.ssl = ssl + self.base_uri = base_uri + self.timeout = timeout + self.name = name + self.success_codes = SUCCESS_CODES + self.auth = None + if auth: + self.auth = 'Basic ' + base64.encodestring(auth).strip() + + def rest_call(self, action, resource, data, headers): + uri = self.base_uri + resource + body = json.dumps(data) + if not headers: + headers = {} + headers['Content-type'] = 'application/json' + headers['Accept'] = 'application/json' + headers['QuantumProxy-Agent'] = self.name + if self.auth: + headers['Authorization'] = self.auth + + LOG.debug('ServerProxy: server=%s, port=%d, ssl=%r, action=%s' % + (self.server, self.port, self.ssl, action)) + LOG.debug('ServerProxy: resource=%s, data=%r, headers=%r' % + (resource, data, headers)) + + conn = None + if self.ssl: + conn = httplib.HTTPSConnection( + self.server, self.port, timeout=self.timeout) + if conn is None: + LOG.error('ServerProxy: Could not establish HTTPS connection') + return 0, None, None, None + else: + conn = httplib.HTTPConnection( + self.server, self.port, timeout=self.timeout) + if conn is None: + LOG.error('ServerProxy: Could not establish HTTP connection') + return 0, None, None, None + + try: + conn.request(action, uri, body, headers) + response = conn.getresponse() + respstr = response.read() + respdata = respstr + if response.status in self.success_codes: + try: + respdata = json.loads(respstr) + except ValueError: + # response was not JSON, ignore the exception + pass + ret = (response.status, response.reason, respstr, respdata) + except (socket.timeout, socket.error) as e: + LOG.error('ServerProxy: %s failure, %r' % (action, e)) + ret = 0, None, None, None + conn.close() + LOG.debug('ServerProxy: status=%d, reason=%r, ret=%s, data=%r' % ret) + return ret + + +class ServerPool(object): + def __init__(self, servers, ssl, auth, timeout=10, + base_uri='/quantum/v1.0', name='QuantumRestProxy'): + self.base_uri = base_uri + self.timeout = timeout + self.name = name + self.auth = auth + self.ssl = ssl + self.servers = [] + for server_port in servers: + self.servers.append(self.server_proxy_for(*server_port)) + + def server_proxy_for(self, server, port): + return ServerProxy(server, port, self.ssl, self.auth, self.timeout, + self.base_uri, self.name) + + def server_failure(self, resp): + """Define failure codes as required. + Note: We assume 301-303 is a failure, and try the next server in + the server pool. + """ + return resp[0] in FAILURE_CODES + + def action_success(self, resp): + """Defining success codes as required. + Note: We assume any valid 2xx as being successful response. + """ + return resp[0] in SUCCESS_CODES + + def rest_call(self, action, resource, data, headers): + failed_servers = [] + while self.servers: + active_server = self.servers[0] + ret = active_server.rest_call(action, resource, data, headers) + if not self.server_failure(ret): + self.servers.extend(failed_servers) + return ret + else: + LOG.error('ServerProxy: %s failure for servers: %r' % ( + action, (active_server.server, active_server.port))) + failed_servers.append(self.servers.pop(0)) + + # All servers failed, reset server list and try again next time + LOG.error('ServerProxy: %s failure for all servers: %r' % ( + action, tuple((s.server, s.port) for s in failed_servers))) + self.servers.extend(failed_servers) + return (0, None, None, None) + + def get(self, resource, data='', headers=None): + return self.rest_call('GET', resource, data, headers) + + def put(self, resource, data, headers=None): + return self.rest_call('PUT', resource, data, headers) + + def post(self, resource, data, headers=None): + return self.rest_call('POST', resource, data, headers) + + def delete(self, resource, data='', headers=None): + return self.rest_call('DELETE', resource, data, headers) + + +class RpcProxy(dhcp_rpc_base.DhcpRpcCallbackMixin): + + RPC_API_VERSION = '1.0' + + def __init__(self, rpc_context): + self.rpc_context = rpc_context + + def create_rpc_dispatcher(self): + return dispatcher.RpcDispatcher([self]) + + +class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): + + def __init__(self): + LOG.info('QuantumRestProxy: Starting plugin. Version=%s' % + version_string_with_vcs()) + + # init DB, proxy's persistent store defaults to in-memory sql-lite DB + options = {"sql_connection": "%s" % cfg.CONF.DATABASE.sql_connection, + "sql_max_retries": cfg.CONF.DATABASE.sql_max_retries, + "reconnect_interval": cfg.CONF.DATABASE.reconnect_interval, + "base": models_v2.model_base.BASEV2} + db.configure_db(options) + + # 'servers' is the list of network controller REST end-points + # (used in order specified till one suceeds, and it is sticky + # till next failure). Use 'serverauth' to encode api-key + servers = cfg.CONF.RESTPROXY.servers + serverauth = cfg.CONF.RESTPROXY.serverauth + serverssl = cfg.CONF.RESTPROXY.serverssl + syncdata = cfg.CONF.RESTPROXY.syncdata + timeout = cfg.CONF.RESTPROXY.servertimeout + + # validate config + assert servers is not None, 'Servers not defined. Aborting plugin' + servers = tuple(s.rsplit(':', 1) for s in servers.split(',')) + servers = tuple((server, int(port)) for server, port in servers) + assert all(len(s) == 2 for s in servers), SYNTAX_ERROR_MESSAGE + + # init network ctrl connections + self.servers = ServerPool(servers, serverssl, serverauth, + timeout) + + # init dhcp support + self.topic = topics.PLUGIN + self.rpc_context = glbcontext.RequestContext( + 'quantum', 'quantum', is_admin=False) + self.conn = rpc.create_connection(new=True) + self.callbacks = RpcProxy(self.rpc_context) + self.dispatcher = self.callbacks.create_rpc_dispatcher() + self.conn.create_consumer(self.topic, self.dispatcher, + fanout=False) + # Consume from all consumers in a thread + self.conn.consume_in_thread() + if syncdata: + self._send_all_data() + + LOG.debug("QuantumRestProxyV2: initialization done") + + def create_network(self, context, network): + """Create a network, which represents an L2 network segment which + can have a set of subnets and ports associated with it. + :param context: quantum api request context + :param network: dictionary describing the network + + :returns: a sequence of mappings with the following signature: + { + "id": UUID representing the network. + "name": Human-readable name identifying the network. + "tenant_id": Owner of network. NOTE: only admin user can specify + a tenant_id other than its own. + "admin_state_up": Sets admin state of network. + if down, network does not forward packets. + "status": Indicates whether network is currently operational + (values are "ACTIVE", "DOWN", "BUILD", and "ERROR") + "subnets": Subnets associated with this network. + } + + :raises: RemoteRestError + """ + + LOG.debug("QuantumRestProxyV2: create_network() called") + + # Validate args + tenant_id = self._get_tenant_id_for_create(context, network["network"]) + net_name = network["network"]["name"] + if network["network"]["admin_state_up"] is False: + LOG.warning("Network with admin_state_up=False are not yet " + "supported by this plugin. Ignoring setting for " + "network %s", net_name) + + # create in DB + new_net = super(QuantumRestProxyV2, self).create_network(context, + network) + + # create on networl ctrl + try: + resource = NET_RESOURCE_PATH % tenant_id + data = { + "network": { + "id": new_net["id"], + "name": new_net["name"], + } + } + ret = self.servers.post(resource, data) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + except RemoteRestError as e: + LOG.error("QuantumRestProxyV2:Unable to create remote network:%s" % + e.message) + super(QuantumRestProxyV2, self).delete_network(context, + new_net['id']) + raise + + # return created network + return new_net + + def update_network(self, context, net_id, network): + """Updates the properties of a particular Virtual Network. + :param context: quantum api request context + :param net_id: uuid of the network to update + :param network: dictionary describing the updates + + :returns: a sequence of mappings with the following signature: + { + "id": UUID representing the network. + "name": Human-readable name identifying the network. + "tenant_id": Owner of network. NOTE: only admin user can + specify a tenant_id other than its own. + "admin_state_up": Sets admin state of network. + if down, network does not forward packets. + "status": Indicates whether network is currently operational + (values are "ACTIVE", "DOWN", "BUILD", and "ERROR") + "subnets": Subnets associated with this network. + } + + :raises: exceptions.NetworkNotFound + :raises: RemoteRestError + """ + + LOG.debug("QuantumRestProxyV2.update_network() called") + + # Validate Args + if network["network"].get("admin_state_up"): + if network["network"]["admin_state_up"] is False: + LOG.warning("Network with admin_state_up=False are not yet " + "supported by this plugin. Ignoring setting for " + "network %s", net_name) + + # update DB + orig_net = super(QuantumRestProxyV2, self).get_network(context, net_id) + tenant_id = orig_net["tenant_id"] + new_net = super(QuantumRestProxyV2, self).update_network( + context, net_id, network) + + # update network on network controller + if new_net["name"] != orig_net["name"]: + try: + resource = NETWORKS_PATH % (tenant_id, net_id) + data = { + "network": new_net, + } + ret = self.servers.put(resource, data) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + except RemoteRestError as e: + LOG.error( + "QuantumRestProxyV2: Unable to update remote network: %s" % + e.message) + # reset network to original state + super(QuantumRestProxyV2, self).update_network( + context, id, orig_net) + raise + + # return updated network + return new_net + + def delete_network(self, context, net_id): + """Delete a network. + :param context: quantum api request context + :param id: UUID representing the network to delete. + + :returns: None + + :raises: exceptions.NetworkInUse + :raises: exceptions.NetworkNotFound + :raises: RemoteRestError + """ + LOG.debug("QuantumRestProxyV2: delete_network() called") + + # Validate args + orig_net = super(QuantumRestProxyV2, self).get_network(context, net_id) + tenant_id = orig_net["tenant_id"] + + # delete from network ctrl. Remote error on delete is ignored + try: + resource = NETWORKS_PATH % (tenant_id, net_id) + ret = self.servers.delete(resource) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + ret_val = super(QuantumRestProxyV2, self).delete_network(context, + net_id) + return ret_val + except RemoteRestError as e: + LOG.error( + "QuantumRestProxyV2: Unable to update remote network: %s" % + e.message) + + def create_port(self, context, port): + """Create a port, which is a connection point of a device + (e.g., a VM NIC) to attach to a L2 Quantum network. + :param context: quantum api request context + :param port: dictionary describing the port + + :returns: + { + "id": uuid represeting the port. + "network_id": uuid of network. + "tenant_id": tenant_id + "mac_address": mac address to use on this port. + "admin_state_up": Sets admin state of port. if down, port + does not forward packets. + "status": dicates whether port is currently operational + (limit values to "ACTIVE", "DOWN", "BUILD", and "ERROR") + "fixed_ips": list of subnet ID"s and IP addresses to be used on + this port + "device_id": identifies the device (e.g., virtual server) using + this port. + } + + :raises: exceptions.NetworkNotFound + :raises: exceptions.StateInvalid + :raises: RemoteRestError + """ + LOG.debug("QuantumRestProxyV2: create_port() called") + + # Update DB + port["port"]["admin_state_up"] = False + new_port = super(QuantumRestProxyV2, self).create_port(context, port) + net = super(QuantumRestProxyV2, + self).get_network(context, new_port["network_id"]) + + # create on networl ctrl + try: + resource = PORT_RESOURCE_PATH % (net["tenant_id"], net["id"]) + data = { + "port": { + "id": new_port["id"], + "state": "ACTIVE", + } + } + ret = self.servers.post(resource, data) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + + # connect device to network, if present + if port["port"].get("device_id"): + self._plug_interface(context, + net["tenant_id"], net["id"], + new_port["id"], new_port["id"] + "00") + except RemoteRestError as e: + LOG.error("QuantumRestProxyV2: Unable to create remote port: %s" % + e.message) + super(QuantumRestProxyV2, self).delete_port(context, + new_port["id"]) + raise + + # Set port state up and return that port + port_update = {"port": {"admin_state_up": True}} + return super(QuantumRestProxyV2, self).update_port(context, + new_port["id"], + port_update) + + def update_port(self, context, port_id, port): + """Update values of a port. + :param context: quantum api request context + :param id: UUID representing the port to update. + :param port: dictionary with keys indicating fields to update. + + :returns: a mapping sequence with the following signature: + { + "id": uuid represeting the port. + "network_id": uuid of network. + "tenant_id": tenant_id + "mac_address": mac address to use on this port. + "admin_state_up": sets admin state of port. if down, port + does not forward packets. + "status": dicates whether port is currently operational + (limit values to "ACTIVE", "DOWN", "BUILD", and "ERROR") + "fixed_ips": list of subnet ID's and IP addresses to be used on + this port + "device_id": identifies the device (e.g., virtual server) using + this port. + } + + :raises: exceptions.StateInvalid + :raises: exceptions.PortNotFound + :raises: RemoteRestError + """ + LOG.debug("QuantumRestProxyV2: update_port() called") + + # Validate Args + orig_port = super(QuantumRestProxyV2, self).get_port(context, port_id) + + # Update DB + new_port = super(QuantumRestProxyV2, self).update_port(context, + port_id, port) + + # update on networl ctrl + try: + resource = PORTS_PATH % (orig_port["tenant_id"], + orig_port["network_id"], port_id) + data = {"port": new_port, } + ret = self.servers.put(resource, data) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + + if new_port.get("device_id") != orig_port.get("device_id"): + if orig_port.get("device_id"): + self._unplug_interface(context, orig_port["tenant_id"], + orig_port["network_id"], + orig_port["id"]) + if new_port.get("device_id"): + self._plug_interface(context, new_port["tenant_id"], + new_port["network_id"], + new_port["id"], new_port["id"] + "00") + + except RemoteRestError as e: + LOG.error( + "QuantumRestProxyV2: Unable to create remote port: %s" % + e.message) + # reset port to original state + super(QuantumRestProxyV2, self).update_port(context, port_id, + orig_port) + raise + + # return new_port + return new_port + + def delete_port(self, context, port_id): + """Delete a port. + :param context: quantum api request context + :param id: UUID representing the port to delete. + + :raises: exceptions.PortInUse + :raises: exceptions.PortNotFound + :raises: exceptions.NetworkNotFound + :raises: RemoteRestError + """ + + LOG.debug("QuantumRestProxyV2: delete_port() called") + + # Delete from DB + port = super(QuantumRestProxyV2, self).get_port(context, port_id) + + # delete from network ctrl. Remote error on delete is ignored + try: + resource = PORTS_PATH % (port["tenant_id"], port["network_id"], + port_id) + ret = self.servers.delete(resource) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + + if port.get("device_id"): + self._unplug_interface(context, port["tenant_id"], + port["network_id"], port["id"]) + ret_val = super(QuantumRestProxyV2, self).delete_port(context, + port_id) + return ret_val + except RemoteRestError as e: + LOG.error( + "QuantumRestProxyV2: Unable to update remote port: %s" % + e.message) + + def _plug_interface(self, context, tenant_id, net_id, port_id, + remote_interface_id): + """Attaches a remote interface to the specified port on the + specified Virtual Network. + + :returns: None + + :raises: exceptions.NetworkNotFound + :raises: exceptions.PortNotFound + :raises: RemoteRestError + """ + LOG.debug("QuantumRestProxyV2: _plug_interface() called") + + # update attachment on network controller + try: + port = super(QuantumRestProxyV2, self).get_port(context, port_id) + mac = port["mac_address"] + + for ip in port["fixed_ips"]: + if ip.get("subnet_id") is not None: + subnet = super(QuantumRestProxyV2, self).get_subnet( + context, ip["subnet_id"]) + gateway = subnet.get("gateway_ip") + if gateway is not None: + resource = NETWORKS_PATH % (tenant_id, net_id) + data = {"network": + {"id": net_id, + "gateway": gateway, + } + } + ret = self.servers.put(resource, data) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + + if mac is not None: + resource = ATTACHMENT_PATH % (tenant_id, net_id, port_id) + data = {"attachment": + {"id": remote_interface_id, + "mac": mac, + } + } + ret = self.servers.put(resource, data) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + except RemoteRestError as e: + LOG.error("QuantumRestProxyV2:Unable to update remote network:%s" % + e.message) + raise + + def _unplug_interface(self, context, tenant_id, net_id, port_id): + """Detaches a remote interface from the specified port on the + network controller + + :returns: None + + :raises: RemoteRestError + """ + LOG.debug("QuantumRestProxyV2: _unplug_interface() called") + + # delete from network ctrl. Remote error on delete is ignored + try: + resource = ATTACHMENT_PATH % (tenant_id, net_id, port_id) + ret = self.servers.delete(resource) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + except RemoteRestError as e: + LOG.error( + "QuantumRestProxyV2: Unable to update remote port: %s" % + e.message) + + def _send_all_data(self): + """Pushes all data to network ctrl (networks/ports, ports/attachments) + to give the controller an option to re-sync it's persistent store + with quantum's current view of that data. + """ + admin_context = qcontext.get_admin_context() + networks = {} + ports = {} + + all_networks = super(QuantumRestProxyV2, + self).get_networks(admin_context) or [] + for net in all_networks: + networks[net.get('id')] = { + 'id': net.get('id'), + 'name': net.get('name'), + 'op-status': net.get('admin_state_up'), + } + + subnets = net.get('subnets', []) + for subnet_id in subnets: + subnet = self.get_subnet(admin_context, subnet_id) + gateway_ip = subnet.get('gateway_ip') + if gateway_ip: + # FIX: For backward compatibility with wire protocol + networks[net.get('id')]['gateway'] = gateway_ip + + ports = [] + net_filter = {'network_id': [net.get('id')]} + net_ports = super(QuantumRestProxyV2, + self).get_ports(admin_context, + filters=net_filter) or [] + for port in net_ports: + port_details = { + 'id': port.get('id'), + 'attachment': { + 'id': port.get('id') + '00', + 'mac': port.get('mac_address'), + }, + 'state': port.get('status'), + 'op-status': port.get('admin_state_up'), + 'mac': None + } + ports.append(port_details) + networks[net.get('id')]['ports'] = ports + try: + resource = '/topology' + data = { + 'networks': networks, + } + ret = self.servers.put(resource, data) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + return ret + except RemoteRestError as e: + LOG.error( + 'QuantumRestProxy: Unable to update remote network: %s' % + e.message) + raise diff --git a/quantum/plugins/bigswitch/tests/__init__.py b/quantum/plugins/bigswitch/tests/__init__.py new file mode 100644 index 000000000..2a2421616 --- /dev/null +++ b/quantum/plugins/bigswitch/tests/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Big Switch Networks, 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. +# diff --git a/quantum/plugins/bigswitch/tests/test_server.py b/quantum/plugins/bigswitch/tests/test_server.py new file mode 100644 index 000000000..d1323e680 --- /dev/null +++ b/quantum/plugins/bigswitch/tests/test_server.py @@ -0,0 +1,186 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012, Big Switch Networks, 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: Mandeep Dhami, Big Switch Networks, Inc. +""" +Test server mocking a REST based network ctrl. Used for QuantumRestProxy tests +""" + +import json +import re + +from wsgiref.simple_server import make_server +from wsgiref.util import request_uri, application_uri + + +class TestNetworkCtrl(object): + def __init__(self, host='', port=8000, + default_status='404 Not Found', + default_response='404 Not Found', + debug=False): + self.host = host + self.port = port + self.default_status = default_status + self.default_response = default_response + self.debug = debug + self.debug_env = False + self.debug_resp = False + self.matches = [] + + def match(self, prior, method_regexp, uri_regexp, handler, data=None, + multi=True): + """Adds to the list of exptected inputs. The incomming request is + matched in the order of priority. For same priority, match the + oldest match request first. + + :param prior: intgere priority of this match (e.g. 100) + :param method_regexp: regexp to match method (e.g. 'PUT|POST') + :param uri_regexp: regexp to match uri (e.g. '/quantum/v?.?/') + :param handler: function with signature: + lambda(method, uri, body, **kwargs) : status, body + where + - method: HTTP method for this request + - uri: URI for this HTTP request + - body: body of this HTTP request + - kwargs are: + - data: data object that was in the match call + - node: TestNetworkCtrl object itself + - id: offset of the matching tuple + and return values is: + (status, body) where: + - status: HTTP resp status (e.g. '200 OK'). + If None, use default_status + - body: HTTP resp body. If None, use '' + """ + assert int(prior) == prior, 'Priority should an integer be >= 0' + assert prior >= 0, 'Priority should an integer be >= 0' + + lo, hi = 0, len(self.matches) + while lo < hi: + mid = (lo + hi) // 2 + if prior < self.matches[mid]: + hi = mid + else: + lo = mid + 1 + self.matches.insert(lo, (prior, method_regexp, uri_regexp, handler, + data, multi)) + + def remove_id(self, id_): + assert id_ >= 0, 'remove_id: id < 0' + assert id_ <= len(self.matches), 'remove_id: id > len()' + self.matches.pop(id_) + + def remove_match(self, prior, method_regexp, uri_regexp): + for i in self.matches: + if (i[0], i[1], i[2]) == (method_regexp, uri_regexp, idstr): + self.remove_id(i) + break + + def request_handler(self, method, uri, body): + retstatus = self.default_status + retbody = self.default_response + for i in xrange(len(self.matches)): + (prior, method_regexp, uri_regexp, handler, data, multi) = \ + self.matches[i] + if re.match(method_regexp, method) and re.match(uri_regexp, uri): + kwargs = { + 'data': data, + 'node': self, + 'id': i, + } + retstatus, retbody = handler(method, uri, body, **kwargs) + if multi is False: + self.remove_id(i) + break + if retbody is None: + retbody = '' + return (retstatus, retbody) + + def server(self): + def app(environ, start_response): + uri = environ['PATH_INFO'] + method = environ['REQUEST_METHOD'] + headers = [('Content-type', 'text/json')] + content_len_str = environ['CONTENT_LENGTH'] + + content_len = 0 + request_data = None + if content_len_str: + content_len = int(content_len_str) + request_data = environ.get('wsgi.input').read(content_len) + if request_data: + try: + request_data = json.loads(request_data) + except: + # OK for it not to be json! Ignore it + pass + + if self.debug: + print '\n' + if self.debug_env: + print '%s:' % 'environ:' + for (key, value) in sorted(environ.iteritems()): + print ' %16s : %s' % (key, value) + + print '%s %s' % (method, uri) + if request_data: + print '%s' % ( + json.dumps(request_data, sort_keys=True, indent=4)) + + status, body = self.request_handler(method, uri, None) + body_data = None + if body: + try: + body_data = json.loads(body) + except: + # OK for it not to be json! Ignore it + pass + + start_response(status, headers) + if self.debug: + if self.debug_env: + print '%s: %s' % ('Response', + json.dumps(body_data, sort_keys=True, indent=4)) + return body + return make_server(self.host, self.port, app) + + def run(self): + print "Serving on port %d ..." % self.port + try: + self.server().serve_forever() + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + import sys + + port = 8899 + if len(sys.argv) > 1: + port = int(sys.argv[1]) + + debug = False + if len(sys.argv) > 2: + if sys.argv[2].lower() in ['debug', 'true']: + debug = True + + ctrl = TestNetworkCtrl(port=port, + default_status='200 OK', + default_response='{"status":"200 OK"}', + debug=debug) + ctrl.match(100, 'GET', '/test', + lambda m, u, b, **k: ('200 OK', '["200 OK"]')) + ctrl.run() diff --git a/quantum/plugins/bigswitch/version.py b/quantum/plugins/bigswitch/version.py new file mode 100644 index 000000000..3ec12c809 --- /dev/null +++ b/quantum/plugins/bigswitch/version.py @@ -0,0 +1,57 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 OpenStack, LLC +# Copyright 2012, Big Switch Networks, Inc. +# +# 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. +# +# Based on openstack generic code +# @author: Mandeep Dhami, Big Switch Networks, Inc. + +"""Determine version of QuantumRestProxy plugin""" + +# if vcsversion exists, use it. Else, use LOCALBRANCH:LOCALREVISION +try: + from bigswitch.vcsversion import version_info +except ImportError: + version_info = {'branch_nick': u'LOCALBRANCH', + 'revision_id': u'LOCALREVISION', + 'revno': 0} + + +QUANTUMRESTPROXY_VERSION = ['2012', '1', None] +YEAR, COUNT, REVISION = QUANTUMRESTPROXY_VERSION +FINAL = False # This becomes true at Release Candidate time + + +def canonical_version_string(): + return '.'.join(filter(None, QUANTUMRESTPROXY_VERSION)) + + +def version_string(): + if FINAL: + return canonical_version_string() + else: + return '%s-dev' % (canonical_version_string(),) + + +def vcs_version_string(): + return "%s:%s" % (version_info['branch_nick'], version_info['revision_id']) + + +def version_string_with_vcs(): + return "%s-%s" % (canonical_version_string(), vcs_version_string()) + + +if __name__ == "__main__": + print version_string_with_vcs() diff --git a/quantum/tests/unit/bigswitch/__init__.py b/quantum/tests/unit/bigswitch/__init__.py new file mode 100644 index 000000000..cbf4a4506 --- /dev/null +++ b/quantum/tests/unit/bigswitch/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 OpenStack LLC. +# 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. diff --git a/quantum/tests/unit/bigswitch/etc/restproxy.ini.test b/quantum/tests/unit/bigswitch/etc/restproxy.ini.test new file mode 100644 index 000000000..ebcc7fe09 --- /dev/null +++ b/quantum/tests/unit/bigswitch/etc/restproxy.ini.test @@ -0,0 +1,26 @@ +# Test config file for quantum-proxy-plugin. + +[DATABASE] +# This line MUST be changed to actually run the plugin. +# Example: +# sql_connection = mysql://root:pass@127.0.0.1:3306/restproxy_quantum +# Replace 127.0.0.1 above with the IP address of the database used by the +# main quantum server. (Leave it as is if the database runs on this host.) +sql_connection = sqlite:// +# Database reconnection retry times - in event connectivity is lost +# set to -1 implies an infinite retry count +# sql_max_retries = 10 +# Database reconnection interval in seconds - in event connectivity is lost +reconnect_interval = 2 + +[RESTPROXY] +# All configuration for this plugin is in section '[restproxy]' +# +# The following parameters are supported: +# servers : [,]* (Error if not set) +# serverauth : (default: no auth) +# serverssl : True | False (default: False) +# +servers=localhost:8899 +serverssl=False +#serverauth=username:password diff --git a/quantum/tests/unit/bigswitch/test_restproxy_plugin.py b/quantum/tests/unit/bigswitch/test_restproxy_plugin.py new file mode 100644 index 000000000..bfa1e6bcd --- /dev/null +++ b/quantum/tests/unit/bigswitch/test_restproxy_plugin.py @@ -0,0 +1,112 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# Copyright 2012 Big Switch Networks, 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. + +import os + +from mock import patch + +import quantum.common.test_lib as test_lib +from quantum.manager import QuantumManager +import quantum.tests.unit.test_db_plugin as test_plugin + + +RESTPROXY_PKG_PATH = 'quantum.plugins.bigswitch.plugin' + + +class HTTPResponseMock(): + status = 200 + reason = 'OK' + + def __init__(self, sock, debuglevel=0, strict=0, method=None, + buffering=False): + pass + + def read(self): + return "{'status': '200 OK'}" + + +class HTTPConnectionMock(): + + def __init__(self, server, port, timeout): + pass + + def request(self, action, uri, body, headers): + return + + def getresponse(self): + return HTTPResponseMock(None) + + def close(self): + pass + + +class BigSwitchProxyPluginV2TestCase(test_plugin.QuantumDbPluginV2TestCase): + + _plugin_name = ('%s.QuantumRestProxyV2' % RESTPROXY_PKG_PATH) + + def setUp(self): + etc_path = os.path.join(os.path.dirname(__file__), 'etc') + test_lib.test_config['config_files'] = [os.path.join(etc_path, + 'restproxy.ini.test')] + + self.httpPatch = patch('httplib.HTTPConnection', create=True, + new=HTTPConnectionMock) + MockHTTPConnection = self.httpPatch.start() + super(BigSwitchProxyPluginV2TestCase, + self).setUp(self._plugin_name) + + def tearDown(self): + super(BigSwitchProxyPluginV2TestCase, self).tearDown() + self.httpPatch.stop() + + +class TestBigSwitchProxyBasicGet(test_plugin.TestBasicGet, + BigSwitchProxyPluginV2TestCase): + + pass + + +class TestBigSwitchProxyV2HTTPResponse(test_plugin.TestV2HTTPResponse, + BigSwitchProxyPluginV2TestCase): + + pass + + +class TestBigSwitchProxyPortsV2(test_plugin.TestPortsV2, + BigSwitchProxyPluginV2TestCase): + + pass + + +class TestBigSwitchProxyNetworksV2(test_plugin.TestNetworksV2, + BigSwitchProxyPluginV2TestCase): + + pass + + +class TestBigSwitchProxySubnetsV2(test_plugin.TestSubnetsV2, + BigSwitchProxyPluginV2TestCase): + + pass + + +class TestBigSwitchProxySync(BigSwitchProxyPluginV2TestCase): + + def test_send_data(self): + plugin_obj = QuantumManager.get_plugin() + result = plugin_obj._send_all_data() + self.assertEquals(result[0], 200) -- 2.45.2