]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
RESTProxy Plugin for Floodlight and BigSwitch
authorSumit Naiksatam <sumitnaiksatam@gmail.com>
Tue, 23 Oct 2012 08:37:28 +0000 (01:37 -0700)
committerSumit Naiksatam <sumitnaiksatam@gmail.com>
Wed, 14 Nov 2012 03:30:05 +0000 (19:30 -0800)
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 [new file with mode: 0644]
quantum/plugins/bigswitch/README [new file with mode: 0644]
quantum/plugins/bigswitch/__init__.py [new file with mode: 0644]
quantum/plugins/bigswitch/plugin.py [new file with mode: 0644]
quantum/plugins/bigswitch/tests/__init__.py [new file with mode: 0644]
quantum/plugins/bigswitch/tests/test_server.py [new file with mode: 0644]
quantum/plugins/bigswitch/version.py [new file with mode: 0644]
quantum/tests/unit/bigswitch/__init__.py [new file with mode: 0644]
quantum/tests/unit/bigswitch/etc/restproxy.ini.test [new file with mode: 0644]
quantum/tests/unit/bigswitch/test_restproxy_plugin.py [new file with mode: 0644]

diff --git a/etc/quantum/plugins/bigswitch/restproxy.ini b/etc/quantum/plugins/bigswitch/restproxy.ini
new file mode 100644 (file)
index 0000000..bb85243
--- /dev/null
@@ -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     :   <host:port>[,<host:port>]*  (Error if not set)
+#   serverauth  :   <username:password>         (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 (file)
index 0000000..ecdca13
--- /dev/null
@@ -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 (file)
index 0000000..2a24216
--- /dev/null
@@ -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 (file)
index 0000000..50e1ab8
--- /dev/null
@@ -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 (file)
index 0000000..2a24216
--- /dev/null
@@ -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 (file)
index 0000000..d1323e6
--- /dev/null
@@ -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 (file)
index 0000000..3ec12c8
--- /dev/null
@@ -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 (file)
index 0000000..cbf4a45
--- /dev/null
@@ -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 (file)
index 0000000..ebcc7fe
--- /dev/null
@@ -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     :   <host:port>[,<host:port>]*  (Error if not set)
+#   serverauth  :   <username:password>         (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 (file)
index 0000000..bfa1e6b
--- /dev/null
@@ -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)