]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
BigSwitch: Move config and REST to diff modules
authorKevin Benton <blak111@gmail.com>
Fri, 14 Feb 2014 06:52:36 +0000 (06:52 +0000)
committerKevin Benton <blak111@gmail.com>
Thu, 20 Feb 2014 09:43:52 +0000 (01:43 -0800)
No functionality change. Separates the config,
rest call, and backend server management from
the main plugin.py file. Necessary to make
downstream patches more managable and easier
to review.

Implements: blueprint bigswitch-separate-server-module
Change-Id: Ie1fd18a9d8cde24945513c06f7b62239202258a3

etc/neutron/plugins/bigswitch/restproxy.ini
neutron/plugins/bigswitch/config.py [new file with mode: 0644]
neutron/plugins/bigswitch/plugin.py
neutron/plugins/bigswitch/servermanager.py [new file with mode: 0644]
neutron/plugins/ml2/drivers/mech_bigswitch/driver.py
neutron/tests/unit/bigswitch/test_base.py

index 9c2a83a34537acac9922187ebe8f0aff8745df41..a89f84fa8760fb6fbdf8edb1506a9c1fde0fe19f 100644 (file)
@@ -4,13 +4,13 @@
 # All configuration for this plugin is in section '[restproxy]'
 #
 # The following parameters are supported:
-#   servers     :   <host:port>[,<host:port>]*  (Error if not set)
-#   server_auth  :   <username:password>         (default: no auth)
-#   server_ssl   :   True | False                (default: False)
-#   sync_data   :   True | False                (default: False)
-#   server_timeout   :  10                       (default: 10 seconds)
-#   neutron_id: <string>                         (default: neutron-<hostname>)
-#   add_meta_server_route: True | False          (default: True)
+#   servers               :  <host:port>[,<host:port>]*   (Error if not set)
+#   server_auth           :  <username:password>          (default: no auth)
+#   server_ssl            :  True | False                 (default: False)
+#   sync_data             :  True | False                 (default: False)
+#   server_timeout        :  10                           (default: 10 seconds)
+#   neutron_id            :  <string>                     (default: neutron-<hostname>)
+#   add_meta_server_route :  True | False                 (default: True)
 #
 
 # A comma separated list of BigSwitch or Floodlight servers and port numbers. The plugin proxies the requests to the BigSwitch/Floodlight server, which performs the networking configuration. Note that only one server is needed per deployment, but you may wish to deploy multiple servers to support failover.
@@ -19,11 +19,11 @@ servers=localhost:8080
 # The username and password for authenticating against  the BigSwitch or Floodlight controller.
 # server_auth=username:password
 
-# If True, Use SSL when connecting to the BigSwitch or Floodlight controller.
-# server_ssl=True
+# Use SSL when connecting to the BigSwitch or Floodlight controller.
+# server_ssl=False
 
 # Sync data on connect
-# sync_data=True
+# sync_data=False
 
 # Maximum number of seconds to wait for proxy request to connect and complete.
 # server_timeout=10
