RESTProxy Plugin for Floodlight and BigSwitch

blueprint restproxy-plugin

Implements blueprint restproxy-plugin.

This Quantum plugin translates Quantum function calls to authenticated
REST requests to a set of redundant external network controllers.

Change-Id: Idbde6a7403c459503e8e0ee8b25ddaa8af5be36e
This commit is contained in:
Sumit Naiksatam 2012-10-23 01:37:28 -07:00
parent dd2c77281d
commit 4f824cf4fb
10 changed files with 1211 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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.
#

View File

@ -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

View File

@ -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.
#

View File

@ -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()

View File

@ -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()

View File

@ -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.

View File

@ -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

View File

@ -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)