From 6020c897aba06b8701210a2473e19c609cee0e57 Mon Sep 17 00:00:00 2001 From: Sumit Naiksatam Date: Tue, 5 Feb 2013 20:25:46 -0800 Subject: [PATCH] L3 API support for BigSwitch-FloodLight Plugin In keeping with the philosophy of the RESTProxy plugin, L3 extension calls are processed (CRUD of logical resources) and the state changes are proxied to a backend controller. A configuration variable specific to the RESTProxy plugin is being added to identify that particular Quantum server's ID. blueprint quantum-floodlight-bigswitch-l3 Change-Id: I24be02cd836352497fe5b1e7622d138e0c816377 --- etc/quantum/plugins/bigswitch/restproxy.ini | 16 +- quantum/plugins/bigswitch/plugin.py | 696 +++++++++++++++--- quantum/plugins/bigswitch/vcsversion.py | 27 + quantum/plugins/bigswitch/version.py | 12 +- .../tests/unit/bigswitch/test_router_db.py | 317 ++++++++ 5 files changed, 942 insertions(+), 126 deletions(-) create mode 100644 quantum/plugins/bigswitch/vcsversion.py create mode 100644 quantum/tests/unit/bigswitch/test_router_db.py diff --git a/etc/quantum/plugins/bigswitch/restproxy.ini b/etc/quantum/plugins/bigswitch/restproxy.ini index 957179795b2..afe71f46660 100644 --- a/etc/quantum/plugins/bigswitch/restproxy.ini +++ b/etc/quantum/plugins/bigswitch/restproxy.ini @@ -28,13 +28,13 @@ reconnect_interval = 2 # # The following parameters are supported: # servers : [,]* (Error if not set) -# serverauth : (default: no auth) -# serverssl : True | False (default: False) -# syncdata : True | False (default: False) -# servertimeout : 10 (default: 10 seconds) +# server_auth : (default: no auth) +# server_ssl : True | False (default: False) +# sync_data : True | False (default: False) +# server_timeout : 10 (default: 10 seconds) # servers=localhost:8080 -#serverauth=username:password -#serverssl=True -#syncdata=True -#servertimeout=10 +#server_auth=username:password +#server_ssl=True +#sync_data=True +#server_timeout=10 diff --git a/quantum/plugins/bigswitch/plugin.py b/quantum/plugins/bigswitch/plugin.py index 5105da748ed..8566e72099c 100644 --- a/quantum/plugins/bigswitch/plugin.py +++ b/quantum/plugins/bigswitch/plugin.py @@ -45,18 +45,24 @@ 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 quantum.common import constants as const from quantum.common import exceptions from quantum.common import rpc as q_rpc from quantum.common import topics +from quantum.common import utils 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 l3_db +from quantum.extensions import l3 from quantum.openstack.common import cfg +from quantum.openstack.common import lockutils from quantum.openstack.common import log as logging from quantum.openstack.common import rpc from quantum.plugins.bigswitch.version import version_string_with_vcs @@ -69,16 +75,20 @@ restproxy_opts = [ cfg.StrOpt('servers', default='localhost:8800', help=_("A comma separated list of servers and port numbers " "to proxy request to.")), - cfg.StrOpt('serverauth', default='username:password', - help=_("Server authentication"), - secret=True), - cfg.BoolOpt('serverssl', default=False, + cfg.StrOpt('server_auth', default='username:password', secret=True, + help=_("Server authentication")), + cfg.BoolOpt('server_ssl', default=False, help=_("Use SSL to connect")), - cfg.BoolOpt('syncdata', default=False, + cfg.BoolOpt('sync_data', default=False, help=_("Sync data on connect")), - cfg.IntOpt('servertimeout', default=10, + cfg.IntOpt('server_timeout', default=10, help=_("Maximum number of seconds to wait for proxy request " "to connect and complete.")), + cfg.StrOpt('quantum_id', default='Quantum-' + utils.get_hostname(), + help=_("User defined identifier for this Quantum 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")), ] @@ -88,13 +98,20 @@ 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" +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 = 'Quantum v2.0' +METADATA_SERVER_IP = '169.254.169.254' class RemoteRestError(exceptions.QuantumException): @@ -109,7 +126,8 @@ class RemoteRestError(exceptions.QuantumException): class ServerProxy(object): """REST server proxy to a network controller.""" - def __init__(self, server, port, ssl, auth, timeout, base_uri, name): + def __init__(self, server, port, ssl, auth, quantum_id, timeout, + base_uri, name): self.server = server self.port = port self.ssl = ssl @@ -118,9 +136,11 @@ class ServerProxy(object): self.name = name self.success_codes = SUCCESS_CODES self.auth = None + self.quantum_id = quantum_id if auth: self.auth = 'Basic ' + base64.encodestring(auth).strip() + @lockutils.synchronized('rest_call', 'bsn-', external=True) def rest_call(self, action, resource, data, headers): uri = self.base_uri + resource body = json.dumps(data) @@ -129,6 +149,8 @@ class ServerProxy(object): headers['Content-type'] = 'application/json' headers['Accept'] = 'application/json' headers['QuantumProxy-Agent'] = self.name + headers['Instance-ID'] = self.quantum_id + headers['Orchestration-Service-ID'] = ORCHESTRATION_SERVICE_ID if self.auth: headers['Authorization'] = self.auth @@ -180,20 +202,21 @@ class ServerProxy(object): class ServerPool(object): - def __init__(self, servers, ssl, auth, timeout=10, + def __init__(self, servers, ssl, auth, quantum_id, 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.quantum_id = quantum_id 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) + return ServerProxy(server, port, self.ssl, self.auth, self.quantum_id, + self.timeout, self.base_uri, self.name) def server_failure(self, resp): """Define failure codes as required. @@ -254,7 +277,10 @@ class RpcProxy(dhcp_rpc_base.DhcpRpcCallbackMixin): return q_rpc.PluginRpcDispatcher([self]) -class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): +class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2, + l3_db.L3_NAT_db_mixin): + + supported_extension_aliases = ["router"] def __init__(self): LOG.info(_('QuantumRestProxy: Starting plugin. Version=%s'), @@ -265,12 +291,14 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): # '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 + # till next failure). Use 'server_auth' 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 + server_auth = cfg.CONF.RESTPROXY.server_auth + server_ssl = cfg.CONF.RESTPROXY.server_ssl + sync_data = cfg.CONF.RESTPROXY.sync_data + timeout = cfg.CONF.RESTPROXY.server_timeout + quantum_id = cfg.CONF.RESTPROXY.quantum_id + self.add_meta_server_route = cfg.CONF.RESTPROXY.add_meta_server_route # validate config assert servers is not None, 'Servers not defined. Aborting plugin' @@ -279,8 +307,8 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): assert all(len(s) == 2 for s in servers), SYNTAX_ERROR_MESSAGE # init network ctrl connections - self.servers = ServerPool(servers, serverssl, serverauth, - timeout) + self.servers = ServerPool(servers, server_ssl, server_auth, quantum_id, + timeout, BASE_URI) # init dhcp support self.topic = topics.PLUGIN @@ -291,7 +319,7 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): fanout=False) # Consume from all consumers in a thread self.conn.consume_in_thread() - if syncdata: + if sync_data: self._send_all_data() LOG.debug(_("QuantumRestProxyV2: initialization done")) @@ -320,26 +348,25 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): LOG.debug(_("QuantumRestProxyV2: create_network() called")) + self._warn_on_state_status(network['network']) + # 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) + session = context.session + with session.begin(subtransactions=True): + # create network in DB + new_net = super(QuantumRestProxyV2, self).create_network(context, + network) + self._process_l3_create(context, network['network'], new_net['id']) + self._extend_network_dict_l3(context, new_net) - # create on networl ctrl + # create network on the network controller try: resource = NET_RESOURCE_PATH % tenant_id + mapped_network = self._get_mapped_network_with_subnets(new_net) data = { - "network": { - "id": new_net["id"], - "name": new_net["name"], - } + "network": mapped_network } ret = self.servers.post(resource, data) if not self.servers.action_success(ret): @@ -379,36 +406,28 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): 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)) + self._warn_on_state_status(network['network']) - # 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) + session = context.session + with session.begin(subtransactions=True): + orig_net = super(QuantumRestProxyV2, self).get_network(context, + net_id) + new_net = super(QuantumRestProxyV2, self).update_network(context, + net_id, + network) + self._process_l3_update(context, network['network'], net_id) + self._extend_network_dict_l3(context, new_net) # 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 + try: + self._send_update_network(new_net) + 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 @@ -430,6 +449,17 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): orig_net = super(QuantumRestProxyV2, self).get_network(context, net_id) tenant_id = orig_net["tenant_id"] + filter = {'network_id': [net_id]} + ports = self.get_ports(context, filters=filter) + + # check if there are any tenant owned ports in-use + auto_delete_port_owners = db_base_plugin_v2.AUTO_DELETE_PORT_OWNERS + only_auto_del = all(p['device_owner'] in auto_delete_port_owners + for p in ports) + + if not only_auto_del: + raise exceptions.NetworkInUse(net_id=net_id) + # delete from network ctrl. Remote error on delete is ignored try: resource = NETWORKS_PATH % (tenant_id, net_id) @@ -442,6 +472,7 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): except RemoteRestError as e: LOG.error(_("QuantumRestProxyV2: Unable to update remote " "network: %s"), e.message) + raise def create_port(self, context, port): """Create a port, which is a connection point of a device @@ -477,24 +508,28 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): net = super(QuantumRestProxyV2, self).get_network(context, new_port["network_id"]) + if self.add_meta_server_route: + if new_port['device_owner'] == 'network:dhcp': + destination = METADATA_SERVER_IP + '/32' + self._add_host_route(context, destination, new_port) + # create on networl ctrl try: resource = PORT_RESOURCE_PATH % (net["tenant_id"], net["id"]) + mapped_port = self._map_state_and_status(new_port) data = { - "port": { - "id": new_port["id"], - "state": "ACTIVE", - } + "port": mapped_port } 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"): + device_id = port["port"].get("device_id") + if device_id: self._plug_interface(context, net["tenant_id"], net["id"], - new_port["id"], new_port["id"] + "00") + new_port["id"], device_id) except RemoteRestError as e: LOG.error(_("QuantumRestProxyV2: Unable to create remote port: " "%s"), e.message) @@ -536,6 +571,8 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): """ LOG.debug(_("QuantumRestProxyV2: update_port() called")) + self._warn_on_state_status(port['port']) + # Validate Args orig_port = super(QuantumRestProxyV2, self).get_port(context, port_id) @@ -547,7 +584,8 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): try: resource = PORTS_PATH % (orig_port["tenant_id"], orig_port["network_id"], port_id) - data = {"port": new_port, } + mapped_port = self._map_state_and_status(new_port) + data = {"port": mapped_port} ret = self.servers.put(resource, data) if not self.servers.action_success(ret): raise RemoteRestError(ret[2]) @@ -557,10 +595,11 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): self._unplug_interface(context, orig_port["tenant_id"], orig_port["network_id"], orig_port["id"]) - if new_port.get("device_id"): + device_id = new_port.get("device_id") + if device_id: self._plug_interface(context, new_port["tenant_id"], new_port["network_id"], - new_port["id"], new_port["id"] + "00") + new_port["id"], device_id) except RemoteRestError as e: LOG.error(_("QuantumRestProxyV2: Unable to create remote port: " @@ -573,7 +612,7 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): # return new_port return new_port - def delete_port(self, context, port_id): + def delete_port(self, context, port_id, l3_port_check=True): """Delete a port. :param context: quantum api request context :param id: UUID representing the port to delete. @@ -586,6 +625,15 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): LOG.debug(_("QuantumRestProxyV2: delete_port() called")) + # if needed, check to see if this is a port owned by + # and l3-router. If so, we should prevent deletion. + if l3_port_check: + self.prevent_l3_port_deletion(context, port_id) + self.disassociate_floatingips(context, port_id) + + super(QuantumRestProxyV2, self).delete_port(context, port_id) + + def _delete_port(self, context, port_id): # Delete from DB port = super(QuantumRestProxyV2, self).get_port(context, port_id) @@ -600,12 +648,13 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): 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) + 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) + raise def _plug_interface(self, context, tenant_id, net_id, port_id, remote_interface_id): @@ -625,22 +674,6 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): 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": @@ -676,31 +709,326 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): LOG.error(_("QuantumRestProxyV2: Unable to update remote port: " "%s"), e.message) + def create_subnet(self, context, subnet): + LOG.debug(_("QuantumRestProxyV2: create_subnet() called")) + + self._warn_on_state_status(subnet['subnet']) + + # create subnet in DB + new_subnet = super(QuantumRestProxyV2, self).create_subnet(context, + subnet) + net_id = new_subnet['network_id'] + orig_net = super(QuantumRestProxyV2, self).get_network(context, + net_id) + # update network on network controller + try: + self._send_update_network(orig_net) + except RemoteRestError as e: + # rollback creation of subnet + super(QuantumRestProxyV2, self).delete_subnet(context, + subnet['id']) + raise + return new_subnet + + def update_subnet(self, context, id, subnet): + LOG.debug(_("QuantumRestProxyV2: update_subnet() called")) + + self._warn_on_state_status(subnet['subnet']) + + orig_subnet = super(QuantumRestProxyV2, self)._get_subnet(context, id) + + # update subnet in DB + new_subnet = super(QuantumRestProxyV2, self).update_subnet(context, id, + subnet) + net_id = new_subnet['network_id'] + orig_net = super(QuantumRestProxyV2, self).get_network(context, + net_id) + # update network on network controller + try: + self._send_update_network(orig_net) + except RemoteRestError as e: + # rollback updation of subnet + super(QuantumRestProxyV2, self).update_subnet(context, id, + orig_subnet) + raise + return new_subnet + + def delete_subnet(self, context, id): + LOG.debug(_("QuantumRestProxyV2: delete_subnet() called")) + orig_subnet = super(QuantumRestProxyV2, self).get_subnet(context, id) + net_id = orig_subnet['network_id'] + # delete subnet in DB + super(QuantumRestProxyV2, self).delete_subnet(context, id) + orig_net = super(QuantumRestProxyV2, self).get_network(context, + net_id) + # update network on network controller + try: + self._send_update_network(orig_net) + except RemoteRestError as e: + # TODO (Sumit): rollback deletion of subnet + raise + + def create_router(self, context, router): + LOG.debug(_("QuantumRestProxyV2: create_router() called")) + + self._warn_on_state_status(router['router']) + + tenant_id = self._get_tenant_id_for_create(context, router["router"]) + + # create router in DB + new_router = super(QuantumRestProxyV2, self).create_router(context, + router) + + # create router on the network controller + try: + resource = ROUTER_RESOURCE_PATH % tenant_id + mapped_router = self._map_state_and_status(new_router) + data = { + "router": mapped_router + } + 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 router: " + "%s"), e.message) + super(QuantumRestProxyV2, self).delete_router(context, + new_router['id']) + raise + + # return created router + return new_router + + def update_router(self, context, router_id, router): + + LOG.debug(_("QuantumRestProxyV2.update_router() called")) + + self._warn_on_state_status(router['router']) + + orig_router = super(QuantumRestProxyV2, self).get_router(context, + router_id) + tenant_id = orig_router["tenant_id"] + new_router = super(QuantumRestProxyV2, self).update_router(context, + router_id, + router) + + # update router on network controller + try: + resource = ROUTERS_PATH % (tenant_id, router_id) + mapped_router = self._map_state_and_status(new_router) + data = { + "router": mapped_router + } + 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 router: " + "%s"), e.message) + # reset router to original state + super(QuantumRestProxyV2, self).update_router(context, + router_id, + orig_router) + raise + + # return updated router + return new_router + + def delete_router(self, context, router_id): + LOG.debug(_("QuantumRestProxyV2: delete_router() called")) + + with context.session.begin(subtransactions=True): + orig_router = self._get_router(context, router_id) + tenant_id = orig_router["tenant_id"] + + # Ensure that the router is not used + router_filter = {'router_id': [router_id]} + fips = self.get_floatingips_count(context.elevated(), + filters=router_filter) + if fips: + raise l3.RouterInUse(router_id=router_id) + + device_owner = l3_db.DEVICE_OWNER_ROUTER_INTF + device_filter = {'device_id': [router_id], + 'device_owner': [device_owner]} + ports = self.get_ports_count(context.elevated(), + filters=device_filter) + if ports: + raise l3.RouterInUse(router_id=router_id) + + # delete from network ctrl. Remote error on delete is ignored + try: + resource = ROUTERS_PATH % (tenant_id, router_id) + ret = self.servers.delete(resource) + if not self.servers.action_success(ret): + raise RemoteRestError(ret[2]) + ret_val = super(QuantumRestProxyV2, self).delete_router(context, + router_id) + return ret_val + except RemoteRestError as e: + LOG.error(_("QuantumRestProxyV2: Unable to delete remote router: " + "%s"), e.message) + raise + + def add_router_interface(self, context, router_id, interface_info): + + LOG.debug(_("QuantumRestProxyV2: add_router_interface() called")) + + # Validate args + router = self._get_router(context, router_id) + tenant_id = router['tenant_id'] + + # create interface in DB + new_interface_info = super(QuantumRestProxyV2, + self).add_router_interface(context, + router_id, + interface_info) + port = self._get_port(context, new_interface_info['port_id']) + net_id = port['network_id'] + subnet_id = new_interface_info['subnet_id'] + # we will use the port's network id as interface's id + interface_id = net_id + intf_details = self._get_router_intf_details(context, + interface_id, + subnet_id) + + # create interface on the network controller + try: + resource = ROUTER_INTF_OP_PATH % (tenant_id, router_id) + data = {"interface": intf_details} + 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 interface: " + "%s"), e.message) + super(QuantumRestProxyV2, + self).remove_router_interface(context, router_id, + interface_info) + raise + + return new_interface_info + + def remove_router_interface(self, context, router_id, interface_info): + + LOG.debug(_("QuantumRestProxyV2: remove_router_interface() called")) + + # Validate args + router = self._get_router(context, router_id) + tenant_id = router['tenant_id'] + + # we will first get the interface identifier before deleting in the DB + if not interface_info: + msg = "Either subnet_id or port_id must be specified" + raise exceptions.BadRequest(resource='router', msg=msg) + if 'port_id' in interface_info: + port = self._get_port(context, interface_info['port_id']) + interface_id = port['network_id'] + elif 'subnet_id' in interface_info: + subnet = self._get_subnet(context, interface_info['subnet_id']) + interface_id = subnet['network_id'] + else: + msg = "Either subnet_id or port_id must be specified" + raise exceptions.BadRequest(resource='router', msg=msg) + + # remove router in DB + del_intf_info = super(QuantumRestProxyV2, + self).remove_router_interface(context, + router_id, + interface_info) + + # create router on the network controller + try: + resource = ROUTER_INTF_PATH % (tenant_id, router_id, interface_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 delete remote intf: " + "%s"), e.message) + raise + + # return new interface + return del_intf_info + + def create_floatingip(self, context, floatingip): + LOG.debug(_("QuantumRestProxyV2: create_floatingip() called")) + + # create floatingip in DB + new_fl_ip = super(QuantumRestProxyV2, + self).create_floatingip(context, floatingip) + + net_id = new_fl_ip['floating_network_id'] + orig_net = super(QuantumRestProxyV2, self).get_network(context, + net_id) + # create floatingip on the network controller + try: + self._send_update_network(orig_net) + except RemoteRestError as e: + LOG.error(_("QuantumRestProxyV2: Unable to create remote " + "floatin IP: %s"), e.message) + super(QuantumRestProxyV2, self).delete_floatingip(context, + floatingip) + raise + + # return created floating IP + return new_fl_ip + + def update_floatingip(self, context, id, floatingip): + LOG.debug(_("QuantumRestProxyV2: update_floatingip() called")) + + orig_fl_ip = super(QuantumRestProxyV2, self).get_floatingip(context, + id) + + # update floatingip in DB + new_fl_ip = super(QuantumRestProxyV2, + self).update_floatingip(context, id, floatingip) + + net_id = new_fl_ip['floating_network_id'] + orig_net = super(QuantumRestProxyV2, self).get_network(context, + net_id) + # update network on network controller + try: + self._send_update_network(orig_net) + except RemoteRestError as e: + # rollback updation of subnet + super(QuantumRestProxyV2, self).update_floatingip(context, id, + orig_fl_ip) + raise + return new_fl_ip + + def delete_floatingip(self, context, id): + LOG.debug(_("QuantumRestProxyV2: delete_floatingip() called")) + + orig_fl_ip = super(QuantumRestProxyV2, self).get_floatingip(context, + id) + # delete floating IP in DB + net_id = orig_fl_ip['floating_network_id'] + super(QuantumRestProxyV2, self).delete_floatingip(context, id) + + orig_net = super(QuantumRestProxyV2, self).get_network(context, + net_id) + # update network on network controller + try: + self._send_update_network(orig_net) + except RemoteRestError as e: + # TODO(Sumit): rollback deletion of floating IP + raise + 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 = {} + networks = [] + routers = [] 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 + mapped_network = self._get_mapped_network_with_subnets(net) + net_fl_ips = self._get_network_with_floatingips(mapped_network) ports = [] net_filter = {'network_id': [net.get('id')]} @@ -708,28 +1036,166 @@ class QuantumRestProxyV2(db_base_plugin_v2.QuantumDbPluginV2): 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 + mapped_port = self._map_state_and_status(port) + mapped_port['attachment'] = { + 'id': port.get('device_id'), + 'mac': port.get('mac_address'), } - ports.append(port_details) - networks[net.get('id')]['ports'] = ports + ports.append(mapped_port) + net_fl_ips['ports'] = ports + + networks.append(net_fl_ips) + + all_routers = super(QuantumRestProxyV2, + self).get_routers(admin_context) or [] + for router in all_routers: + interfaces = [] + mapped_router = self._map_state_and_status(router) + router_filter = { + 'device_owner': ["network:router_interface"], + 'device_id': [router.get('id')] + } + router_ports = super(QuantumRestProxyV2, + self).get_ports(admin_context, + filters=router_filter) or [] + for port in router_ports: + net_id = port.get('network_id') + subnet_id = port['fixed_ips'][0]['subnet_id'] + intf_details = self._get_router_intf_details(admin_context, + net_id, + subnet_id) + interfaces.append(intf_details) + mapped_router['interfaces'] = interfaces + + routers.append(mapped_router) + try: resource = '/topology' data = { 'networks': networks, + 'routers': routers, } 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) + LOG.error(_('QuantumRestProxy: Unable to update remote ' + 'topology: %s'), e.message) raise + + def _add_host_route(self, context, destination, port): + subnet = {} + for fixed_ip in port['fixed_ips']: + subnet_id = fixed_ip['subnet_id'] + nexthop = fixed_ip['ip_address'] + subnet['host_routes'] = [{'destination': destination, + 'nexthop': nexthop}] + self.update_subnet(context, subnet_id, {'subnet': subnet}) + LOG.debug(_("Adding host route: ")) + LOG.debug(_("destination:%s nexthop:%s"), (destination, + nexthop)) + + def _get_network_with_floatingips(self, network): + admin_context = qcontext.get_admin_context() + + net_id = network['id'] + net_filter = {'floating_network_id': [net_id]} + fl_ips = super(QuantumRestProxyV2, + self).get_floatingips(admin_context, + filters=net_filter) or [] + network['floatingips'] = fl_ips + + return network + + def _get_all_subnets_json_for_network(self, net_id): + admin_context = qcontext.get_admin_context() + subnets = self._get_subnets_by_network(admin_context, + net_id) + subnets_details = [] + if subnets: + for subnet in subnets: + subnet_dict = self._make_subnet_dict(subnet) + mapped_subnet = self._map_state_and_status(subnet_dict) + subnets_details.append(mapped_subnet) + + return subnets_details + + def _get_mapped_network_with_subnets(self, network): + admin_context = qcontext.get_admin_context() + network = self._map_state_and_status(network) + subnets = self._get_all_subnets_json_for_network(network['id']) + network['subnets'] = subnets + for subnet in (subnets or []): + if subnet['gateway_ip']: + # FIX: For backward compatibility with wire protocol + network['gateway'] = subnet['gateway_ip'] + break + else: + network['gateway'] = '' + + network[l3.EXTERNAL] = self._network_is_external(admin_context, + network['id']) + + return network + + def _send_update_network(self, network): + net_id = network['id'] + tenant_id = network['tenant_id'] + # update network on network controller + try: + resource = NETWORKS_PATH % (tenant_id, net_id) + mapped_network = self._get_mapped_network_with_subnets(network) + net_fl_ips = self._get_network_with_floatingips(mapped_network) + data = { + "network": net_fl_ips, + } + 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 _map_state_and_status(self, resource): + resource = copy.copy(resource) + + resource['state'] = ('UP' if resource.pop('admin_state_up', + True) else 'DOWN') + + if 'status' in resource: + del resource['status'] + + return resource + + def _warn_on_state_status(self, resource): + if resource.get('admin_state_up', True) is False: + LOG.warning(_("Setting admin_state_up=False is not supported" + " in this plugin version. Ignoring setting for " + "resource: %s"), resource) + + if 'status' in resource: + if resource['status'] is not const.NET_STATUS_ACTIVE: + LOG.warning(_("Operational status is internally set by the" + " plugin. Ignoring setting status=%s."), + resource['status']) + + def _get_router_intf_details(self, context, intf_id, subnet_id): + + # we will use the network id as interface's id + net_id = intf_id + network = super(QuantumRestProxyV2, self).get_network(context, + net_id) + subnet = super(QuantumRestProxyV2, self).get_subnet(context, + subnet_id) + mapped_network = self._get_mapped_network_with_subnets(network) + mapped_subnet = self._map_state_and_status(subnet) + + data = { + 'id': intf_id, + "network": mapped_network, + "subnet": mapped_subnet + } + + return data diff --git a/quantum/plugins/bigswitch/vcsversion.py b/quantum/plugins/bigswitch/vcsversion.py new file mode 100644 index 00000000000..651594179e0 --- /dev/null +++ b/quantum/plugins/bigswitch/vcsversion.py @@ -0,0 +1,27 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 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: Sumit Naiksatam, sumitnaiksatam@gmail.com +# +version_info = {'branch_nick': u'quantum/trunk', + 'revision_id': u'1', + 'revno': 0} + + +QUANTUMRESTPROXY_VERSION = ['2013', '1', None] + + +FINAL = False # This becomes true at Release Candidate time diff --git a/quantum/plugins/bigswitch/version.py b/quantum/plugins/bigswitch/version.py index 2a441ed1fbe..711d1052bba 100755 --- a/quantum/plugins/bigswitch/version.py +++ b/quantum/plugins/bigswitch/version.py @@ -23,16 +23,22 @@ # if vcsversion exists, use it. Else, use LOCALBRANCH:LOCALREVISION try: - from bigswitch.vcsversion import version_info + from quantum.plugins.bigswitch.vcsversion import version_info except ImportError: version_info = {'branch_nick': u'LOCALBRANCH', 'revision_id': u'LOCALREVISION', 'revno': 0} +try: + from quantum.plugins.bigswitch.vcsversion import QUANTUMRESTPROXY_VERSION +except ImportError: + QUANTUMRESTPROXY_VERSION = ['2013', '1', None] +try: + from quantum.plugins.bigswitch.vcsversion import FINAL +except ImportError: + FINAL = False # This becomes true at Release Candidate time -QUANTUMRESTPROXY_VERSION = ['2012', '1', None] YEAR, COUNT, REVISION = QUANTUMRESTPROXY_VERSION -FINAL = False # This becomes true at Release Candidate time def canonical_version_string(): diff --git a/quantum/tests/unit/bigswitch/test_router_db.py b/quantum/tests/unit/bigswitch/test_router_db.py new file mode 100644 index 00000000000..e829eb85de6 --- /dev/null +++ b/quantum/tests/unit/bigswitch/test_router_db.py @@ -0,0 +1,317 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. +# +# Adapted from quantum.tests.unit.test_l3_plugin +# @author: Sumit Naiksatam, sumitnaiksatam@gmail.com +# + +import os + +from mock import patch +from webob import exc + +from quantum.common.test_lib import test_config +from quantum.extensions import l3 +from quantum.manager import QuantumManager +from quantum.openstack.common import cfg +from quantum.openstack.common.notifier import api as notifier_api +from quantum.openstack.common.notifier import test_notifier +from quantum.tests.unit import test_l3_plugin + + +def new_L3_setUp(self): + test_config['plugin_name_v2'] = ( + 'quantum.plugins.bigswitch.plugin.QuantumRestProxyV2') + etc_path = os.path.join(os.path.dirname(__file__), 'etc') + rp_conf_file = os.path.join(etc_path, 'restproxy.ini.test') + test_config['config_files'] = [rp_conf_file] + cfg.CONF.set_default('allow_overlapping_ips', False) + ext_mgr = L3TestExtensionManager() + test_config['extension_manager'] = ext_mgr + super(test_l3_plugin.L3NatDBTestCase, self).setUp() + + # Set to None to reload the drivers + notifier_api._drivers = None + cfg.CONF.set_override("notification_driver", [test_notifier.__name__]) + + +origSetUp = test_l3_plugin.L3NatDBTestCase.setUp + + +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 L3TestExtensionManager(object): + + def get_resources(self): + return l3.L3.get_resources() + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class RouterDBTestCase(test_l3_plugin.L3NatDBTestCase): + + def setUp(self): + self.httpPatch = patch('httplib.HTTPConnection', create=True, + new=HTTPConnectionMock) + self.httpPatch.start() + test_l3_plugin.L3NatDBTestCase.setUp = new_L3_setUp + super(RouterDBTestCase, self).setUp() + self.plugin_obj = QuantumManager.get_plugin() + + def tearDown(self): + self.httpPatch.stop() + super(RouterDBTestCase, self).tearDown() + del test_config['plugin_name_v2'] + del test_config['config_files'] + cfg.CONF.reset() + test_l3_plugin.L3NatDBTestCase.setUp = origSetUp + + def test_router_remove_router_interface_wrong_subnet_returns_409(self): + with self.router() as r: + with self.subnet() as s: + with self.subnet(cidr='10.0.10.0/24') as s1: + with self.port(subnet=s1, no_delete=True) as p: + self._router_interface_action('add', + r['router']['id'], + None, + p['port']['id']) + self._router_interface_action('remove', + r['router']['id'], + s['subnet']['id'], + p['port']['id'], + exc.HTTPConflict.code) + #remove properly to clean-up + self._router_interface_action('remove', + r['router']['id'], + None, + p['port']['id']) + + def test_router_remove_router_interface_wrong_port_returns_404(self): + with self.router() as r: + with self.subnet() as s: + with self.port(subnet=s, no_delete=True) as p: + self._router_interface_action('add', + r['router']['id'], + None, + p['port']['id']) + # create another port for testing failure case + res = self._create_port('json', p['port']['network_id']) + p2 = self.deserialize('json', res) + self._router_interface_action('remove', + r['router']['id'], + None, + p2['port']['id'], + exc.HTTPNotFound.code) + # remove correct interface to cleanup + self._router_interface_action('remove', + r['router']['id'], + None, + p['port']['id']) + # remove extra port created + self._delete('ports', p2['port']['id']) + + def test_create_floatingip_no_ext_gateway_return_404(self): + with self.subnet(cidr='10.0.10.0/24') as public_sub: + self._set_net_external(public_sub['subnet']['network_id']) + with self.port() as private_port: + with self.router() as r: + res = self._create_floatingip( + 'json', + public_sub['subnet']['network_id'], + port_id=private_port['port']['id']) + self.assertEqual(res.status_int, exc.HTTPNotFound.code) + + def test_router_update_gateway(self): + with self.router() as r: + with self.subnet() as s1: + with self.subnet(cidr='10.0.10.0/24') as s2: + self._set_net_external(s1['subnet']['network_id']) + self._add_external_gateway_to_router( + r['router']['id'], + s1['subnet']['network_id']) + body = self._show('routers', r['router']['id']) + net_id = (body['router'] + ['external_gateway_info']['network_id']) + self.assertEquals(net_id, s1['subnet']['network_id']) + self._set_net_external(s2['subnet']['network_id']) + self._add_external_gateway_to_router( + r['router']['id'], + s2['subnet']['network_id']) + body = self._show('routers', r['router']['id']) + net_id = (body['router'] + ['external_gateway_info']['network_id']) + self.assertEquals(net_id, s2['subnet']['network_id']) + self._remove_external_gateway_from_router( + r['router']['id'], + s2['subnet']['network_id']) + + def test_router_add_interface_overlapped_cidr(self): + self.skipTest("Plugin does not support") + + def test_router_add_interface_overlapped_cidr_returns_400(self): + self.skipTest("Plugin does not support") + + def test_list_nets_external(self): + self.skipTest("Plugin does not support") + + def test_router_update_gateway_with_existed_floatingip(self): + with self.subnet(cidr='10.0.10.0/24') as subnet: + self._set_net_external(subnet['subnet']['network_id']) + with self.floatingip_with_assoc() as fip: + self._add_external_gateway_to_router( + fip['floatingip']['router_id'], + subnet['subnet']['network_id'], + expected_code=exc.HTTPConflict.code) + + def test_router_remove_interface_wrong_subnet_returns_409(self): + with self.router() as r: + with self.subnet(cidr='10.0.10.0/24') as s: + with self.port(no_delete=True) as p: + self._router_interface_action('add', + r['router']['id'], + None, + p['port']['id']) + self._router_interface_action('remove', + r['router']['id'], + s['subnet']['id'], + p['port']['id'], + exc.HTTPConflict.code) + #remove properly to clean-up + self._router_interface_action('remove', + r['router']['id'], + None, + p['port']['id']) + + def test_router_remove_interface_wrong_port_returns_404(self): + with self.router() as r: + with self.subnet(cidr='10.0.10.0/24') as s: + with self.port(no_delete=True) as p: + self._router_interface_action('add', + r['router']['id'], + None, + p['port']['id']) + # create another port for testing failure case + res = self._create_port('json', p['port']['network_id']) + p2 = self.deserialize('json', res) + self._router_interface_action('remove', + r['router']['id'], + None, + p2['port']['id'], + exc.HTTPNotFound.code) + # remove correct interface to cleanup + self._router_interface_action('remove', + r['router']['id'], + None, + p['port']['id']) + # remove extra port created + self._delete('ports', p2['port']['id']) + + def test_send_data(self): + fmt = 'json' + plugin_obj = QuantumManager.get_plugin() + + with self.router() as r: + r_id = r['router']['id'] + + with self.subnet(cidr='10.0.10.0/24') as s: + s_id = s['subnet']['id'] + + with self.router() as r1: + r1_id = r1['router']['id'] + body = self._router_interface_action('add', r_id, s_id, + None) + self.assertTrue('port_id' in body) + r_port_id = body['port_id'] + body = self._show('ports', r_port_id) + self.assertEquals(body['port']['device_id'], r_id) + + with self.subnet(cidr='10.0.20.0/24') as s1: + s1_id = s1['subnet']['id'] + body = self._router_interface_action('add', r1_id, + s1_id, None) + self.assertTrue('port_id' in body) + r1_port_id = body['port_id'] + body = self._show('ports', r1_port_id) + self.assertEquals(body['port']['device_id'], r1_id) + + with self.subnet(cidr='11.0.0.0/24') as public_sub: + public_net_id = public_sub['subnet']['network_id'] + self._set_net_external(public_net_id) + + with self.port() as prv_port: + prv_fixed_ip = prv_port['port']['fixed_ips'][0] + priv_sub_id = prv_fixed_ip['subnet_id'] + self._add_external_gateway_to_router( + r_id, public_net_id) + self._router_interface_action('add', r_id, + priv_sub_id, + None) + + priv_port_id = prv_port['port']['id'] + res = self._create_floatingip( + fmt, public_net_id, + port_id=priv_port_id) + self.assertEqual(res.status_int, + exc.HTTPCreated.code) + floatingip = self.deserialize(fmt, res) + + result = plugin_obj._send_all_data() + self.assertEquals(result[0], 200) + + self._delete('floatingips', + floatingip['floatingip']['id']) + self._remove_external_gateway_from_router( + r_id, public_net_id) + self._router_interface_action('remove', r_id, + priv_sub_id, + None) + self._router_interface_action('remove', r_id, s_id, + None) + self._show('ports', r_port_id, + expected_code=exc.HTTPNotFound.code) + self._router_interface_action('remove', r1_id, s1_id, + None) + self._show('ports', r1_port_id, + expected_code=exc.HTTPNotFound.code)