diff --git a/neutron/plugins/bigswitch/config.py b/neutron/plugins/bigswitch/config.py
new file mode 100644 (file)
index 0000000..7c46e10
--- /dev/null
@@ -0,0 +1,87 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2014 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.
+# @author: Kevin Benton, Big Switch Networks, Inc.
+
+"""
+This module manages configuration options
+"""
+
+from oslo.config import cfg
+
+from neutron.common import utils
+from neutron.extensions import portbindings
+
+restproxy_opts = [
+    cfg.ListOpt('servers', default=['localhost:8800'],
+                help=_("A comma separated list of BigSwitch or Floodlight "
+                       "servers and port numbers. The plugin proxies the "
+                       "requests to the BigSwitch/Floodlight server, "
+                       "which performs the networking configuration. Only one"
+                       "server is needed per deployment, but you may wish to"
+                       "deploy multiple servers to support failover.")),
+    cfg.StrOpt('server_auth', default=None, secret=True,
+               help=_("The username and password for authenticating against "
+                      " the BigSwitch or Floodlight controller.")),
+    cfg.BoolOpt('server_ssl', default=False,
+                help=_("If True, Use SSL when connecting to the BigSwitch or "
+                       "Floodlight controller.")),
+    cfg.BoolOpt('sync_data', default=False,
+                help=_("Sync data on connect")),
+    cfg.IntOpt('server_timeout', default=10,
+               help=_("Maximum number of seconds to wait for proxy request "
+                      "to connect and complete.")),
+    cfg.StrOpt('neutron_id', default='neutron-' + utils.get_hostname(),
+               deprecated_name='quantum_id',
+               help=_("User defined identifier for this Neutron deployment")),
+    cfg.BoolOpt('add_meta_server_route', default=True,
+                help=_("Flag to decide if a route to the metadata server "
+                       "should be injected into the VM")),
+]
+router_opts = [
+    cfg.MultiStrOpt('tenant_default_router_rule', default=['*:any:any:permit'],
+                    help=_("The default router rules installed in new tenant "
+                           "routers. Repeat the config option for each rule. "
+                           "Format is <tenant>:<source>:<destination>:<action>"
+                           " Use an * to specify default for all tenants.")),
+    cfg.IntOpt('max_router_rules', default=200,
+               help=_("Maximum number of router rules")),
+]
+nova_opts = [
+    cfg.StrOpt('vif_type', default='ovs',
+               help=_("Virtual interface type to configure on "
+                      "Nova compute nodes")),
+]
+
+# Each VIF Type can have a list of nova host IDs that are fixed to that type
+for i in portbindings.VIF_TYPES:
+    opt = cfg.ListOpt('node_override_vif_' + i, default=[],
+                      help=_("Nova compute nodes to manually set VIF "
+                             "type to %s") % i)
+    nova_opts.append(opt)
+
+# Add the vif types for reference later
+nova_opts.append(cfg.ListOpt('vif_types',
+                             default=portbindings.VIF_TYPES,
+                             help=_('List of allowed vif_type values.')))
+
+
+def register_config():
+    cfg.CONF.register_opts(restproxy_opts, "RESTPROXY")
+    cfg.CONF.register_opts(router_opts, "ROUTER")
+    cfg.CONF.register_opts(nova_opts, "NOVA")
index cbeafcfd2c9da9873e6fa15edc3c0de8bab2d06a..d0bc7d46e4b53fcd31a06a05d660337a72de1b50 100644 (file)
@@ -44,11 +44,7 @@ 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 copy
-import httplib
-import json
-import socket
 
 from oslo.config import cfg
 
@@ -58,7 +54,6 @@ from neutron.common import constants as const
 from neutron.common import exceptions
 from neutron.common import rpc as q_rpc
 from neutron.common import topics
-from neutron.common import utils
 from neutron import context as qcontext
 from neutron.db import agents_db
 from neutron.db import agentschedulers_db
@@ -76,349 +71,20 @@ from neutron.openstack.common import excutils
 from neutron.openstack.common import importutils
 from neutron.openstack.common import log as logging
 from neutron.openstack.common import rpc
+from neutron.plugins.bigswitch import config as pl_config
 from neutron.plugins.bigswitch.db import porttracker_db
 from neutron.plugins.bigswitch import extensions
 from neutron.plugins.bigswitch import routerrule_db
+from neutron.plugins.bigswitch import servermanager
 from neutron.plugins.bigswitch.version import version_string_with_vcs
 
 LOG = logging.getLogger(__name__)
 
