diff --git a/etc/neutron/plugins/bigswitch/restproxy.ini b/etc/neutron/plugins/bigswitch/restproxy.ini index 9c2a83a345..a89f84fa87 100644 --- a/etc/neutron/plugins/bigswitch/restproxy.ini +++ b/etc/neutron/plugins/bigswitch/restproxy.ini @@ -4,13 +4,13 @@ # All configuration for this plugin is in section '[restproxy]' # # The following parameters are supported: -# servers : [,]* (Error if not set) -# server_auth : (default: no auth) -# server_ssl : True | False (default: False) -# sync_data : True | False (default: False) -# server_timeout : 10 (default: 10 seconds) -# neutron_id: (default: neutron-) -# add_meta_server_route: True | False (default: True) +# servers : [,]* (Error if not set) +# server_auth : (default: no auth) +# server_ssl : True | False (default: False) +# sync_data : True | False (default: False) +# server_timeout : 10 (default: 10 seconds) +# neutron_id : (default: neutron-) +# 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 index 0000000000..7c46e105da --- /dev/null +++ b/neutron/plugins/bigswitch/config.py @@ -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 :::" + " 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") diff --git a/neutron/plugins/bigswitch/plugin.py b/neutron/plugins/bigswitch/plugin.py index cbeafcfd2c..d0bc7d46e4 100644 --- a/neutron/plugins/bigswitch/plugin.py +++ b/neutron/plugins/bigswitch/plugin.py @@ -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 :::" - " 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 :')) - 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 index 0000000000..73650833eb --- /dev/null +++ b/neutron/plugins/bigswitch/servermanager.py @@ -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 :. ' + '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) diff --git a/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py b/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py index d1eb08ec01..05143c307c 100644 --- a/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py +++ b/neutron/plugins/ml2/drivers/mech_bigswitch/driver.py @@ -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 diff --git a/neutron/tests/unit/bigswitch/test_base.py b/neutron/tests/unit/bigswitch/test_base.py index 1797c51052..1dd40ec0b6 100644 --- a/neutron/tests/unit/bigswitch/test_base.py +++ b/neutron/tests/unit/bigswitch/test_base.py @@ -17,15 +17,17 @@ 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()