From d4c3047ba4d46b3b6fe56940832e6e1142c2e441 Mon Sep 17 00:00:00 2001 From: Ivar Lazzaro Date: Mon, 22 Jul 2013 13:32:31 -0700 Subject: [PATCH] Embrane Neutron Plugin Implements blueprint embrane-neutron-plugin This commit implements the main Embrane plugin, which covers route and extratoute extensions. The plugin will rely on existing plugins for leveraging L2 networks, for now it only supports OpenVSwitch and vlan networks, but more supports are to come if the model is approved (or a different one is suggested). Change-Id: I90aff8ec4324bd3a7c59c411374db6a118d1a72b --- etc/neutron/plugins/embrane/heleos_conf.ini | 41 ++ neutron/plugins/embrane/README | 9 + neutron/plugins/embrane/__init__.py | 18 + neutron/plugins/embrane/agent/__init__.py | 18 + neutron/plugins/embrane/agent/dispatcher.py | 145 +++++++ .../embrane/agent/operations/__init__.py | 18 + .../agent/operations/router_operations.py | 154 ++++++++ neutron/plugins/embrane/base_plugin.py | 364 ++++++++++++++++++ neutron/plugins/embrane/common/__init__.py | 18 + neutron/plugins/embrane/common/config.py | 50 +++ neutron/plugins/embrane/common/constants.py | 84 ++++ neutron/plugins/embrane/common/contexts.py | 40 ++ neutron/plugins/embrane/common/exceptions.py | 35 ++ neutron/plugins/embrane/common/operation.py | 51 +++ neutron/plugins/embrane/common/utils.py | 65 ++++ neutron/plugins/embrane/l2base/__init__.py | 18 + .../plugins/embrane/l2base/fake/__init__.py | 18 + .../embrane/l2base/fake/fake_l2_plugin.py | 24 ++ .../embrane/l2base/fake/fakeplugin_support.py | 43 +++ .../embrane/l2base/openvswitch/__init__.py | 18 + .../l2base/openvswitch/openvswitch_support.py | 56 +++ .../plugins/embrane/l2base/support_base.py | 48 +++ .../embrane/l2base/support_exceptions.py | 25 ++ neutron/plugins/embrane/plugins/__init__.py | 18 + .../embrane/plugins/embrane_fake_plugin.py | 34 ++ .../embrane/plugins/embrane_ovs_plugin.py | 37 ++ neutron/tests/unit/embrane/__init__.py | 18 + .../unit/embrane/test_embrane_defaults.py | 37 ++ .../unit/embrane/test_embrane_l3_plugin.py | 46 +++ .../embrane/test_embrane_neutron_plugin.py | 81 ++++ 30 files changed, 1631 insertions(+) create mode 100644 etc/neutron/plugins/embrane/heleos_conf.ini create mode 100644 neutron/plugins/embrane/README create mode 100644 neutron/plugins/embrane/__init__.py create mode 100644 neutron/plugins/embrane/agent/__init__.py create mode 100644 neutron/plugins/embrane/agent/dispatcher.py create mode 100644 neutron/plugins/embrane/agent/operations/__init__.py create mode 100644 neutron/plugins/embrane/agent/operations/router_operations.py create mode 100644 neutron/plugins/embrane/base_plugin.py create mode 100644 neutron/plugins/embrane/common/__init__.py create mode 100644 neutron/plugins/embrane/common/config.py create mode 100644 neutron/plugins/embrane/common/constants.py create mode 100644 neutron/plugins/embrane/common/contexts.py create mode 100644 neutron/plugins/embrane/common/exceptions.py create mode 100644 neutron/plugins/embrane/common/operation.py create mode 100644 neutron/plugins/embrane/common/utils.py create mode 100644 neutron/plugins/embrane/l2base/__init__.py create mode 100644 neutron/plugins/embrane/l2base/fake/__init__.py create mode 100644 neutron/plugins/embrane/l2base/fake/fake_l2_plugin.py create mode 100644 neutron/plugins/embrane/l2base/fake/fakeplugin_support.py create mode 100644 neutron/plugins/embrane/l2base/openvswitch/__init__.py create mode 100644 neutron/plugins/embrane/l2base/openvswitch/openvswitch_support.py create mode 100644 neutron/plugins/embrane/l2base/support_base.py create mode 100644 neutron/plugins/embrane/l2base/support_exceptions.py create mode 100644 neutron/plugins/embrane/plugins/__init__.py create mode 100644 neutron/plugins/embrane/plugins/embrane_fake_plugin.py create mode 100644 neutron/plugins/embrane/plugins/embrane_ovs_plugin.py create mode 100644 neutron/tests/unit/embrane/__init__.py create mode 100644 neutron/tests/unit/embrane/test_embrane_defaults.py create mode 100644 neutron/tests/unit/embrane/test_embrane_l3_plugin.py create mode 100644 neutron/tests/unit/embrane/test_embrane_neutron_plugin.py diff --git a/etc/neutron/plugins/embrane/heleos_conf.ini b/etc/neutron/plugins/embrane/heleos_conf.ini new file mode 100644 index 000000000..0ca9b46f8 --- /dev/null +++ b/etc/neutron/plugins/embrane/heleos_conf.ini @@ -0,0 +1,41 @@ +[heleos] +#configure the ESM management address +#in the first version of this plugin, only one ESM can be specified +#Example: +#esm_mgmt= + +#configure admin username and password +#admin_username= +#admin_password= + +#router image id +#Example: +#router_image=932ce713-e210-3d54-a0a5-518b0b5ee1b0 + +#mgmt shared security zone id +#defines the shared management security zone. Each tenant can have a private one configured through the ESM +#Example: +#mgmt_id=c0bc9b6c-f110-46cf-bb01-733bfe4b5a1a + +#in-band shared security zone id +#defines the shared in-band security zone. Each tenant can have a private one configured through the ESM +#Example: +#inband_id=a6b7999d-3806-4b04-81f6-e0c5c8271afc + +#oob-band shared security zone id +#defines the shared out-of-band security zone. Each tenant can have a private one configured through the ESM +#Example: +#oob_id=e7eda5cc-b977-46cb-9c14-cab43c1b7871 + +#dummy security zone id +#defines the dummy security zone ID. this security zone will be used by the DVAs with no neutron interfaces +#Example: +#dummy_utif_id=d9911310-25fc-4733-a2e0-c0eda024ef08 + +#resource pool id +#define the shared resource pool. Each tenant can have a private one configured through the ESM +#Example +#resource_pool_id= + +#define if the requests have to be executed asynchronously by the plugin or not +#async_requests= diff --git a/neutron/plugins/embrane/README b/neutron/plugins/embrane/README new file mode 100644 index 000000000..15ad1abbd --- /dev/null +++ b/neutron/plugins/embrane/README @@ -0,0 +1,9 @@ +Embrane Neutron Plugin + +This plugin interfaces OpenStack Neutron with Embrane's heleos platform, which +provides layer 3-7 network services for cloud environments. + +L2 connectivity is leveraged by one of the supported existing plugins. + +For more details on use, configuration and implementation please refer to: +http://wiki.openstack.org/wiki/Neutron/EmbraneNeutronPlugin \ No newline at end of file diff --git a/neutron/plugins/embrane/__init__.py b/neutron/plugins/embrane/__init__.py new file mode 100644 index 000000000..1fac4725b --- /dev/null +++ b/neutron/plugins/embrane/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. diff --git a/neutron/plugins/embrane/agent/__init__.py b/neutron/plugins/embrane/agent/__init__.py new file mode 100644 index 000000000..1fac4725b --- /dev/null +++ b/neutron/plugins/embrane/agent/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. diff --git a/neutron/plugins/embrane/agent/dispatcher.py b/neutron/plugins/embrane/agent/dispatcher.py new file mode 100644 index 000000000..8038b93e0 --- /dev/null +++ b/neutron/plugins/embrane/agent/dispatcher.py @@ -0,0 +1,145 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from eventlet import greenthread +from eventlet import queue +from heleosapi import constants as h_con +from heleosapi import exceptions as h_exc + +from neutron.openstack.common import log as logging +from neutron.plugins.embrane.agent.operations import router_operations +from neutron.plugins.embrane.common import constants as p_con +from neutron.plugins.embrane.common import contexts as ctx +from neutron.plugins.embrane.common import exceptions as plugin_exc + + +LOG = logging.getLogger(__name__) + + +def _validate_operation(event, status, item_id): + if status and event not in p_con.operation_filter[status]: + raise plugin_exc.StateConstraintException(operation=event, + dva_id=item_id, state=status) + + +class Dispatcher(object): + + def __init__(self, plugin, async=True): + self._async = async + self._plugin = plugin + self.sync_items = dict() + + def dispatch_l3(self, d_context, args=(), kwargs={}): + item = d_context.item + event = d_context.event + q_context = d_context.q_context + chain = d_context.chain + + item_id = item["id"] + # First round validation (Controller level) + _validate_operation(event, item["status"], item_id) + + handlers = router_operations.handlers + if event in handlers: + for f in handlers[event]: + first_run = False + if item_id not in self.sync_items: + self.sync_items[item_id] = queue.Queue() + first_run = True + self.sync_items[item_id].put( + ctx.OperationContext(event, q_context, item, chain, f, + args, kwargs)) + if first_run: + t = greenthread.spawn(self._consume_l3, + item_id, + self.sync_items[item_id], + self._plugin) + if not self._async: + t.wait() + + def _consume_l3(self, sync_item, sync_queue, plugin): + current_state = None + while True: + try: + # If the DVA is deleted, the thread (and the associated queue) + # can die as well + if current_state == p_con.Status.DELETED: + del self.sync_items[sync_item] + return + try: + operation_context = sync_queue.get( + timeout=p_con.QUEUE_TIMEOUT) + except queue.Empty: + del self.sync_items[sync_item] + return + # Second round validation (enqueued level) + _validate_operation(operation_context.event, + current_state, + operation_context.item["id"]) + # Execute the preliminary operations + (operation_context.chain and + operation_context.chain.execute_all()) + # Execute the main operation, a transient state is maintained + # so that the consumer can decide if it has + # to be burned to the DB + transient_state = None + try: + dva_state = operation_context.function( + plugin._esm_api, + operation_context.q_context.tenant_id, + operation_context.item, + *operation_context.args, + **operation_context.kwargs) + if dva_state == p_con.Status.DELETED: + transient_state = dva_state + else: + if not dva_state: + transient_state = p_con.Status.ERROR + elif dva_state == h_con.DvaState.POWER_ON: + transient_state = p_con.Status.ACTIVE + else: + transient_state = p_con.Status.READY + + except (h_exc.PendingDva, h_exc.DvaNotFound, + h_exc.BrokenInterface, h_exc.DvaCreationFailed, + h_exc.DvaCreationPending, h_exc.BrokenDva, + h_exc.ConfigurationFailed) as ex: + LOG.warning(p_con.error_map[type(ex)] % ex.message) + transient_state = p_con.Status.ERROR + except h_exc.DvaDeleteFailed as ex: + LOG.warning(p_con.error_map[type(ex)] % ex.message) + transient_state = p_con.Status.DELETED + finally: + # if the returned transient state is None, no operations + # are required on the DVA status + if transient_state: + if transient_state == p_con.Status.DELETED: + current_state = plugin._delete_router( + operation_context.q_context, + operation_context.item["id"]) + # Error state cannot be reverted + elif current_state != p_con.Status.ERROR: + current_state = plugin._update_neutron_state( + operation_context.q_context, + operation_context.item, + transient_state) + except plugin_exc.StateConstraintException as e: + LOG.error(_("%s"), e.message) + except Exception: + LOG.exception(_("Unhandled exception occurred")) diff --git a/neutron/plugins/embrane/agent/operations/__init__.py b/neutron/plugins/embrane/agent/operations/__init__.py new file mode 100644 index 000000000..1fac4725b --- /dev/null +++ b/neutron/plugins/embrane/agent/operations/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. diff --git a/neutron/plugins/embrane/agent/operations/router_operations.py b/neutron/plugins/embrane/agent/operations/router_operations.py new file mode 100644 index 000000000..d460bc47e --- /dev/null +++ b/neutron/plugins/embrane/agent/operations/router_operations.py @@ -0,0 +1,154 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from functools import wraps + +from heleosapi import exceptions as h_exc + +from neutron.openstack.common import log as logging +from neutron.plugins.embrane.common import constants as p_con + +LOG = logging.getLogger(__name__) +handlers = dict() + + +def handler(event, handler): + def wrap(f): + if event not in handler.keys(): + new_func_list = [f] + handler[event] = new_func_list + else: + handler[event].append(f) + + @wraps(f) + def wrapped_f(*args, **kwargs): + return f(*args, **kwargs) + return wrapped_f + return wrap + + +@handler(p_con.Events.CREATE_ROUTER, handlers) +def _create_dva_and_assign_address(api, tenant_id, neutron_router, + flavor, utif_info=None, + ip_allocation_info=None): + """Creates a new router, and assign the gateway interface if any.""" + + dva = api.create_router(tenant_id=tenant_id, + router_id=neutron_router["id"], + name=neutron_router["name"], + flavor=flavor, + up=neutron_router["admin_state_up"]) + try: + if utif_info: + api.grow_interface(utif_info, neutron_router["admin_state_up"], + tenant_id, neutron_router["id"]) + if ip_allocation_info: + dva = api.allocate_address(neutron_router["id"], + neutron_router["admin_state_up"], + ip_allocation_info) + except h_exc.PreliminaryOperationsFailed as ex: + raise h_exc.BrokenInterface(err_msg=ex.message) + + state = api.extract_dva_state(dva) + return state + + +@handler(p_con.Events.UPDATE_ROUTER, handlers) +def _update_dva_and_assign_address(api, tenant_id, neutron_router, + utif_info=None, ip_allocation_info=None, + routes_info=[]): + name = neutron_router["name"] + up = neutron_router["admin_state_up"] + r_id = neutron_router["id"] + if ip_allocation_info or routes_info: + up = True + dva = api.update_dva(tenant_id=tenant_id, router_id=r_id, name=name, + up=up, utif_info=utif_info) + if ip_allocation_info: + api.allocate_address(r_id, up, ip_allocation_info) + + if routes_info: + api.delete_extra_routes(r_id, up) + api.set_extra_routes(r_id, neutron_router["admin_state_up"], + routes_info) + + return api.extract_dva_state(dva) + + +@handler(p_con.Events.DELETE_ROUTER, handlers) +def _delete_dva(api, tenant_id, neutron_router): + try: + api.delete_dva(tenant_id, neutron_router["id"]) + except h_exc.DvaNotFound: + LOG.warning(_("The router %s had no physical representation," + "likely already deleted"), neutron_router["id"]) + return p_con.Status.DELETED + + +@handler(p_con.Events.GROW_ROUTER_IF, handlers) +def _grow_dva_iface_and_assign_address(api, tenant_id, neutron_router, + utif_info=None, + ip_allocation_info=None): + try: + dva = api.grow_interface(utif_info, neutron_router["admin_state_up"], + tenant_id, neutron_router["id"]) + if ip_allocation_info: + dva = api.allocate_address(neutron_router["id"], + neutron_router["admin_state_up"], + ip_allocation_info) + except h_exc.PreliminaryOperationsFailed as ex: + raise h_exc.BrokenInterface(err_msg=ex.message) + + state = api.extract_dva_state(dva) + return state + + +@handler(p_con.Events.SHRINK_ROUTER_IF, handlers) +def _shrink_dva_iface(api, tenant_id, neutron_router, port_id): + try: + dva = api.shrink_interface(tenant_id, neutron_router["id"], + neutron_router["admin_state_up"], port_id) + except h_exc.InterfaceNotFound: + LOG.warning(_("Interface %s not found in the heleos back-end," + "likely already deleted"), port_id) + except h_exc.PreliminaryOperationsFailed as ex: + raise h_exc.BrokenInterface(err_msg=ex.message) + state = api.extract_dva_state(dva) + return state + + +@handler(p_con.Events.SET_NAT_RULE, handlers) +def _create_nat_rule(api, tenant_id, neutron_router, nat_info=None): + + dva = api.create_nat_entry(neutron_router["id"], + neutron_router["admin_state_up"], nat_info) + + state = api.extract_dva_state(dva) + return state + + +@handler(p_con.Events.RESET_NAT_RULE, handlers) +def _delete_nat_rule(api, tenant_id, neutron_router, floating_ip_id): + + dva = api.remove_nat_entry(neutron_router["id"], + neutron_router["admin_state_up"], + floating_ip_id) + + state = api.extract_dva_state(dva) + return state diff --git a/neutron/plugins/embrane/base_plugin.py b/neutron/plugins/embrane/base_plugin.py new file mode 100644 index 000000000..2aa981f01 --- /dev/null +++ b/neutron/plugins/embrane/base_plugin.py @@ -0,0 +1,364 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from heleosapi import backend_operations as h_op +from heleosapi import constants as h_con +from heleosapi import exceptions as h_exc +from oslo.config import cfg + +from neutron.common import constants as l3_constants +from neutron.common import exceptions as neutron_exc +from neutron.db import extraroute_db +from neutron.db import l3_db +from neutron.db import models_v2 +from neutron.extensions import l3 +from neutron.openstack.common import log as logging +from neutron.plugins.embrane.agent import dispatcher +from neutron.plugins.embrane.common import config # noqa +from neutron.plugins.embrane.common import constants as p_con +from neutron.plugins.embrane.common import contexts as embrane_ctx +from neutron.plugins.embrane.common import exceptions as c_exc +from neutron.plugins.embrane.common import operation +from neutron.plugins.embrane.common import utils + +LOG = logging.getLogger(__name__) +conf = cfg.CONF.heleos + + +class EmbranePlugin(object): + """Embrane Neutron plugin. + + uses the heleos(c) platform and a support L2 plugin to leverage networking + in cloud environments. + + """ + _l3super = extraroute_db.ExtraRoute_db_mixin + + def __init__(self): + pass + + def _run_embrane_config(self): + # read configurations + config_esm_mgmt = conf.esm_mgmt + config_admin_username = conf.admin_username + config_admin_password = conf.admin_password + config_router_image_id = conf.router_image + config_security_zones = {h_con.SzType.IB: conf.inband_id, + h_con.SzType.OOB: conf.oob_id, + h_con.SzType.MGMT: conf.mgmt_id, + h_con.SzType.DUMMY: conf.dummy_utif_id} + config_resource_pool = conf.resource_pool_id + self._embrane_async = conf.async_requests + self._esm_api = h_op.BackendOperations( + esm_mgmt=config_esm_mgmt, + admin_username=config_admin_username, + admin_password=config_admin_password, + router_image_id=config_router_image_id, + security_zones=config_security_zones, + resource_pool=config_resource_pool) + self._dispatcher = dispatcher.Dispatcher(self, self._embrane_async) + + def _make_router_dict(self, *args, **kwargs): + return self._l3super._make_router_dict(self, *args, **kwargs) + + def _delete_router(self, context, router_id): + self._l3super.delete_router(self, context, router_id) + + def _update_db_router_state(self, context, neutron_router, dva_state): + if not dva_state: + new_state = p_con.Status.ERROR + elif dva_state == h_con.DvaState.POWER_ON: + new_state = p_con.Status.ACTIVE + else: + new_state = p_con.Status.READY + self._set_db_router_state(context, neutron_router, new_state) + return new_state + + def _set_db_router_state(self, context, neutron_router, new_state): + return utils.set_db_item_state(context, neutron_router, new_state) + + def _update_db_interfaces_state(self, context, neutron_router): + router_ports = self.get_ports(context, + {"device_id": [neutron_router["id"]]}) + self._esm_api.update_ports_status(neutron_router["id"], router_ports) + for port in router_ports: + db_port = self._get_port(context, port["id"]) + db_port["status"] = port["status"] + context.session.merge(db_port) + + def _update_neutron_state(self, context, neutron_router, state): + try: + self._update_db_interfaces_state(context, neutron_router) + except Exception: + LOG.exception(_("Unhandled exception occurred")) + return self._set_db_router_state(context, neutron_router, state) + + def _retrieve_prefix_from_port(self, context, neutron_port): + subnet_id = neutron_port["fixed_ips"][0]["subnet_id"] + subnet = self._get_subnet(context, subnet_id) + prefix = subnet["cidr"].split("/")[1] + return prefix + + # L3 extension + def create_router(self, context, router): + r = router["router"] + self._get_tenant_id_for_create(context, r) + with context.session.begin(subtransactions=True): + neutron_router = self._l3super.create_router(self, context, router) + network_id = None + gw_port = None + ext_gw_info = neutron_router.get(l3_db.EXTERNAL_GW_INFO) + if ext_gw_info: + network_id = ext_gw_info.get("network_id") + if network_id: + gw_ports = self.get_ports( + context, + {"device_id": [id], + "device_owner": ["network:router_gateway"]}) + if len(gw_ports) != 1: + raise c_exc.EmbranePluginException( + err_msg=_("there must be only one gateway port " + "per router at once")) + gw_port = gw_ports[0] + + # For now, only small flavor is used + utif_info = (self._plugin_support.retrieve_utif_info(context, + gw_port) + if network_id else None) + ip_allocation_info = (utils.retrieve_ip_allocation_info(self, + context, + gw_port) + if network_id else None) + neutron_router = self._l3super._get_router(self, context, + neutron_router["id"]) + neutron_router["status"] = p_con.Status.CREATING + self._dispatcher.dispatch_l3( + d_context=embrane_ctx.DispatcherContext( + p_con.Events.CREATE_ROUTER, neutron_router, context, None), + args=(h_con.Flavor.SMALL, utif_info, ip_allocation_info)) + return self._make_router_dict(neutron_router) + + def update_router(self, context, id, router): + with context.session.begin(subtransactions=True): + db_router = self._l3super.update_router(self, context, id, router) + gw_port = None + ext_gw_info = db_router.get(l3_db.EXTERNAL_GW_INFO) + if ext_gw_info: + ext_gw_info = db_router[l3_db.EXTERNAL_GW_INFO] + network_id = (ext_gw_info.get("network_id") + if ext_gw_info else None) + if network_id: + gw_ports = self.get_ports( + context, + {"device_id": [id], + "device_owner": ["network:router_gateway"]}) + if len(gw_ports) != 1: + raise c_exc.EmbranePluginException( + err_msg=_("there must be only one gateway port" + " per router at once")) + gw_port = gw_ports[0] + + utif_info = (self._plugin_support.retrieve_utif_info(context, + gw_port) + if gw_port else None) + ip_allocation_info = (utils.retrieve_ip_allocation_info(self, + context, + gw_port) + if gw_port else None) + + routes_info = router["router"].get("routes") + + neutron_router = self._l3super._get_router(self, context, id) + state_change = operation.Operation( + self._set_db_router_state, + args=(context, neutron_router, p_con.Status.UPDATING)) + self._dispatcher.dispatch_l3( + d_context=embrane_ctx.DispatcherContext( + p_con.Events.UPDATE_ROUTER, neutron_router, context, + state_change), + args=(utif_info, ip_allocation_info, routes_info)) + return self._make_router_dict(neutron_router) + + def get_router(self, context, id, fields=None): + """Ensures that id does exist in the ESM.""" + neutron_router = self._get_router(context, id) + + try: + if neutron_router["status"] != p_con.Status.CREATING: + self._esm_api.get_dva(id) + except h_exc.DvaNotFound: + + LOG.error(_("The following routers have not physical match: %s"), + id) + self._set_db_router_state(context, neutron_router, + p_con.Status.ERROR) + + LOG.debug(_("Requested router: %s"), neutron_router) + return self._make_router_dict(neutron_router, fields) + + def get_routers(self, context, filters=None, fields=None, sorts=None, + limit=None, marker=None, page_reverse=False): + """Retrieves the router list defined by the incoming filters.""" + router_query = self._apply_filters_to_query( + self._model_query(context, l3_db.Router), + l3_db.Router, filters) + id_list = [x["id"] for x in router_query + if x["status"] != p_con.Status.CREATING] + try: + self._esm_api.get_dvas(id_list) + except h_exc.DvaNotFound: + LOG.error(_("The following routers have not physical match: %s"), + repr(id_list)) + error_routers = [] + for id in id_list: + try: + error_routers.append(self._get_router(context, id)) + except l3.RouterNotFound: + pass + for error_router in error_routers: + self._set_db_router_state(context, error_router, + p_con.Status.ERROR) + return [self._make_router_dict(router, fields) + for router in router_query] + + def delete_router(self, context, id): + """Deletes the DVA with the specific router id.""" + # Copy of the parent validation code, shouldn't the base modules + # provide functions for validating operations? + with context.session.begin(subtransactions=True): + DEVICE_OWNER_ROUTER_INTF = l3_constants.DEVICE_OWNER_ROUTER_INTF + fips = self.get_floatingips_count(context.elevated(), + filters={"router_id": [id]}) + if fips: + raise l3.RouterInUse(router_id=id) + + device_filter = {"device_id": [id], + "device_owner": [DEVICE_OWNER_ROUTER_INTF]} + ports = self.get_ports_count(context.elevated(), + filters=device_filter) + if ports: + raise l3.RouterInUse(router_id=id) + neutron_router = self._get_router(context, id) + state_change = operation.Operation(self._set_db_router_state, + args=(context, neutron_router, + p_con.Status.DELETING)) + self._dispatcher.dispatch_l3( + d_context=embrane_ctx.DispatcherContext( + p_con.Events.DELETE_ROUTER, neutron_router, context, + state_change), args=()) + LOG.debug(_("Deleting router=%s"), neutron_router) + return neutron_router + + def add_router_interface(self, context, router_id, interface_info): + """Grows DVA interface in the specified subnet.""" + with context.session.begin(subtransactions=True): + neutron_router = self._get_router(context, router_id) + rport_qry = context.session.query(models_v2.Port) + ports = rport_qry.filter_by( + device_id=router_id).all() + if len(ports) >= p_con.UTIF_LIMIT: + raise neutron_exc.BadRequest( + resource=router_id, + msg=("this router doesn't support more than " + + str(p_con.UTIF_LIMIT) + " interfaces")) + neutron_router_iface = self._l3super.add_router_interface( + self, context, router_id, interface_info) + port = self._get_port(context, neutron_router_iface["port_id"]) + utif_info = self._plugin_support.retrieve_utif_info(context, port) + ip_allocation_info = utils.retrieve_ip_allocation_info(self, + context, + port) + state_change = operation.Operation(self._set_db_router_state, + args=(context, neutron_router, + p_con.Status.UPDATING)) + self._dispatcher.dispatch_l3( + d_context=embrane_ctx.DispatcherContext( + p_con.Events.GROW_ROUTER_IF, neutron_router, context, + state_change), + args=(utif_info, ip_allocation_info)) + return neutron_router_iface + + def remove_router_interface(self, context, router_id, interface_info): + port_id = None + if "port_id" in interface_info: + port_id = interface_info["port_id"] + elif "subnet_id" in interface_info: + subnet_id = interface_info["subnet_id"] + subnet = self._get_subnet(context, subnet_id) + rport_qry = context.session.query(models_v2.Port) + ports = rport_qry.filter_by( + device_id=router_id, + device_owner=l3_constants.DEVICE_OWNER_ROUTER_INTF, + network_id=subnet["network_id"]) + for p in ports: + if p["fixed_ips"][0]["subnet_id"] == subnet_id: + port_id = p["id"] + break + neutron_router = self._get_router(context, router_id) + self._l3super.remove_router_interface(self, context, router_id, + interface_info) + state_change = operation.Operation(self._set_db_router_state, + args=(context, neutron_router, + p_con.Status.UPDATING)) + self._dispatcher.dispatch_l3( + d_context=embrane_ctx.DispatcherContext( + p_con.Events.SHRINK_ROUTER_IF, neutron_router, context, + state_change), + args=(port_id,)) + + def update_floatingip(self, context, id, floatingip): + with context.session.begin(subtransactions=True): + db_fip = self._l3super.get_floatingip(self, context, id) + result = self._l3super.update_floatingip(self, context, id, + floatingip) + + if db_fip["port_id"]: + neutron_router = self._get_router(context, db_fip["router_id"]) + fip_id = db_fip["id"] + state_change = operation.Operation( + self._set_db_router_state, + args=(context, neutron_router, p_con.Status.UPDATING)) + + self._dispatcher.dispatch_l3( + d_context=embrane_ctx.DispatcherContext( + p_con.Events.RESET_NAT_RULE, neutron_router, context, + state_change), + args=(fip_id,)) + if floatingip["floatingip"]["port_id"]: + neutron_router = self._get_router(context, result["router_id"]) + db_fixed_port = self._get_port(context, result["port_id"]) + fixed_prefix = self._retrieve_prefix_from_port(context, + db_fixed_port) + db_floating_port = neutron_router["gw_port"] + floating_prefix = self._retrieve_prefix_from_port( + context, db_floating_port) + nat_info = utils.retrieve_nat_info(context, result, + fixed_prefix, + floating_prefix, + neutron_router) + state_change = operation.Operation( + self._set_db_router_state, + args=(context, neutron_router, p_con.Status.UPDATING)) + + self._dispatcher.dispatch_l3( + d_context=embrane_ctx.DispatcherContext( + p_con.Events.SET_NAT_RULE, neutron_router, context, + state_change), + args=(nat_info,)) + return result diff --git a/neutron/plugins/embrane/common/__init__.py b/neutron/plugins/embrane/common/__init__.py new file mode 100644 index 000000000..1fac4725b --- /dev/null +++ b/neutron/plugins/embrane/common/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. diff --git a/neutron/plugins/embrane/common/config.py b/neutron/plugins/embrane/common/config.py new file mode 100644 index 000000000..bbf825b9b --- /dev/null +++ b/neutron/plugins/embrane/common/config.py @@ -0,0 +1,50 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from oslo.config import cfg + + +heleos_opts = [ + cfg.StrOpt('esm_mgmt', + default=None, + help=_('ESM management root address')), + cfg.StrOpt('admin_username', default='admin', + help=_('ESM admin username.')), + cfg.StrOpt('admin_password', default=None, + secret=True, + help=_('ESM admin password.')), + cfg.StrOpt('router_image', default=None, + help=_('Router image id (Embrane FW/VPN)')), + cfg.StrOpt('inband_id', default=None, + help=_('In band Security Zone id')), + cfg.StrOpt('oob_id', default=None, + help=_('Out of band Security Zone id')), + cfg.StrOpt('mgmt_id', default=None, + help=_('Management Security Zone id')), + cfg.StrOpt('dummy_utif_id', default=None, + help=_('Dummy user traffic Security Zone id')), + cfg.StrOpt('resource_pool_id', default='default', + help=_('Shared resource pool id')), + cfg.BoolOpt('async_requests', default=True, + help=_('define if the requests have ' + 'run asynchronously or not')), +] + + +cfg.CONF.register_opts(heleos_opts, "heleos") diff --git a/neutron/plugins/embrane/common/constants.py b/neutron/plugins/embrane/common/constants.py new file mode 100644 index 000000000..ae75e89d2 --- /dev/null +++ b/neutron/plugins/embrane/common/constants.py @@ -0,0 +1,84 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from heleosapi import exceptions as h_exc + +from neutron.plugins.common import constants + + +# Router specific constants +UTIF_LIMIT = 7 +QUEUE_TIMEOUT = 300 + + +class Status: + # Transient + CREATING = constants.PENDING_CREATE + UPDATING = constants.PENDING_UPDATE + DELETING = constants.PENDING_DELETE + # Final + ACTIVE = constants.ACTIVE + ERROR = constants.ERROR + READY = constants.INACTIVE + DELETED = "DELETED" # not visible + + +class Events: + CREATE_ROUTER = "create_router" + UPDATE_ROUTER = "update_router" + DELETE_ROUTER = "delete_router" + GROW_ROUTER_IF = "grow_router_if" + SHRINK_ROUTER_IF = "shrink_router_if" + SET_NAT_RULE = "set_nat_rule" + RESET_NAT_RULE = "reset_nat_rule" + +operation_filter = { + Status.ACTIVE: [Events.DELETE_ROUTER, Events.GROW_ROUTER_IF, + Events.SHRINK_ROUTER_IF, Events.UPDATE_ROUTER, + Events.SET_NAT_RULE, Events.RESET_NAT_RULE], + Status.READY: [Events.DELETE_ROUTER, Events.GROW_ROUTER_IF, + Events.SHRINK_ROUTER_IF, Events.UPDATE_ROUTER], + Status.ERROR: [Events.DELETE_ROUTER, Events.SHRINK_ROUTER_IF], + Status.UPDATING: [Events.DELETE_ROUTER, Events.SHRINK_ROUTER_IF, + Events.RESET_NAT_RULE], + Status.CREATING: [Events.DELETE_ROUTER, Events.CREATE_ROUTER], + Status.DELETING: [Events.DELETE_ROUTER]} + +_DVA_PENDING_ERROR_MSG = _("Dva is pending for the following reason: %s") +_DVA_NOT_FOUNT_ERROR_MSG = _("Dva can't be found to execute the operation, " + "probably was cancelled through the heleos UI") +_DVA_BROKEN_ERROR_MSG = _("Dva seems to be broken for reason %s") +_DVA_BROKEN_INTERFACE_ERROR_MSG = _("Dva interface seems to be broken " + "for reason %s") +_DVA_CREATION_FAILED_ERROR_MSG = _("Dva creation failed reason %s") +_DVA_CREATION_PENDING_ERROR_MSG = _("Dva creation is in pending state " + "for reason %s") +_CFG_FAILED_ERROR_MSG = _("Dva configuration failed for reason %s") +_DVA_DEL_FAILED_ERROR_MSG = _("Failed to delete the backend " + "router for reason %s. Please remove " + "it manually through the heleos UI") + +error_map = {h_exc.PendingDva: _DVA_PENDING_ERROR_MSG, + h_exc.DvaNotFound: _DVA_NOT_FOUNT_ERROR_MSG, + h_exc.BrokenDva: _DVA_BROKEN_ERROR_MSG, + h_exc.BrokenInterface: _DVA_BROKEN_INTERFACE_ERROR_MSG, + h_exc.DvaCreationFailed: _DVA_CREATION_FAILED_ERROR_MSG, + h_exc.DvaCreationPending: _DVA_CREATION_PENDING_ERROR_MSG, + h_exc.ConfigurationFailed: _CFG_FAILED_ERROR_MSG, + h_exc.DvaDeleteFailed: _DVA_DEL_FAILED_ERROR_MSG} diff --git a/neutron/plugins/embrane/common/contexts.py b/neutron/plugins/embrane/common/contexts.py new file mode 100644 index 000000000..fbbf0aff5 --- /dev/null +++ b/neutron/plugins/embrane/common/contexts.py @@ -0,0 +1,40 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + + +class DispatcherContext(object): + + def __init__(self, event, item, neutron_context, chain=None): + self.event = event + self.item = item + self.q_context = neutron_context + self.chain = chain + + +class OperationContext(DispatcherContext): + """Operational context. + + contains all the parameters needed to execute a status aware operation + + """ + def __init__(self, event, context, item, chain, function, args, kwargs): + super(OperationContext, self).__init__(event, item, context, chain) + self.function = function + self.args = args + self.kwargs = kwargs diff --git a/neutron/plugins/embrane/common/exceptions.py b/neutron/plugins/embrane/common/exceptions.py new file mode 100644 index 000000000..763dabdad --- /dev/null +++ b/neutron/plugins/embrane/common/exceptions.py @@ -0,0 +1,35 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from neutron.common import exceptions as neutron_exec + + +class EmbranePluginException(neutron_exec.NeutronException): + message = _("An unexpected error occurred:%(err_msg)s") + + +# Not permitted operation +class NonPermitted(neutron_exec.BadRequest): + pass + + +class StateConstraintException(NonPermitted): + message = _("Operation not permitted due to state constraint violation:" + "%(operation)s not allowed for DVA %(dva_id)s in state " + " %(state)s") diff --git a/neutron/plugins/embrane/common/operation.py b/neutron/plugins/embrane/common/operation.py new file mode 100644 index 000000000..39fa413e2 --- /dev/null +++ b/neutron/plugins/embrane/common/operation.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + + +class Operation(object): + """Defines a series of operations which shall be executed in order. + + the operations expected are procedures, return values are discarded + + """ + + def __init__(self, procedure, args=(), kwargs={}, nextop=None): + self._procedure = procedure + self.args = args[:] + self.kwargs = dict(kwargs) + self.nextop = nextop + + def execute(self): + args = self.args + self._procedure(*args, **self.kwargs) + return self.nextop + + def execute_all(self): + nextop = self.execute() + while nextop: + nextop = self.execute_all() + + def has_next(self): + return self.nextop is not None + + def add_bottom_operation(self, operation): + op = self + while op.has_next(): + op = op.nextop + op.nextop = operation diff --git a/neutron/plugins/embrane/common/utils.py b/neutron/plugins/embrane/common/utils.py new file mode 100644 index 000000000..78c3f8f73 --- /dev/null +++ b/neutron/plugins/embrane/common/utils.py @@ -0,0 +1,65 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from heleosapi import info as h_info + +from neutron.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +def set_db_item_state(context, neutron_item, new_state): + with context.session.begin(subtransactions=True): + if neutron_item["status"] != new_state: + neutron_item["status"] = new_state + context.session.merge(neutron_item) + + +def retrieve_ip_allocation_info(l2_plugin, context, neutron_port): + """Retrieves ip allocation info for a specific port if any.""" + + try: + subnet_id = neutron_port["fixed_ips"][0]["subnet_id"] + except (KeyError, IndexError): + LOG.info(_("No ip allocation set")) + return + subnet = l2_plugin._get_subnet(context, subnet_id) + allocated_ip = neutron_port["fixed_ips"][0]["ip_address"] + is_gw_port = neutron_port["device_owner"] == "network:router_gateway" + gateway_ip = subnet["gateway_ip"] + + ip_allocation_info = h_info.IpAllocationInfo( + is_gw=is_gw_port, + ip_version=subnet["ip_version"], + prefix=subnet["cidr"].split("/")[1], + ip_address=allocated_ip, + port_id=neutron_port["id"], + gateway_ip=gateway_ip) + + return ip_allocation_info + + +def retrieve_nat_info(context, fip, fixed_prefix, floating_prefix, router): + nat_info = h_info.NatInfo(source_address=fip["floating_ip_address"], + source_prefix=floating_prefix, + destination_address=fip["fixed_ip_address"], + destination_prefix=fixed_prefix, + floating_ip_id=fip["id"], + fixed_port_id=fip["port_id"]) + return nat_info diff --git a/neutron/plugins/embrane/l2base/__init__.py b/neutron/plugins/embrane/l2base/__init__.py new file mode 100644 index 000000000..1fac4725b --- /dev/null +++ b/neutron/plugins/embrane/l2base/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. diff --git a/neutron/plugins/embrane/l2base/fake/__init__.py b/neutron/plugins/embrane/l2base/fake/__init__.py new file mode 100644 index 000000000..1fac4725b --- /dev/null +++ b/neutron/plugins/embrane/l2base/fake/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. diff --git a/neutron/plugins/embrane/l2base/fake/fake_l2_plugin.py b/neutron/plugins/embrane/l2base/fake/fake_l2_plugin.py new file mode 100644 index 000000000..5cf68df28 --- /dev/null +++ b/neutron/plugins/embrane/l2base/fake/fake_l2_plugin.py @@ -0,0 +1,24 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from neutron.db import db_base_plugin_v2 + + +class FakeL2Plugin(db_base_plugin_v2.NeutronDbPluginV2): + supported_extension_aliases = [] diff --git a/neutron/plugins/embrane/l2base/fake/fakeplugin_support.py b/neutron/plugins/embrane/l2base/fake/fakeplugin_support.py new file mode 100644 index 000000000..51a4d0b73 --- /dev/null +++ b/neutron/plugins/embrane/l2base/fake/fakeplugin_support.py @@ -0,0 +1,43 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from heleosapi import info as h_info + +from neutron import manager +from neutron.plugins.embrane.l2base import support_base as base + + +class FakePluginSupport(base.SupportBase): + + def __init__(self): + super(FakePluginSupport, self).__init__() + + def retrieve_utif_info(self, context, neutron_port): + plugin = manager.NeutronManager.get_plugin() + network_id = neutron_port["network_id"] + network = plugin._get_network(context, network_id) + is_gw = neutron_port["device_owner"] == "network:router_gateway" + result = h_info.UtifInfo(vlan=0, + network_name=network["name"], + network_id=network["id"], + is_gw=is_gw, + owner_tenant=network["tenant_id"], + port_id=neutron_port["id"], + mac_address=neutron_port["mac_address"]) + return result diff --git a/neutron/plugins/embrane/l2base/openvswitch/__init__.py b/neutron/plugins/embrane/l2base/openvswitch/__init__.py new file mode 100644 index 000000000..1fac4725b --- /dev/null +++ b/neutron/plugins/embrane/l2base/openvswitch/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. diff --git a/neutron/plugins/embrane/l2base/openvswitch/openvswitch_support.py b/neutron/plugins/embrane/l2base/openvswitch/openvswitch_support.py new file mode 100644 index 000000000..d342abaef --- /dev/null +++ b/neutron/plugins/embrane/l2base/openvswitch/openvswitch_support.py @@ -0,0 +1,56 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from heleosapi import info as h_info + +from neutron import manager +from neutron.plugins.embrane.l2base import support_base as base +from neutron.plugins.embrane.l2base import support_exceptions as exc +from neutron.plugins.openvswitch import ovs_db_v2 + + +class OpenvswitchSupport(base.SupportBase): + """OpenVSwitch plugin support. + + Obtains the informations needed to build the user security zones + + """ + + def __init__(self): + super(OpenvswitchSupport, self).__init__() + + def retrieve_utif_info(self, context, neutron_port): + plugin = manager.NeutronManager.get_plugin() + session = context.session + network_id = neutron_port["network_id"] + network_binding = ovs_db_v2.get_network_binding(session, network_id) + if not network_binding["segmentation_id"]: + raise exc.UtifInfoError( + err_msg=_("No segmentation_id found for the network, " + "please be sure that tenant_network_type is vlan")) + network = plugin._get_network(context, network_id) + is_gw = neutron_port["device_owner"] == "network:router_gateway" + result = h_info.UtifInfo(vlan=network_binding["segmentation_id"], + network_name=network["name"], + network_id=network["id"], + is_gw=is_gw, + owner_tenant=network["tenant_id"], + port_id=neutron_port["id"], + mac_address=neutron_port["mac_address"]) + return result diff --git a/neutron/plugins/embrane/l2base/support_base.py b/neutron/plugins/embrane/l2base/support_base.py new file mode 100644 index 000000000..ee4ca65cc --- /dev/null +++ b/neutron/plugins/embrane/l2base/support_base.py @@ -0,0 +1,48 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +import abc + + +class SupportBase(object): + """abstract support class. + + Defines the methods a plugin support should implement to be used as + the L2 base for Embrane plugin. + + """ + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def __init__(self): + pass + + @abc.abstractmethod + def retrieve_utif_info(self, context, neutron_port=None, network=None): + """Retrieve specific network info. + + each plugin support, querying its own DB, can collect all the + information needed by the ESM in order to create the + user traffic security zone. + + :param interface_info: the foo parameter + :param context: neutron request context + :returns: heleosapi.info.UtifInfo -- specific network info + :raises: UtifInfoError + """ diff --git a/neutron/plugins/embrane/l2base/support_exceptions.py b/neutron/plugins/embrane/l2base/support_exceptions.py new file mode 100644 index 000000000..3af17e311 --- /dev/null +++ b/neutron/plugins/embrane/l2base/support_exceptions.py @@ -0,0 +1,25 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from neutron.plugins.embrane.common import exceptions as embrane_exc + + +class UtifInfoError(embrane_exc.EmbranePluginException): + message = _("cannot retrieve utif info for the following reason: " + "%(err_msg)s") diff --git a/neutron/plugins/embrane/plugins/__init__.py b/neutron/plugins/embrane/plugins/__init__.py new file mode 100644 index 000000000..1fac4725b --- /dev/null +++ b/neutron/plugins/embrane/plugins/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. diff --git a/neutron/plugins/embrane/plugins/embrane_fake_plugin.py b/neutron/plugins/embrane/plugins/embrane_fake_plugin.py new file mode 100644 index 000000000..a7ba6782d --- /dev/null +++ b/neutron/plugins/embrane/plugins/embrane_fake_plugin.py @@ -0,0 +1,34 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from neutron.db import extraroute_db +from neutron.plugins.embrane import base_plugin as base +from neutron.plugins.embrane.l2base.fake import fake_l2_plugin as l2 +from neutron.plugins.embrane.l2base.fake import fakeplugin_support as sup + + +class EmbraneFakePlugin(base.EmbranePlugin, extraroute_db.ExtraRoute_db_mixin, + l2.FakeL2Plugin): + _plugin_support = sup.FakePluginSupport + + def __init__(self): + '''First run plugin specific initialization, then Embrane's.''' + self.supported_extension_aliases += ["extraroute", "router"] + l2.FakeL2Plugin.__init__(self) + self._run_embrane_config() diff --git a/neutron/plugins/embrane/plugins/embrane_ovs_plugin.py b/neutron/plugins/embrane/plugins/embrane_ovs_plugin.py new file mode 100644 index 000000000..ded3b9916 --- /dev/null +++ b/neutron/plugins/embrane/plugins/embrane_ovs_plugin.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +from neutron.plugins.embrane import base_plugin as base +from neutron.plugins.embrane.l2base.openvswitch import openvswitch_support +from neutron.plugins.openvswitch import ovs_neutron_plugin as l2 + + +class EmbraneOvsPlugin(base.EmbranePlugin, l2.OVSNeutronPluginV2): + '''EmbraneOvsPlugin. + + This plugin uses OpenVSwitch specific L2 plugin for providing L2 networks + and the base EmbranePlugin for L3. + + ''' + _plugin_support = openvswitch_support.OpenvswitchSupport + + def __init__(self): + '''First run plugin specific initialization, then Embrane's.''' + l2.OVSNeutronPluginV2.__init__(self) + self._run_embrane_config() diff --git a/neutron/tests/unit/embrane/__init__.py b/neutron/tests/unit/embrane/__init__.py new file mode 100644 index 000000000..bb81770cd --- /dev/null +++ b/neutron/tests/unit/embrane/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. diff --git a/neutron/tests/unit/embrane/test_embrane_defaults.py b/neutron/tests/unit/embrane/test_embrane_defaults.py new file mode 100644 index 000000000..1bb6a565e --- /dev/null +++ b/neutron/tests/unit/embrane/test_embrane_defaults.py @@ -0,0 +1,37 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +import sys + +import mock +from oslo.config import cfg + +from neutron.plugins.embrane.common import config # noqa +from neutron.tests import base + +# Need to mock heleosapi. +sys.modules["heleosapi"] = mock.Mock() + + +class ConfigurationTest(base.BaseTestCase): + + def test_defaults(self): + self.assertEqual('admin', cfg.CONF.heleos.admin_username) + self.assertEqual('default', cfg.CONF.heleos.resource_pool_id) + self.assertTrue(cfg.CONF.heleos.async_requests) diff --git a/neutron/tests/unit/embrane/test_embrane_l3_plugin.py b/neutron/tests/unit/embrane/test_embrane_l3_plugin.py new file mode 100644 index 000000000..f7353bd41 --- /dev/null +++ b/neutron/tests/unit/embrane/test_embrane_l3_plugin.py @@ -0,0 +1,46 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +import sys + +import mock +from oslo.config import cfg + +from neutron.db import api as db +from neutron.plugins.embrane.common import config # noqa +from neutron.tests.unit import test_extension_extraroute as extraroute_test +from neutron.tests.unit import test_l3_plugin as router_test + +PLUGIN_NAME = ('neutron.plugins.embrane.plugins.embrane_fake_plugin.' + 'EmbraneFakePlugin') +sys.modules["heleosapi"] = mock.Mock() + + +class TestEmbraneL3NatDBTestCase(router_test.L3NatDBTestCase): + _plugin_name = PLUGIN_NAME + + def setUp(self): + cfg.CONF.set_override('admin_password', "admin123", 'heleos') + self.addCleanup(cfg.CONF.reset) + self.addCleanup(db.clear_db) + super(TestEmbraneL3NatDBTestCase, self).setUp() + + +class ExtraRouteDBTestCase(extraroute_test.ExtraRouteDBTestCase): + _plugin_name = PLUGIN_NAME diff --git a/neutron/tests/unit/embrane/test_embrane_neutron_plugin.py b/neutron/tests/unit/embrane/test_embrane_neutron_plugin.py new file mode 100644 index 000000000..4594de971 --- /dev/null +++ b/neutron/tests/unit/embrane/test_embrane_neutron_plugin.py @@ -0,0 +1,81 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 Embrane, 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: Ivar Lazzaro, Embrane, Inc. + +import sys + +import mock +from oslo.config import cfg + +from neutron.db import api as db +from neutron.plugins.embrane.common import config # noqa +from neutron.tests.unit import test_db_plugin as test_plugin + +PLUGIN_NAME = ('neutron.plugins.embrane.plugins.embrane_fake_plugin.' + 'EmbraneFakePlugin') +sys.modules["heleosapi"] = mock.Mock() + + +class EmbranePluginV2TestCase(test_plugin.NeutronDbPluginV2TestCase): + _plugin_name = PLUGIN_NAME + + def setUp(self): + cfg.CONF.set_override('admin_password', "admin123", 'heleos') + self.addCleanup(cfg.CONF.reset) + self.addCleanup(db.clear_db) + super(EmbranePluginV2TestCase, self).setUp(self._plugin_name) + + +class TestEmbraneBasicGet(test_plugin.TestBasicGet, EmbranePluginV2TestCase): + pass + + +class TestEmbraneV2HTTPResponse(test_plugin.TestV2HTTPResponse, + EmbranePluginV2TestCase): + pass + + +class TestEmbranePortsV2(test_plugin.TestPortsV2, EmbranePluginV2TestCase): + + def test_create_ports_bulk_emulated_plugin_failure(self): + self.skip("Temporary skipping due to incompatibility with the" + " plugin dynamic class type") + + def test_recycle_expired_previously_run_within_context(self): + self.skip("Temporary skipping due to incompatibility with the" + " plugin dynamic class type") + + def test_recycle_held_ip_address(self): + self.skip("Temporary skipping due to incompatibility with the" + " plugin dynamic class type") + + +class TestEmbraneNetworksV2(test_plugin.TestNetworksV2, + EmbranePluginV2TestCase): + + def test_create_networks_bulk_emulated_plugin_failure(self): + self.skip("Temporary skipping due to incompatibility with the" + " plugin dynamic class type") + + +class TestEmbraneSubnetsV2(test_plugin.TestSubnetsV2, + EmbranePluginV2TestCase): + + def test_create_subnets_bulk_emulated_plugin_failure(self): + self.skip("Temporary skipping due to incompatibility with the" + " plugin dynamic class type")