-restproxy_opts = [
-    cfg.StrOpt('servers', default='localhost:8800',
-               help=_("A comma separated list of BigSwitch or Floodlight "
-                      "servers and port numbers. The plugin proxies the "
-                      "requests to the BigSwitch/Floodlight server, "
-                      "which performs the networking configuration. Note that "
-                      "only one server is needed per deployment, but you may "
-                      "wish to deploy multiple servers to support failover.")),
-    cfg.StrOpt('server_auth', default=None, secret=True,
-               help=_("The username and password for authenticating against "
-                      " the BigSwitch or Floodlight controller.")),
-    cfg.BoolOpt('server_ssl', default=False,
-                help=_("If True, Use SSL when connecting to the BigSwitch or "
-                       "Floodlight controller.")),
-    cfg.BoolOpt('sync_data', default=False,
-                help=_("Sync data on connect")),
-    cfg.IntOpt('server_timeout', default=10,
-               help=_("Maximum number of seconds to wait for proxy request "
-                      "to connect and complete.")),
-    cfg.StrOpt('neutron_id', default='neutron-' + utils.get_hostname(),
-               deprecated_name='quantum_id',
-               help=_("User defined identifier for this Neutron deployment")),
-    cfg.BoolOpt('add_meta_server_route', default=True,
-                help=_("Flag to decide if a route to the metadata server "
-                       "should be injected into the VM")),
-]
-
-
-cfg.CONF.register_opts(restproxy_opts, "RESTPROXY")
-
-router_opts = [
-    cfg.MultiStrOpt('tenant_default_router_rule', default=['*:any:any:permit'],
-                    help=_("The default router rules installed in new tenant "
-                           "routers. Repeat the config option for each rule. "
-                           "Format is <tenant>:<source>:<destination>:<action>"
-                           " Use an * to specify default for all tenants.")),
-    cfg.IntOpt('max_router_rules', default=200,
-               help=_("Maximum number of router rules")),
-]
-
-cfg.CONF.register_opts(router_opts, "ROUTER")
-
-nova_opts = [
-    cfg.StrOpt('vif_type', default='ovs',
-               help=_("Virtual interface type to configure on "
-                      "Nova compute nodes")),
-]
-
-# Each VIF Type can have a list of nova host IDs that are fixed to that type
-for i in portbindings.VIF_TYPES:
-    opt = cfg.ListOpt('node_override_vif_' + i, default=[],
-                      help=_("Nova compute nodes to manually set VIF "
-                             "type to %s") % i)
-    nova_opts.append(opt)
-
-# Add the vif types for reference later
-nova_opts.append(cfg.ListOpt('vif_types',
-                             default=portbindings.VIF_TYPES,
-                             help=_('List of allowed vif_type values.')))
-
-cfg.CONF.register_opts(nova_opts, "NOVA")
-
-
-# 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"
-ROUTER_RESOURCE_PATH = "/tenants/%s/routers"
-ROUTER_INTF_OP_PATH = "/tenants/%s/routers/%s/interfaces"
-NETWORKS_PATH = "/tenants/%s/networks/%s"
-PORTS_PATH = "/tenants/%s/networks/%s/ports/%s"
-ATTACHMENT_PATH = "/tenants/%s/networks/%s/ports/%s/attachment"
-ROUTERS_PATH = "/tenants/%s/routers/%s"
-ROUTER_INTF_PATH = "/tenants/%s/routers/%s/interfaces/%s"
-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')
-BASE_URI = '/networkService/v1.1'
-ORCHESTRATION_SERVICE_ID = 'Neutron v2.0'
 METADATA_SERVER_IP = '169.254.169.254'
 
 
-class RemoteRestError(exceptions.NeutronException):
-    message = _("Error in REST call to remote network "
-                "controller: %(reason)s")
-
-
-class ServerProxy(object):
-    """REST server proxy to a network controller."""
-
-    def __init__(self, server, port, ssl, auth, neutron_id, 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
-        self.neutron_id = neutron_id
-        self.failed = False
-        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['NeutronProxy-Agent'] = self.name
-        headers['Instance-ID'] = self.neutron_id
-        headers['Orchestration-Service-ID'] = ORCHESTRATION_SERVICE_ID
-        if self.auth:
-            headers['Authorization'] = self.auth
-
-        LOG.debug(_("ServerProxy: server=%(server)s, port=%(port)d, "
-                    "ssl=%(ssl)r"),
-                  {'server': self.server, 'port': self.port, 'ssl': self.ssl})
-        LOG.debug(_("ServerProxy: resource=%(resource)s, action=%(action)s, "
-                    "data=%(data)r, headers=%(headers)r"),
-                  {'resource': resource, 'data': data, 'headers': headers,
-                   'action': action})
-
-        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: %(action)s failure, %(e)r'),
-                      {'action': action, 'e': e})
-            ret = 0, None, None, None
-        conn.close()
-        LOG.debug(_("ServerProxy: status=%(status)d, reason=%(reason)r, "
-                    "ret=%(ret)s, data=%(data)r"), {'status': ret[0],
-                                                    'reason': ret[1],
-                                                    'ret': ret[2],
-                                                    'data': ret[3]})
-        return ret
-
-
-class ServerPool(object):
-
-    def __init__(self, timeout=10,
-                 base_uri=BASE_URI, name='NeutronRestProxy'):
-        LOG.debug(_("ServerPool: initializing"))
-        # 'servers' is the list of network controller REST end-points
-        # (used in order specified till one succeeds, and it is sticky
-        # till next failure). Use 'server_auth' to encode api-key
-        servers = cfg.CONF.RESTPROXY.servers
-        self.auth = cfg.CONF.RESTPROXY.server_auth
-        self.ssl = cfg.CONF.RESTPROXY.server_ssl
-        self.neutron_id = cfg.CONF.RESTPROXY.neutron_id
-        self.base_uri = base_uri
-        self.name = name
-        timeout = cfg.CONF.RESTPROXY.server_timeout
-        if timeout is not None:
-            self.timeout = timeout
-
-        # validate config
-        if not servers:
-            raise cfg.Error(_('Servers not defined. Aborting plugin'))
-        if any((len(spl) != 2) for spl in [sp.split(':', 1)
-                                           for sp in servers.split(',')]):
-            raise cfg.Error(_('Servers must be defined as <ip>:<port>'))
-        self.servers = [
-            self.server_proxy_for(server, int(port))
-            for server, port in (s.rsplit(':', 1) for s in servers.split(','))
-        ]
-        LOG.debug(_("ServerPool: initialization done"))
-
-    def server_proxy_for(self, server, port):
-        return ServerProxy(server, port, self.ssl, self.auth, self.neutron_id,
-                           self.timeout, self.base_uri, self.name)
-
-    def server_failure(self, resp, ignore_codes=[]):
-        """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 and resp[0] not in ignore_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
-
-    @utils.synchronized('bsn-rest-call', external=True)
-    def rest_call(self, action, resource, data, headers, ignore_codes):
-        good_first = sorted(self.servers, key=lambda x: x.failed)
-        for active_server in good_first:
-            ret = active_server.rest_call(action, resource, data, headers)
-            if not self.server_failure(ret, ignore_codes):
-                active_server.failed = False
-                return ret
-            else:
-                LOG.error(_('ServerProxy: %(action)s failure for servers: '
-                            '%(server)r Response: %(response)s'),
-                          {'action': action,
-                           'server': (active_server.server,
-                                      active_server.port),
-                           'response': ret[3]})
-                LOG.error(_("ServerProxy: Error details: status=%(status)d, "
-                            "reason=%(reason)r, ret=%(ret)s, data=%(data)r"),
-                          {'status': ret[0], 'reason': ret[1], 'ret': ret[2],
-                           'data': ret[3]})
-                active_server.failed = True
-
-        # All servers failed, reset server list and try again next time
-        LOG.error(_('ServerProxy: %(action)s failure for all servers: '
-                    '%(server)r'),
-                  {'action': action,
-                   'server': tuple((s.server,
-                                    s.port) for s in self.servers)})
-        return (0, None, None, None)
-
-    def rest_action(self, action, resource, data='', errstr='%s',
-                    ignore_codes=[], headers=None):
-        """
-        Wrapper for rest_call that verifies success and raises a
-        RemoteRestError on failure with a provided error string
-        By default, 404 errors on DELETE calls are ignored because
-        they already do not exist on the backend.
-        """
-        if not ignore_codes and action == 'DELETE':
-            ignore_codes = [404]
-        resp = self.rest_call(action, resource, data, headers, ignore_codes)
-        if self.server_failure(resp, ignore_codes):
-            LOG.error(errstr, resp[2])
-            raise RemoteRestError(reason=resp[2])
-        if resp[0] in ignore_codes:
-            LOG.warning(_("NeutronRestProxyV2: Received and ignored error "
-                          "code %(code)s on %(action)s action to resource "
-                          "%(resource)s"),
-                        {'code': resp[2], 'action': action,
-                         'resource': resource})
-        return resp
-
-    def rest_create_router(self, tenant_id, router):
-        resource = ROUTER_RESOURCE_PATH % tenant_id
-        data = {"router": router}
-        errstr = _("Unable to create remote router: %s")
-        self.rest_action('POST', resource, data, errstr)
-
-    def rest_update_router(self, tenant_id, router, router_id):
-        resource = ROUTERS_PATH % (tenant_id, router_id)
-        data = {"router": router}
-        errstr = _("Unable to update remote router: %s")
-        self.rest_action('PUT', resource, data, errstr)
-
-    def rest_delete_router(self, tenant_id, router_id):
-        resource = ROUTERS_PATH % (tenant_id, router_id)
-        errstr = _("Unable to delete remote router: %s")
-        self.rest_action('DELETE', resource, errstr=errstr)
-
-    def rest_add_router_interface(self, tenant_id, router_id, intf_details):
-        resource = ROUTER_INTF_OP_PATH % (tenant_id, router_id)
-        data = {"interface": intf_details}
-        errstr = _("Unable to add router interface: %s")
-        self.rest_action('POST', resource, data, errstr)
-
-    def rest_remove_router_interface(self, tenant_id, router_id, interface_id):
-        resource = ROUTER_INTF_PATH % (tenant_id, router_id, interface_id)
-        errstr = _("Unable to delete remote intf: %s")
-        self.rest_action('DELETE', resource, errstr=errstr)
-
-    def rest_create_network(self, tenant_id, network):
-        resource = NET_RESOURCE_PATH % tenant_id
-        data = {"network": network}
-        errstr = _("Unable to create remote network: %s")
-        self.rest_action('POST', resource, data, errstr)
-
-    def rest_update_network(self, tenant_id, net_id, network):
-        resource = NETWORKS_PATH % (tenant_id, net_id)
-        data = {"network": network}
-        errstr = _("Unable to update remote network: %s")
-        self.rest_action('PUT', resource, data, errstr)
-
-    def rest_delete_network(self, tenant_id, net_id):
-        resource = NETWORKS_PATH % (tenant_id, net_id)
-        errstr = _("Unable to update remote network: %s")
-        self.rest_action('DELETE', resource, errstr=errstr)
-
-    def rest_create_port(self, tenant_id, net_id, port):
-        resource = ATTACHMENT_PATH % (tenant_id, net_id, port["id"])
-        data = {"port": port}
-        device_id = port.get("device_id")
-        if not port["mac_address"] or not device_id:
-            # controller only cares about ports attached to devices
-            LOG.warning(_("No device attached to port %s. "
-                          "Skipping notification to controller."), port["id"])
-            return
-        data["attachment"] = {"id": device_id,
-                              "mac": port["mac_address"]}
-        errstr = _("Unable to create remote port: %s")
-        self.rest_action('PUT', resource, data, errstr)
-
-    def rest_delete_port(self, tenant_id, network_id, port_id):
-        resource = ATTACHMENT_PATH % (tenant_id, network_id, port_id)
-        errstr = _("Unable to delete remote port: %s")
-        self.rest_action('DELETE', resource, errstr=errstr)
-
-    def rest_update_port(self, tenant_id, net_id, port):
-        # Controller has no update operation for the port endpoint
-        self.rest_create_port(tenant_id, net_id, port)
-
-
 class RpcProxy(dhcp_rpc_base.DhcpRpcCallbackMixin):
 
     RPC_API_VERSION = '1.1'
@@ -585,9 +251,7 @@ class NeutronRestProxyV2Base(db_base_plugin_v2.NeutronDbPluginV2,
 
         resource['state'] = ('UP' if resource.pop('admin_state_up',
                                                   True) else 'DOWN')
-
-        if 'status' in resource:
-            del resource['status']
+        resource.pop('status', None)
 
         return resource
 
@@ -659,7 +323,7 @@ class NeutronRestProxyV2(NeutronRestProxyV2Base,
     def __init__(self, server_timeout=None):
         LOG.info(_('NeutronRestProxy: Starting plugin. Version=%s'),
                  version_string_with_vcs())
-
+        pl_config.register_config()
         # init DB, proxy's persistent store defaults to in-memory sql-lite DB
         db.configure_db()
 
@@ -669,7 +333,7 @@ class NeutronRestProxyV2(NeutronRestProxyV2Base,
         self.add_meta_server_route = cfg.CONF.RESTPROXY.add_meta_server_route
 
         # init network ctrl connections
-        self.servers = ServerPool(server_timeout, BASE_URI)
+        self.servers = servermanager.ServerPool(server_timeout)
 
         # init dhcp support
         self.topic = topics.PLUGIN
@@ -1180,7 +844,7 @@ class NeutronRestProxyV2(NeutronRestProxyV2Base,
             # create floatingip on the network controller
             try:
                 self._send_floatingip_update(context)
-            except RemoteRestError as e:
+            except servermanager.RemoteRestError as e:
                 with excutils.save_and_reraise_exception():
                     LOG.error(
                         _("NeutronRestProxyV2: Unable to create remote "
diff --git a/neutron/plugins/bigswitch/servermanager.py b/neutron/plugins/bigswitch/servermanager.py
new file mode 100644 (file)
index 0000000..7365083
--- /dev/null
@@ -0,0 +1,320 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+# Copyright 2014 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.
+# @author: Kevin Benton, Big Switch Networks, Inc.
+
+"""
+This module manages the HTTP and HTTPS connections to the backend controllers.
+
+The main class it provides for external use is ServerPool which manages a set
+of ServerProxy objects that correspond to individual backend controllers.
+
+The following functionality is handled by this module:
+- Translation of rest_* function calls to HTTP/HTTPS calls to the controllers
+- Automatic failover between controllers
+- HTTP Authentication
+
+"""
+import base64
+import httplib
+import json
+import socket
+
+from oslo.config import cfg
+
+from neutron.common import exceptions
+from neutron.common import utils
+from neutron.openstack.common import log as logging
+
+
+LOG = logging.getLogger(__name__)
+
+# 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"
+ROUTER_RESOURCE_PATH = "/tenants/%s/routers"
+ROUTER_INTF_OP_PATH = "/tenants/%s/routers/%s/interfaces"
+NETWORKS_PATH = "/tenants/%s/networks/%s"
+PORTS_PATH = "/tenants/%s/networks/%s/ports/%s"
+ATTACHMENT_PATH = "/tenants/%s/networks/%s/ports/%s/attachment"
+ROUTERS_PATH = "/tenants/%s/routers/%s"
+ROUTER_INTF_PATH = "/tenants/%s/routers/%s/interfaces/%s"
+SUCCESS_CODES = range(200, 207)
+FAILURE_CODES = [0, 301, 302, 303, 400, 401, 403, 404, 500, 501, 502, 503,
+                 504, 505]
+BASE_URI = '/networkService/v1.1'
+ORCHESTRATION_SERVICE_ID = 'Neutron v2.0'
+
+
+class RemoteRestError(exceptions.NeutronException):
+    message = _("Error in REST call to remote network "
+                "controller: %(reason)s")
+
+
+class ServerProxy(object):
+    """REST server proxy to a network controller."""
+
+    def __init__(self, server, port, ssl, auth, neutron_id, 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
+        self.neutron_id = neutron_id
+        self.failed = False
+        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['NeutronProxy-Agent'] = self.name
+        headers['Instance-ID'] = self.neutron_id
+        headers['Orchestration-Service-ID'] = ORCHESTRATION_SERVICE_ID
+        if self.auth:
+            headers['Authorization'] = self.auth
+
+        LOG.debug(_("ServerProxy: server=%(server)s, port=%(port)d, "
+                    "ssl=%(ssl)r"),
+                  {'server': self.server, 'port': self.port, 'ssl': self.ssl})
+        LOG.debug(_("ServerProxy: resource=%(resource)s, data=%(data)r, "
+                    "headers=%(headers)r, action=%(action)s"),
+                  {'resource': resource, 'data': data, 'headers': headers,
+                   'action': action})
+
+        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: %(action)s failure, %(e)r'),
+                      {'action': action, 'e': e})
+            ret = 0, None, None, None
+        conn.close()
+        LOG.debug(_("ServerProxy: status=%(status)d, reason=%(reason)r, "
+                    "ret=%(ret)s, data=%(data)r"), {'status': ret[0],
+                                                    'reason': ret[1],
+                                                    'ret': ret[2],
+                                                    'data': ret[3]})
+        return ret
+
+
+class ServerPool(object):
+
+    def __init__(self, timeout=10,
+                 base_uri=BASE_URI, name='NeutronRestProxy'):
+        LOG.debug(_("ServerPool: initializing"))
+        # 'servers' is the list of network controller REST end-points
+        # (used in order specified till one succeeds, and it is sticky
+        # till next failure). Use 'server_auth' to encode api-key
+        servers = cfg.CONF.RESTPROXY.servers
+        self.auth = cfg.CONF.RESTPROXY.server_auth
+        self.ssl = cfg.CONF.RESTPROXY.server_ssl
+        self.neutron_id = cfg.CONF.RESTPROXY.neutron_id
+        self.base_uri = base_uri
+        self.name = name
+        self.timeout = cfg.CONF.RESTPROXY.server_timeout
+        default_port = 8000
+        if timeout is not None:
+            self.timeout = timeout
+
+        if not servers:
+            raise cfg.Error(_('Servers not defined. Aborting server manager.'))
+        servers = [s if len(s.rsplit(':', 1)) == 2
+                   else "%s:%d" % (s, default_port)
+                   for s in servers]
+        if any((len(spl) != 2)for spl in [sp.rsplit(':', 1)
+                                          for sp in servers]):
+            raise cfg.Error(_('Servers must be defined as <ip>:<port>. '
+                              'Configuration was %s') % servers)
+        self.servers = [
+            self.server_proxy_for(server, int(port))
+            for server, port in (s.rsplit(':', 1) for s in servers)
+        ]
+        LOG.debug(_("ServerPool: initialization done"))
+
+    def server_proxy_for(self, server, port):
+        return ServerProxy(server, port, self.ssl, self.auth, self.neutron_id,
+                           self.timeout, self.base_uri, self.name)
+
+    def server_failure(self, resp, ignore_codes=[]):
+        """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 and resp[0] not in ignore_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
+
+    @utils.synchronized('bsn-rest-call', external=True)
+    def rest_call(self, action, resource, data, headers, ignore_codes):
+        good_first = sorted(self.servers, key=lambda x: x.failed)
+        for active_server in good_first:
+            ret = active_server.rest_call(action, resource, data, headers)
+            if not self.server_failure(ret, ignore_codes):
+                active_server.failed = False
+                return ret
+            else:
+                LOG.error(_('ServerProxy: %(action)s failure for servers: '
+                            '%(server)r Response: %(response)s'),
+                          {'action': action,
+                           'server': (active_server.server,
+                                      active_server.port),
+                           'response': ret[3]})
+                LOG.error(_("ServerProxy: Error details: status=%(status)d, "
+                            "reason=%(reason)r, ret=%(ret)s, data=%(data)r"),
+                          {'status': ret[0], 'reason': ret[1], 'ret': ret[2],
+                           'data': ret[3]})
+                active_server.failed = True
+
+        # All servers failed, reset server list and try again next time
+        LOG.error(_('ServerProxy: %(action)s failure for all servers: '
+                    '%(server)r'),
+                  {'action': action,
+                   'server': tuple((s.server,
+                                    s.port) for s in self.servers)})
+        return (0, None, None, None)
+
+    def rest_action(self, action, resource, data='', errstr='%s',
+                    ignore_codes=[], headers=None):
+        """
+        Wrapper for rest_call that verifies success and raises a
+        RemoteRestError on failure with a provided error string
+        By default, 404 errors on DELETE calls are ignored because
+        they already do not exist on the backend.
+        """
+        if not ignore_codes and action == 'DELETE':
+            ignore_codes = [404]
+        resp = self.rest_call(action, resource, data, headers, ignore_codes)
+        if self.server_failure(resp, ignore_codes):
+            LOG.error(errstr, resp[2])
+            raise RemoteRestError(reason=resp[2])
+        if resp[0] in ignore_codes:
+            LOG.warning(_("NeutronRestProxyV2: Received and ignored error "
+                          "code %(code)s on %(action)s action to resource "
+                          "%(resource)s"),
+                        {'code': resp[2], 'action': action,
+                         'resource': resource})
+        return resp
+
+    def rest_create_router(self, tenant_id, router):
+        resource = ROUTER_RESOURCE_PATH % tenant_id
+        data = {"router": router}
+        errstr = _("Unable to create remote router: %s")
+        self.rest_action('POST', resource, data, errstr)
+
+    def rest_update_router(self, tenant_id, router, router_id):
+        resource = ROUTERS_PATH % (tenant_id, router_id)
+        data = {"router": router}
+        errstr = _("Unable to update remote router: %s")
+        self.rest_action('PUT', resource, data, errstr)
+
+    def rest_delete_router(self, tenant_id, router_id):
+        resource = ROUTERS_PATH % (tenant_id, router_id)
+        errstr = _("Unable to delete remote router: %s")
+        self.rest_action('DELETE', resource, errstr=errstr)
+
+    def rest_add_router_interface(self, tenant_id, router_id, intf_details):
+        resource = ROUTER_INTF_OP_PATH % (tenant_id, router_id)
+        data = {"interface": intf_details}
+        errstr = _("Unable to add router interface: %s")
+        self.rest_action('POST', resource, data, errstr)
+
+    def rest_remove_router_interface(self, tenant_id, router_id, interface_id):
+        resource = ROUTER_INTF_PATH % (tenant_id, router_id, interface_id)
+        errstr = _("Unable to delete remote intf: %s")
+        self.rest_action('DELETE', resource, errstr=errstr)
+
+    def rest_create_network(self, tenant_id, network):
+        resource = NET_RESOURCE_PATH % tenant_id
+        data = {"network": network}
+        errstr = _("Unable to create remote network: %s")
+        self.rest_action('POST', resource, data, errstr)
+
+    def rest_update_network(self, tenant_id, net_id, network):
+        resource = NETWORKS_PATH % (tenant_id, net_id)
+        data = {"network": network}
+        errstr = _("Unable to update remote network: %s")
+        self.rest_action('PUT', resource, data, errstr)
+
+    def rest_delete_network(self, tenant_id, net_id):
+        resource = NETWORKS_PATH % (tenant_id, net_id)
+        errstr = _("Unable to update remote network: %s")
+        self.rest_action('DELETE', resource, errstr=errstr)
+
+    def rest_create_port(self, tenant_id, net_id, port):
+        resource = ATTACHMENT_PATH % (tenant_id, net_id, port["id"])
+        data = {"port": port}
+        device_id = port.get("device_id")
+        if not port["mac_address"] or not device_id:
+            # controller only cares about ports attached to devices
+            LOG.warning(_("No device MAC attached to port %s. "
+                          "Skipping notification to controller."), port["id"])
+            return
+        data["attachment"] = {"id": device_id,
+                              "mac": port["mac_address"]}
+        errstr = _("Unable to create remote port: %s")
+        self.rest_action('PUT', resource, data, errstr)
+
+    def rest_delete_port(self, tenant_id, network_id, port_id):
+        resource = ATTACHMENT_PATH % (tenant_id, network_id, port_id)
+        errstr = _("Unable to delete remote port: %s")
+        self.rest_action('DELETE', resource, errstr=errstr)
+
+    def rest_update_port(self, tenant_id, net_id, port):
+        # Controller has no update operation for the port endpoint
+        # the create PUT method will replace
+        self.rest_create_port(tenant_id, net_id, port)
index d1eb08ec019a434a65db5d821b9f3547b754e384..05143c307cf2e4f63e50ad7c5d14d4e52e4f0b36 100644 (file)
@@ -22,9 +22,10 @@ from oslo.config import cfg
 from neutron import context as ctx
 from neutron.extensions import portbindings
 from neutron.openstack.common import log
+from neutron.plugins.bigswitch import config as pl_config
 from neutron.plugins.bigswitch.db import porttracker_db
 from neutron.plugins.bigswitch.plugin import NeutronRestProxyV2Base
-from neutron.plugins.bigswitch.plugin import ServerPool
+from neutron.plugins.bigswitch.servermanager import ServerPool
 from neutron.plugins.ml2 import driver_api as api
 
 
@@ -43,6 +44,8 @@ class BigSwitchMechanismDriver(NeutronRestProxyV2Base,
     def initialize(self, server_timeout=None):
         LOG.debug(_('Initializing driver'))
 
+        # register plugin config opts
+        pl_config.register_config()
         # backend doesn't support bulk operations yet
         self.native_bulk_support = False
 
index 1797c510526c78d4217f1fc20d5c1092523d937b..1dd40ec0b6873290878fb963624fa80cc42394e6 100644 (file)
 
 import os
 
-from mock import patch
+import mock
 from oslo.config import cfg
 
 import neutron.common.test_lib as test_lib
 from neutron.db import api as db
+from neutron.plugins.bigswitch import config
 from neutron.tests.unit.bigswitch import fake_server
 
 RESTPROXY_PKG_PATH = 'neutron.plugins.bigswitch.plugin'
 NOTIFIER = 'neutron.plugins.bigswitch.plugin.RpcProxy'
+HTTPCON = 'httplib.HTTPConnection'
 
 
 class BigSwitchTestBase(object):
@@ -37,13 +39,13 @@ class BigSwitchTestBase(object):
         test_lib.test_config['config_files'] = [os.path.join(etc_path,
                                                 'restproxy.ini.test')]
         self.addCleanup(cfg.CONF.reset)
+        config.register_config()
 
     def setup_patches(self):
-        self.httpPatch = patch('httplib.HTTPConnection', create=True,
-                               new=fake_server.HTTPConnectionMock)
-        self.plugin_notifier_p = patch(NOTIFIER)
-        self.addCleanup(self.plugin_notifier_p.stop)
-        self.addCleanup(self.httpPatch.stop)
+        self.httpPatch = mock.patch(HTTPCON, create=True,
+                                    new=fake_server.HTTPConnectionMock)
+        self.plugin_notifier_p = mock.patch(NOTIFIER)
+        self.addCleanup(mock.patch.stopall)
         self.addCleanup(db.clear_db)
         self.plugin_notifier_p.start()
         self.httpPatch.start()