From 1985e16d2e03074eab958f2c622c71da92909c7c Mon Sep 17 00:00:00 2001 From: Ryota MIBU Date: Thu, 2 Aug 2012 01:42:20 +0900 Subject: [PATCH] NEC OpenFlow plugin support. blueprint quantum-nec-openflow-plugin Change-Id: Ib6d6f658bbf6d653527fa7820685f9bed1412a18 --- etc/quantum/plugins/nec/nec.ini | 39 ++ quantum/plugins/nec/README | 20 + quantum/plugins/nec/__init__.py | 15 + quantum/plugins/nec/agent/__init__.py | 15 + .../plugins/nec/agent/nec_quantum_agent.py | 130 +++++ quantum/plugins/nec/common/__init__.py | 15 + quantum/plugins/nec/common/config.py | 59 ++ quantum/plugins/nec/common/exceptions.py | 39 ++ quantum/plugins/nec/common/ofc_client.py | 99 ++++ quantum/plugins/nec/db/__init__.py | 15 + quantum/plugins/nec/db/api.py | 123 +++++ quantum/plugins/nec/db/models.py | 72 +++ quantum/plugins/nec/db/nec_plugin_base.py | 118 ++++ quantum/plugins/nec/drivers/__init__.py | 36 ++ quantum/plugins/nec/drivers/pfc.py | 101 ++++ quantum/plugins/nec/drivers/trema.py | 248 +++++++++ quantum/plugins/nec/extensions/__init__.py | 15 + .../plugins/nec/extensions/packetfilter.py | 131 +++++ quantum/plugins/nec/nec_plugin.py | 505 ++++++++++++++++++ quantum/plugins/nec/ofc_driver_base.py | 103 ++++ quantum/plugins/nec/ofc_manager.py | 150 ++++++ quantum/plugins/nec/run_tests.py | 72 +++ quantum/plugins/nec/tests/__init__.py | 15 + quantum/plugins/nec/tests/unit/__init__.py | 15 + .../plugins/nec/tests/unit/stub_ofc_driver.py | 56 ++ quantum/plugins/nec/tests/unit/test_config.py | 39 ++ quantum/plugins/nec/tests/unit/test_db.py | 141 +++++ .../nec/tests/unit/test_nec_plugin_v2.py | 62 +++ .../nec/tests/unit/test_ofc_manager.py | 160 ++++++ .../plugins/nec/tests/unit/test_pfc_driver.py | 158 ++++++ .../nec/tests/unit/test_trema_driver.py | 236 ++++++++ setup.py | 6 +- 32 files changed, 3007 insertions(+), 1 deletion(-) create mode 100644 etc/quantum/plugins/nec/nec.ini create mode 100644 quantum/plugins/nec/README create mode 100644 quantum/plugins/nec/__init__.py create mode 100644 quantum/plugins/nec/agent/__init__.py create mode 100755 quantum/plugins/nec/agent/nec_quantum_agent.py create mode 100644 quantum/plugins/nec/common/__init__.py create mode 100644 quantum/plugins/nec/common/config.py create mode 100644 quantum/plugins/nec/common/exceptions.py create mode 100644 quantum/plugins/nec/common/ofc_client.py create mode 100644 quantum/plugins/nec/db/__init__.py create mode 100644 quantum/plugins/nec/db/api.py create mode 100644 quantum/plugins/nec/db/models.py create mode 100644 quantum/plugins/nec/db/nec_plugin_base.py create mode 100644 quantum/plugins/nec/drivers/__init__.py create mode 100644 quantum/plugins/nec/drivers/pfc.py create mode 100644 quantum/plugins/nec/drivers/trema.py create mode 100644 quantum/plugins/nec/extensions/__init__.py create mode 100644 quantum/plugins/nec/extensions/packetfilter.py create mode 100644 quantum/plugins/nec/nec_plugin.py create mode 100644 quantum/plugins/nec/ofc_driver_base.py create mode 100644 quantum/plugins/nec/ofc_manager.py create mode 100755 quantum/plugins/nec/run_tests.py create mode 100644 quantum/plugins/nec/tests/__init__.py create mode 100644 quantum/plugins/nec/tests/unit/__init__.py create mode 100644 quantum/plugins/nec/tests/unit/stub_ofc_driver.py create mode 100644 quantum/plugins/nec/tests/unit/test_config.py create mode 100644 quantum/plugins/nec/tests/unit/test_db.py create mode 100644 quantum/plugins/nec/tests/unit/test_nec_plugin_v2.py create mode 100644 quantum/plugins/nec/tests/unit/test_ofc_manager.py create mode 100644 quantum/plugins/nec/tests/unit/test_pfc_driver.py create mode 100644 quantum/plugins/nec/tests/unit/test_trema_driver.py diff --git a/etc/quantum/plugins/nec/nec.ini b/etc/quantum/plugins/nec/nec.ini new file mode 100644 index 0000000000..e21a2b7c95 --- /dev/null +++ b/etc/quantum/plugins/nec/nec.ini @@ -0,0 +1,39 @@ +# Sample Configurations + +[DATABASE] +# This line MUST be changed to actually run the plugin. +# Example: +# sql_connection = mysql://root:nova@127.0.0.1:3306/ovs_quantum +# Replace 127.0.0.1 above with the IP address of the database used by the +# main quantum server. (Leave it as is if the database runs on this host.) +sql_connection = sqlite:// +# Database reconnection retry times - in event connectivity is lost +# set to -1 implies an infinite retry count +# sql_max_retries = 10 +# Database reconnection interval in seconds - in event connectivity is lost +reconnect_interval = 2 + +[OVS] +# Do not change this parameter unless you have a good reason to. +# This is the name of the OVS integration bridge. There is one per hypervisor. +# The integration bridge acts as a virtual "patch port". All VM VIFs are +# attached to this bridge and then "patched" according to their network +# connectivity. +integration_bridge = br-int + +[AGENT] +# Agent's polling interval in seconds +polling_interval = 2 +# Change to "sudo quantum-rootwrap" to limit commands that can be run +# as root. +root_helper = sudo + +[OFC] +# Specify OpenFlow Controller Host, Port and Driver to connect. +host = 127.0.0.1 +port = 8888 +# Drivers are in quantum/plugins/nec/drivers/ . +driver = trema +# PacketFilter is available when it's enabled in this configuration +# and supported by the driver. +enable_packet_filter = true diff --git a/quantum/plugins/nec/README b/quantum/plugins/nec/README new file mode 100644 index 0000000000..c219364291 --- /dev/null +++ b/quantum/plugins/nec/README @@ -0,0 +1,20 @@ +Quantum NEC OpenFlow Plugin + + +# -- What's this? + +http://wiki.openstack.org/Quantum-NEC-OpenFlow-Plugin + + +# -- Installation + +Use QuickStart Script for this plugin. This provides you auto installation and +configuration of Nova, Quantum and Trema. +https://github.com/nec-openstack/quantum-openflow-plugin/tree/folsom + + +# -- Running Tests + +To run tests of this plugin (run the following from the top level Quantum +directory): +PLUGIN_DIR=quantum/plugins/nec ./run_tests.sh -N diff --git a/quantum/plugins/nec/__init__.py b/quantum/plugins/nec/__init__.py new file mode 100644 index 0000000000..362a360682 --- /dev/null +++ b/quantum/plugins/nec/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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. diff --git a/quantum/plugins/nec/agent/__init__.py b/quantum/plugins/nec/agent/__init__.py new file mode 100644 index 0000000000..362a360682 --- /dev/null +++ b/quantum/plugins/nec/agent/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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. diff --git a/quantum/plugins/nec/agent/nec_quantum_agent.py b/quantum/plugins/nec/agent/nec_quantum_agent.py new file mode 100755 index 0000000000..4e4d824f8e --- /dev/null +++ b/quantum/plugins/nec/agent/nec_quantum_agent.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. +# Based on ryu/openvswitch agents. +# +# Copyright 2012 Isaku Yamahata +# Copyright 2011 Nicira 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: Ryota MIBU + +import logging +import sys +import time +import socket + +from quantum.agent.linux import ovs_lib +from quantum.common import config as logging_config +from quantum.common import topics +from quantum.openstack.common import context +from quantum.openstack.common import rpc +from quantum.plugins.nec.common import config + + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +class NECQuantumAgent(object): + + def __init__(self, integ_br, root_helper, polling_interval): + '''Constructor. + + :param integ_br: name of the integration bridge. + :param root_helper: utility to use when running shell cmds. + :param polling_interval: interval (secs) to check the bridge. + ''' + self.int_br = ovs_lib.OVSBridge(integ_br, root_helper) + self.polling_interval = polling_interval + + self.host = socket.gethostname() + self.agent_id = 'nec-q-agent.%s' % self.host + self.datapath_id = "0x%s" % self.int_br.get_datapath_id() + + # RPC network init + self.context = context.RequestContext('quantum', 'quantum', + is_admin=False) + self.conn = rpc.create_connection(new=True) + + def update_ports(self, port_added=[], port_removed=[]): + """RPC to update information of ports on Quantum Server""" + LOG.info("update ports: added=%s, removed=%s" % + (port_added, port_removed)) + try: + rpc.call(self.context, + topics.PLUGIN, + {'method': 'update_ports', + 'args': {'topic': topics.AGENT, + 'agent_id': self.agent_id, + 'datapath_id': self.datapath_id, + 'port_added': port_added, + 'port_removed': port_removed}}) + except Exception as e: + LOG.warn("update_ports() failed.") + return + + def _vif_port_to_port_info(self, vif_port): + return dict(id=vif_port.vif_id, port_no=vif_port.ofport, + mac=vif_port.vif_mac) + + def daemon_loop(self): + """Main processing loop for NEC Plugin Agent.""" + old_ports = [] + while True: + new_ports = [] + + port_added = [] + for vif_port in self.int_br.get_vif_ports(): + port_id = vif_port.vif_id + new_ports.append(port_id) + if port_id not in old_ports: + port_info = self._vif_port_to_port_info(vif_port) + port_added.append(port_info) + + port_removed = [] + for port_id in old_ports: + if port_id not in new_ports: + port_removed.append(port_id) + + if port_added or port_removed: + self.update_ports(port_added, port_removed) + else: + LOG.debug("No port changed.") + + old_ports = new_ports + time.sleep(self.polling_interval) + + +def main(): + config.CONF(args=sys.argv, project='quantum') + + logging_config.setup_logging(config.CONF) + + # Determine which agent type to use. + integ_br = config.OVS.integration_bridge + root_helper = config.AGENT.root_helper + polling_interval = config.AGENT.polling_interval + + agent = NECQuantumAgent(integ_br, root_helper, polling_interval) + + # Start everything. + agent.daemon_loop() + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/quantum/plugins/nec/common/__init__.py b/quantum/plugins/nec/common/__init__.py new file mode 100644 index 0000000000..362a360682 --- /dev/null +++ b/quantum/plugins/nec/common/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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. diff --git a/quantum/plugins/nec/common/config.py b/quantum/plugins/nec/common/config.py new file mode 100644 index 0000000000..925e8bf4be --- /dev/null +++ b/quantum/plugins/nec/common/config.py @@ -0,0 +1,59 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +from quantum.openstack.common import cfg +# import rpc config options +from quantum.openstack.common import rpc + + +database_opts = [ + cfg.StrOpt('sql_connection', default='sqlite://'), + cfg.IntOpt('sql_max_retries', default=-1), + cfg.IntOpt('reconnect_interval', default=2), +] + +ovs_opts = [ + cfg.StrOpt('integration_bridge', default='br-int'), +] + +agent_opts = [ + cfg.IntOpt('polling_interval', default=2), + cfg.StrOpt('root_helper', default='sudo'), +] + +ofc_opts = [ + cfg.StrOpt('host', default='127.0.0.1'), + cfg.StrOpt('port', default='8888'), + cfg.StrOpt('driver', default='trema'), + cfg.BoolOpt('enable_packet_filter', default=True), + cfg.BoolOpt('use_ssl', default=False), + cfg.StrOpt('key_file', default=None), + cfg.StrOpt('cert_file', default=None), +] + + +cfg.CONF.register_opts(database_opts, "DATABASE") +cfg.CONF.register_opts(ovs_opts, "OVS") +cfg.CONF.register_opts(agent_opts, "AGENT") +cfg.CONF.register_opts(ofc_opts, "OFC") + +# shortcuts +CONF = cfg.CONF +DATABASE = cfg.CONF.DATABASE +OVS = cfg.CONF.OVS +AGENT = cfg.CONF.AGENT +OFC = cfg.CONF.OFC diff --git a/quantum/plugins/nec/common/exceptions.py b/quantum/plugins/nec/common/exceptions.py new file mode 100644 index 0000000000..407dab2069 --- /dev/null +++ b/quantum/plugins/nec/common/exceptions.py @@ -0,0 +1,39 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +from quantum.common import exceptions as qexc + + +class OFCException(qexc.QuantumException): + message = _("An OFC exception has occurred: %(reason)s") + + +class NECDBException(qexc.QuantumException): + message = _("An exception occurred in NECPluginV2 DB: %(reason)s") + + +class OFCConsistencyBroken(qexc.QuantumException): + message = _("Consistency of Quantum-OFC resource map is broken: " + "%(reason)s") + + +class PortInfoNotFound(qexc.NotFound): + message = _("PortInfo %(id)s could not be found") + + +class PacketFilterNotFound(qexc.NotFound): + message = _("PacketFilter %(id)s could not be found") diff --git a/quantum/plugins/nec/common/ofc_client.py b/quantum/plugins/nec/common/ofc_client.py new file mode 100644 index 0000000000..f8db74da70 --- /dev/null +++ b/quantum/plugins/nec/common/ofc_client.py @@ -0,0 +1,99 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import httplib +import json +import logging +import socket + +from quantum.plugins.nec.common import exceptions as nexc + + +LOG = logging.getLogger(__name__) + + +class OFCClient(object): + """A HTTP/HTTPS client for OFC Drivers""" + + def __init__(self, host="127.0.0.1", port=8888, use_ssl=False, + key_file=None, cert_file=None): + """Creates a new client to some OFC. + + :param host: The host where service resides + :param port: The port where service resides + :param use_ssl: True to use SSL, False to use HTTP + :param key_file: The SSL key file to use if use_ssl is true + :param cert_file: The SSL cert file to use if use_ssl is true + """ + self.host = host + self.port = port + self.use_ssl = use_ssl + self.key_file = key_file + self.cert_file = cert_file + self.connection = None + + def get_connection_type(self): + """Returns the proper connection type""" + if self.use_ssl: + return httplib.HTTPSConnection + else: + return httplib.HTTPConnection + + def do_request(self, method, action, body=None): + LOG.debug("Client request: %s %s [%s]" % (method, action, str(body))) + + if type(body) is dict: + body = json.dumps(body) + try: + connection_type = self.get_connection_type() + headers = {"Content-Type": "application/json"} + # Open connection and send request, handling SSL certs + certs = {'key_file': self.key_file, 'cert_file': self.cert_file} + certs = dict((x, certs[x]) for x in certs if certs[x] is not None) + if self.use_ssl and len(certs): + conn = connection_type(self.host, self.port, **certs) + else: + conn = connection_type(self.host, self.port) + conn.request(method, action, body, headers) + res = conn.getresponse() + data = res.read() + LOG.debug("OFC returns [%s:%s]" % (str(res.status), data)) + if res.status in (httplib.OK, + httplib.CREATED, + httplib.ACCEPTED, + httplib.NO_CONTENT): + if data and len(data) > 1: + return json.loads(data) + else: + reason = _("An operation on OFC is failed.") + raise nexc.OFCException(reason=reason) + except (socket.error, IOError), e: + reason = _("Failed to connect OFC : %s" % str(e)) + LOG.error(reason) + raise nexc.OFCException(reason=reason) + + def get(self, action): + return self.do_request("GET", action) + + def post(self, action, body=None): + return self.do_request("POST", action, body=body) + + def put(self, action, body=None): + return self.do_request("PUT", action, body=body) + + def delete(self, action): + return self.do_request("DELETE", action) diff --git a/quantum/plugins/nec/db/__init__.py b/quantum/plugins/nec/db/__init__.py new file mode 100644 index 0000000000..362a360682 --- /dev/null +++ b/quantum/plugins/nec/db/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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. diff --git a/quantum/plugins/nec/db/api.py b/quantum/plugins/nec/db/api.py new file mode 100644 index 0000000000..853f2944a6 --- /dev/null +++ b/quantum/plugins/nec/db/api.py @@ -0,0 +1,123 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import logging + +import sqlalchemy as sa + +from quantum.db import api as db +from quantum.db import model_base +from quantum.plugins.nec.common import config +from quantum.plugins.nec.common import exceptions as nexc +from quantum.plugins.nec.db import models as nmodels + + +LOG = logging.getLogger(__name__) +OFP_VLAN_NONE = 0xffff + + +def initialize(): + options = {"sql_connection": "%s" % config.DATABASE.sql_connection, + "sql_max_retries": config.DATABASE.sql_max_retries, + "reconnect_interval": config.DATABASE.reconnect_interval, + "base": model_base.BASEV2} + db.configure_db(options) + + +def clear_db(base=model_base.BASEV2): + db.clear_db(base) + + +def get_ofc_item(model, id): + session = db.get_session() + try: + return (session.query(model). + filter_by(id=id). + one()) + except sa.orm.exc.NoResultFound: + return None + + +def find_ofc_item(model, quantum_id): + session = db.get_session() + try: + return (session.query(model). + filter_by(quantum_id=quantum_id). + one()) + except sa.orm.exc.NoResultFound: + return None + + +def add_ofc_item(model, id, quantum_id): + session = db.get_session() + try: + item = model(id=id, quantum_id=quantum_id) + session.add(item) + session.flush() + except sa.exc.IntegrityError as exc: + LOG.exception(exc) + raise nexc.NECDBException + return item + + +def del_ofc_item(model, id): + session = db.get_session() + try: + item = (session.query(model). + filter_by(id=id). + one()) + session.delete(item) + session.flush() + except sa.orm.exc.NoResultFound: + LOG.warning("_del_ofc_item(): NotFound item " + "(model=%s, id=%s) " % (model, id)) + + +def get_portinfo(id): + session = db.get_session() + try: + return (session.query(nmodels.PortInfo). + filter_by(id=id). + one()) + except sa.orm.exc.NoResultFound: + return None + + +def add_portinfo(id, datapath_id='', port_no=0, vlan_id=OFP_VLAN_NONE, mac=''): + session = db.get_session() + try: + portinfo = nmodels.PortInfo(id=id, datapath_id=datapath_id, + port_no=port_no, vlan_id=vlan_id, mac=mac) + session.add(portinfo) + session.flush() + except sa.exc.IntegrityError as exc: + LOG.exception(exc) + raise nexc.NECDBException + return portinfo + + +def del_portinfo(id): + session = db.get_session() + try: + portinfo = (session.query(nmodels.PortInfo). + filter_by(id=id). + one()) + session.delete(portinfo) + session.flush() + except sa.orm.exc.NoResultFound: + LOG.warning("del_portinfo(): NotFound portinfo for " + "port_id: %s" % id) diff --git a/quantum/plugins/nec/db/models.py b/quantum/plugins/nec/db/models.py new file mode 100644 index 0000000000..ee20616b9d --- /dev/null +++ b/quantum/plugins/nec/db/models.py @@ -0,0 +1,72 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import sqlalchemy as sa + +from quantum.db import model_base +from quantum.db import models_v2 + + +class HasQuantumId(object): + """Logical ID on Quantum""" + quantum_id = sa.Column(sa.String(36), nullable=False) + + +class OFCTenant(model_base.BASEV2, models_v2.HasId, HasQuantumId): + """Represents a Tenant on OpenFlow Network/Controller.""" + + +class OFCNetwork(model_base.BASEV2, models_v2.HasId, HasQuantumId): + """Represents a Network on OpenFlow Network/Controller.""" + + +class OFCPort(model_base.BASEV2, models_v2.HasId, HasQuantumId): + """Represents a Port on OpenFlow Network/Controller.""" + + +class OFCFilter(model_base.BASEV2, models_v2.HasId, HasQuantumId): + """Represents a Filter on OpenFlow Network/Controller.""" + + +class PortInfo(model_base.BASEV2, models_v2.HasId): + """Represents a Virtual Interface.""" + datapath_id = sa.Column(sa.String(36), nullable=False) + port_no = sa.Column(sa.Integer, nullable=False) + vlan_id = sa.Column(sa.Integer, nullable=False) + mac = sa.Column(sa.String(32), nullable=False) + + +class PacketFilter(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant): + """Represents a packet filter""" + network_id = sa.Column(sa.String(36), + sa.ForeignKey('networks.id', ondelete="CASCADE"), + nullable=False) + priority = sa.Column(sa.Integer, nullable=False) + action = sa.Column(sa.String(16), nullable=False) + # condition + in_port = sa.Column(sa.String(36), nullable=False) + src_mac = sa.Column(sa.String(32), nullable=False) + dst_mac = sa.Column(sa.String(32), nullable=False) + eth_type = sa.Column(sa.Integer, nullable=False) + src_cidr = sa.Column(sa.String(64), nullable=False) + dst_cidr = sa.Column(sa.String(64), nullable=False) + protocol = sa.Column(sa.String(16), nullable=False) + src_port = sa.Column(sa.Integer, nullable=False) + dst_port = sa.Column(sa.Integer, nullable=False) + # status + admin_state_up = sa.Column(sa.Boolean(), nullable=False) + status = sa.Column(sa.String(16), nullable=False) diff --git a/quantum/plugins/nec/db/nec_plugin_base.py b/quantum/plugins/nec/db/nec_plugin_base.py new file mode 100644 index 0000000000..d99e2e3daa --- /dev/null +++ b/quantum/plugins/nec/db/nec_plugin_base.py @@ -0,0 +1,118 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +from quantum.api.v2 import attributes +from quantum.common import utils +from quantum.db import db_base_plugin_v2 +from quantum.plugins.nec.db import models as nmodels + + +class NECPluginV2Base(db_base_plugin_v2.QuantumDbPluginV2): + + """ Base class of plugins that handle packet filters. """ + + def _make_packet_filter_dict(self, packet_filter, fields=None): + res = {'id': packet_filter['id'], + 'tenant_id': packet_filter['tenant_id'], + 'network_id': packet_filter['network_id'], + 'action': packet_filter['action'], + 'priority': packet_filter['priority'], + 'in_port': packet_filter['in_port'], + 'src_mac': packet_filter['src_mac'], + 'dst_mac': packet_filter['dst_mac'], + 'eth_type': packet_filter['eth_type'], + 'src_cidr': packet_filter['src_cidr'], + 'dst_cidr': packet_filter['dst_cidr'], + 'protocol': packet_filter['protocol'], + 'src_port': packet_filter['src_port'], + 'dst_port': packet_filter['dst_port'], + 'admin_state_up': packet_filter['admin_state_up'], + 'status': packet_filter['status']} + return self._fields(res, fields) + + def _get_packet_filter(self, context, id, verbose=None): + try: + packet_filter = self._get_by_id(context, nmodels.PacketFilter, id, + verbose=verbose) + except exc.NoResultFound: + raise q_exc.PacketFilterNotFound(id=id) + except exc.MultipleResultsFound: + LOG.error('Multiple packet_filters match for %s' % id) + raise q_exc.PacketFilterNotFound(id=id) + return packet_filter + + def get_packet_filter(self, context, id, fields=None, verbose=None): + packet_filter = self._get_packet_filter(context, id, verbose=verbose) + return self._make_packet_filter_dict(packet_filter, fields) + + def get_packet_filters(self, context, filters=None, fields=None, + verbose=None): + return self._get_collection(context, + nmodels.PacketFilter, + self._make_packet_filter_dict, + filters=filters, + fields=fields, + verbose=verbose) + + def create_packet_filter(self, context, packet_filter): + pf = packet_filter['packet_filter'] + tenant_id = self._get_tenant_id_for_create(context, pf) + + # validate network ownership + super(NECPluginV2Base, self).get_network(context, pf['network_id']) + if pf.get('in_port') != attributes.ATTR_NOT_SPECIFIED: + # validate port ownership + super(NECPluginV2Base, self).get_port(context, pf['in_port']) + + params = {'tenant_id': tenant_id, + 'id': pf.get('id') or utils.str_uuid(), + 'network_id': pf['network_id'], + 'priority': pf['priority'], + 'action': pf['action'], + 'admin_state_up': pf.get('admin_state_up', True), + 'status': "ACTIVE"} + conditions = {'in_port': '', + 'src_mac': '', + 'dst_mac': '', + 'eth_type': 0, + 'src_cidr': '', + 'dst_cidr': '', + 'src_port': 0, + 'dst_port': 0, + 'protocol': ''} + for key, default in conditions.items(): + if pf.get(key) == attributes.ATTR_NOT_SPECIFIED: + params.update({key: default}) + else: + params.update({key: pf.get(key)}) + + with context.session.begin(): + pf_entry = nmodels.PacketFilter(**params) + context.session.add(pf_entry) + return self._make_packet_filter_dict(pf_entry) + + def update_packet_filter(self, context, id, packet_filter): + pf = packet_filter['packet_filter'] + with context.session.begin(): + pf_entry = self._get_packet_filter(context, id) + pf_entry.update(pf) + return self._make_packet_filter_dict(pf_entry) + + def delete_packet_filter(self, context, id): + with context.session.begin(): + packet_filter = self._get_packet_filter(context, id) + context.session.delete(packet_filter) diff --git a/quantum/plugins/nec/drivers/__init__.py b/quantum/plugins/nec/drivers/__init__.py new file mode 100644 index 0000000000..09e803b31b --- /dev/null +++ b/quantum/plugins/nec/drivers/__init__.py @@ -0,0 +1,36 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import logging + +from quantum.openstack.common import importutils + + +LOG = logging.getLogger(__name__) +DRIVER_PATH = "quantum.plugins.nec.drivers.%s" +DRIVER_LIST = { + 'trema': DRIVER_PATH % "trema.TremaPortBaseDriver", + 'trema_port': DRIVER_PATH % "trema.TremaPortBaseDriver", + 'trema_portmac': DRIVER_PATH % "trema.TremaPortMACBaseDriver", + 'trema_mac': DRIVER_PATH % "trema.TremaMACBaseDriver", + 'pfc': DRIVER_PATH % "pfc.PFCDriver"} + + +def get_driver(driver_name): + LOG.info("Loading OFC driver: %s" % driver_name) + driver_klass = DRIVER_LIST.get(driver_name) or driver_name + return importutils.import_class(driver_klass) diff --git a/quantum/plugins/nec/drivers/pfc.py b/quantum/plugins/nec/drivers/pfc.py new file mode 100644 index 0000000000..c3315a1bed --- /dev/null +++ b/quantum/plugins/nec/drivers/pfc.py @@ -0,0 +1,101 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +from quantum.plugins.nec.common import ofc_client +from quantum.plugins.nec import ofc_driver_base + + +TENANTS_PATH = "/tenants" +TENANT_PATH = "/tenants/%s" +NETWORKS_PATH = "/tenants/%s/networks" +NETWORK_PATH = "/tenants/%s/networks/%s" +PORTS_PATH = "/tenants/%s/networks/%s/ports" +PORT_PATH = "/tenants/%s/networks/%s/ports/%s" + + +class PFCDriver(ofc_driver_base.OFCDriverBase): + + def __init__(self, conf_ofc): + self.client = ofc_client.OFCClient(host=conf_ofc.host, + port=conf_ofc.port, + use_ssl=conf_ofc.use_ssl, + key_file=conf_ofc.key_file, + cert_file=conf_ofc.cert_file) + + @classmethod + def filter_supported(cls): + return False + + def create_tenant(self, description, tenant_id=None): + body = {'description': description} + if tenant_id: + body.update({'id': tenant_id}) + res = self.client.post(TENANTS_PATH, body=body) + ofc_tenant_id = res['id'] + return ofc_tenant_id + + def update_tenant(self, ofc_tenant_id, description): + path = TENANT_PATH % ofc_tenant_id + body = {'description': description} + res = self.client.put(path, body=body) + + def delete_tenant(self, ofc_tenant_id): + path = TENANT_PATH % ofc_tenant_id + return self.client.delete(path) + + def create_network(self, ofc_tenant_id, description, network_id=None): + path = NETWORKS_PATH % ofc_tenant_id + body = {'description': description} + if network_id: + body.update({'id': network_id}) + res = self.client.post(path, body=body) + ofc_network_id = res['id'] + return ofc_network_id + + def update_network(self, ofc_tenant_id, ofc_network_id, description): + path = NETWORK_PATH % (ofc_tenant_id, ofc_network_id) + body = {'description': description} + return self.client.put(path, body=body) + + def delete_network(self, ofc_tenant_id, ofc_network_id): + path = NETWORK_PATH % (ofc_tenant_id, ofc_network_id) + return self.client.delete(path) + + def create_port(self, ofc_tenant_id, ofc_network_id, portinfo, + port_id=None): + path = PORTS_PATH % (ofc_tenant_id, ofc_network_id) + body = {'datapath_id': portinfo.datapath_id, + 'port': str(portinfo.port_no), + 'vid': str(portinfo.vlan_id)} + if port_id: + body.update({'id': port_id}) + res = self.client.post(path, body=body) + ofc_port_id = res['id'] + return ofc_port_id + + def update_port(self, ofc_tenant_id, ofc_network_id, portinfo, port_id): + path = PORT_PATH % (ofc_tenant_id, ofc_network_id, ofc_port_id) + body = {'datapath_id': portinfo.datapath_id, + 'port': str(portinfo.port_no), + 'vid': str(portinfo.vlan_id)} + res = self.client.put(path, body=body) + ofc_port_id = res['id'] + return ofc_port_id + + def delete_port(self, ofc_tenant_id, ofc_network_id, ofc_port_id): + path = PORT_PATH % (ofc_tenant_id, ofc_network_id, ofc_port_id) + return self.client.delete(path) diff --git a/quantum/plugins/nec/drivers/trema.py b/quantum/plugins/nec/drivers/trema.py new file mode 100644 index 0000000000..845b380708 --- /dev/null +++ b/quantum/plugins/nec/drivers/trema.py @@ -0,0 +1,248 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import uuid + +from quantum.plugins.nec.common import ofc_client +from quantum.plugins.nec import ofc_driver_base + + +class TremaDriverBase(ofc_driver_base.OFCDriverBase): + """Common class for Trema (Sliceable Switch) Drivers""" + networks_path = "/networks" + network_path = "/networks/%s" + + def __init__(self, conf_ofc): + # Trema sliceable REST API does not support HTTPS + self.client = ofc_client.OFCClient(host=conf_ofc.host, + port=conf_ofc.port) + + def create_tenant(self, description, tenant_id=None): + return tenant_id or str(uuid.uuid4()) + + def update_tenant(self, ofc_tenant_id, description): + pass + + def delete_tenant(self, ofc_tenant_id): + pass + + def create_network(self, ofc_tenant_id, description, network_id=None): + ofc_network_id = network_id or str(uuid.uuid4()) + body = {'id': ofc_network_id, 'description': description} + self.client.post(self.networks_path, body=body) + return ofc_network_id + + def update_network(self, ofc_tenant_id, ofc_network_id, description): + path = self.network_path % ofc_network_id + body = {'description': description} + return self.client.put(path, body=body) + + def delete_network(self, ofc_tenant_id, ofc_network_id): + path = self.network_path % ofc_network_id + return self.client.delete(path) + + def update_port(self, ofc_tenant_id, ofc_network_id, ofc_port_id, + portinfo): + self.delete_port(ofc_tenant_id, ofc_network_id, ofc_port_id) + self.create_port(ofc_tenant_id, ofc_network_id, portinfo, ofc_port_id) + + +class TremaFilterDriver(object): + """Trema (Sliceable Switch) PacketFilter Driver""" + filters_path = "/filters" + filter_path = "/filters/%s" + + @classmethod + def filter_supported(cls): + return True + + def create_filter(self, ofc_tenant_id, ofc_network_id, filter_dict, + portinfo=None, filter_id=None): + if filter_dict['action'].upper() in ["ACCEPT", "ALLOW"]: + ofc_action = "ALLOW" + elif filter_dict['action'].upper() in ["DROP", "DENY"]: + ofc_action = "DENY" + + body = {'priority': filter_dict['priority'], + 'slice': ofc_network_id, + 'action': ofc_action} + ofp_wildcards = ["dl_vlan", "dl_vlan_pcp", "nw_tos"] + + if portinfo: + body['in_datapath_id'] = portinfo.datapath_id + body['in_port'] = portinfo.port_no + else: + body['wildcards'] = "in_datapath_id" + ofp_wildcards.append("in_port") + + if filter_dict['src_mac']: + body['dl_src'] = filter_dict['src_mac'] + else: + ofp_wildcards.append("dl_src") + + if filter_dict['dst_mac']: + body['dl_dst'] = filter_dict['dst_mac'] + else: + ofp_wildcards.append("dl_dst") + + if filter_dict['src_cidr']: + body['nw_src'] = filter_dict['src_cidr'] + else: + ofp_wildcards.append("nw_src:32") + + if filter_dict['dst_cidr']: + body['nw_dst'] = filter_dict['dst_cidr'] + else: + ofp_wildcards.append("nw_dst:32") + + if filter_dict['protocol']: + if filter_dict['protocol'].upper() in "ICMP": + body['dl_type'] = "0x800" + body['nw_proto'] = hex(1) + elif filter_dict['protocol'].upper() in "TCP": + body['dl_type'] = "0x800" + body['nw_proto'] = hex(6) + elif filter_dict['protocol'].upper() in "UDP": + body['dl_type'] = "0x800" + body['nw_proto'] = hex(17) + elif filter_dict['protocol'].upper() in "ARP": + body['dl_type'] = "0x806" + ofp_wildcards.append("nw_proto") + else: + body['nw_proto'] = filter_dict['protocol'] + if filter_dict['eth_type']: + body['dl_type'] = filter_dict['eth_type'] + else: + ofp_wildcards.append("dl_type") + else: + ofp_wildcards.append("dl_type") + ofp_wildcards.append("nw_proto") + + if filter_dict['src_port']: + body['tp_src'] = hex(filter_dict['src_port']) + else: + ofp_wildcards.append("tp_src") + + if filter_dict['dst_port']: + body['tp_dst'] = hex(filter_dict['dst_port']) + else: + ofp_wildcards.append("tp_dst") + + ofc_filter_id = filter_id or str(uuid.uuid4()) + body['id'] = ofc_filter_id + + body['ofp_wildcards'] = ','.join(ofp_wildcards) + + self.client.post(self.filters_path, body=body) + return ofc_filter_id + + def delete_filter(self, ofc_tenant_id, ofc_network_id, ofc_filter_id): + path = self.filter_path % ofc_filter_id + return self.client.delete(path) + + +class TremaPortBaseDriver(TremaDriverBase, TremaFilterDriver): + """Trema (Sliceable Switch) Driver for port base binding + + TremaPortBaseDriver uses port base binding. + Ports are identified by datapath_id, port_no and vlan_id. + """ + ports_path = "/networks/%s/ports" + port_path = "/networks/%s/ports/%s" + + def create_port(self, ofc_tenant_id, ofc_network_id, portinfo, + port_id=None): + ofc_port_id = port_id or str(uuid.uuid4()) + path = self.ports_path % ofc_network_id + body = {'id': ofc_port_id, + 'datapath_id': portinfo.datapath_id, + 'port': str(portinfo.port_no), + 'vid': str(portinfo.vlan_id)} + self.client.post(path, body=body) + return ofc_port_id + + def delete_port(self, ofc_tenant_id, ofc_network_id, ofc_port_id): + path = self.port_path % (ofc_network_id, ofc_port_id) + return self.client.delete(path) + + +class TremaPortMACBaseDriver(TremaDriverBase, TremaFilterDriver): + """Trema (Sliceable Switch) Driver for port-mac base binding + + TremaPortBaseDriver uses port-mac base binding. + Ports are identified by datapath_id, port_no, vlan_id and mac. + """ + ports_path = "/networks/%s/ports" + port_path = "/networks/%s/ports/%s" + attachments_path = "/networks/%s/ports/%s/attachments" + attachment_path = "/networks/%s/ports/%s/attachments/%s" + + def create_port(self, ofc_tenant_id, ofc_network_id, portinfo, + port_id=None): + #NOTE: This Driver create slices with Port-MAC Based bindings on Trema + # Sliceable. It's REST API requires Port Based binding before you + # define Port-MAC Based binding. + ofc_port_id = port_id or str(uuid.uuid4()) + dummy_port_id = "dummy-%s" % ofc_port_id + + path = self.ports_path % ofc_network_id + body = {'id': dummy_port_id, + 'datapath_id': portinfo.datapath_id, + 'port': str(portinfo.port_no), + 'vid': str(portinfo.vlan_id)} + self.client.post(path, body=body) + + path = self.attachments_path % (ofc_network_id, dummy_port_id) + body = {'id': ofc_port_id, 'mac': portinfo.mac} + self.client.post(path, body=body) + + path = self.port_path % (ofc_network_id, dummy_port_id) + self.client.delete(path) + + return ofc_port_id + + def delete_port(self, ofc_tenant_id, ofc_network_id, ofc_port_id): + dummy_port_id = "dummy-%s" % ofc_port_id + path = self.attachment_path % (ofc_network_id, dummy_port_id, + ofc_port_id) + return self.client.delete(path) + + +class TremaMACBaseDriver(TremaDriverBase): + """Trema (Sliceable Switch) Driver for mac base binding + + TremaPortBaseDriver uses mac base binding. + Ports are identified by mac. + """ + attachments_path = "/networks/%s/attachments" + attachment_path = "/networks/%s/attachments/%s" + + @classmethod + def filter_supported(cls): + return False + + def create_port(self, ofc_tenant_id, ofc_network_id, portinfo, + port_id=None): + ofc_port_id = port_id or str(uuid.uuid4()) + path = self.attachments_path % ofc_network_id + body = {'id': ofc_port_id, 'mac': portinfo.mac} + self.client.post(path, body=body) + return ofc_port_id + + def delete_port(self, ofc_tenant_id, ofc_network_id, ofc_port_id): + path = self.attachment_path % (ofc_network_id, ofc_port_id) + return self.client.delete(path) diff --git a/quantum/plugins/nec/extensions/__init__.py b/quantum/plugins/nec/extensions/__init__.py new file mode 100644 index 0000000000..362a360682 --- /dev/null +++ b/quantum/plugins/nec/extensions/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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. diff --git a/quantum/plugins/nec/extensions/packetfilter.py b/quantum/plugins/nec/extensions/packetfilter.py new file mode 100644 index 0000000000..d92e8aae1e --- /dev/null +++ b/quantum/plugins/nec/extensions/packetfilter.py @@ -0,0 +1,131 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +from quantum.api.v2 import attributes +from quantum.api.v2 import base +from quantum.extensions import extensions +from quantum.manager import QuantumManager +from quantum import quota +from quantum.openstack.common import cfg + + +quota_packet_filter_opts = [ + cfg.IntOpt('quota_packet_filter', + default=100, + help="number of packet_filters allowed per tenant, " + "-1 for unlimited") +] +# Register the configuration options +cfg.CONF.register_opts(quota_packet_filter_opts, 'QUOTAS') + + +PACKET_FILTER_ACTION_REGEX = "(?i)^(allow|accept|drop|deny)$" +PACKET_FILTER_NUMBER_REGEX = "(?i)^(0x[0-9a-fA-F]+|[0-9]+)$" +PACKET_FILTER_PROTOCOL_REGEX = "(?i)^(icmp|tcp|udp|arp|0x[0-9a-fA-F]+|[0-9]+)$" +PACKET_FILTER_ATTR_MAP = { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:regex': attributes.UUID_PATTERN}, + 'is_visible': True}, + 'name': {'allow_post': True, 'allow_put': True, 'default': '', + 'is_visible': True}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'is_visible': True}, + 'network_id': {'allow_post': True, 'allow_put': False, + 'validate': {'type:regex': attributes.UUID_PATTERN}, + 'is_visible': True}, + 'admin_state_up': {'allow_post': True, 'allow_put': True, + 'default': True, + 'convert_to': attributes.convert_to_boolean, + 'validate': {'type:boolean': None}, + 'is_visible': True}, + 'status': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + 'action': {'allow_post': True, 'allow_put': True, + 'validate': {'type:regex': PACKET_FILTER_ACTION_REGEX}, + 'is_visible': True}, + 'priority': {'allow_post': True, 'allow_put': True, + 'validate': {'type:regex': PACKET_FILTER_NUMBER_REGEX}, + 'is_visible': True}, + 'in_port': {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:regex': attributes.UUID_PATTERN}, + 'is_visible': True}, + 'src_mac': {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:mac_address': None}, + 'is_visible': True}, + 'dst_mac': {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:mac_address': None}, + 'is_visible': True}, + 'eth_type': {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:regex': PACKET_FILTER_NUMBER_REGEX}, + 'is_visible': True}, + 'src_cidr': {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:subnet': None}, + 'is_visible': True}, + 'dst_cidr': {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:subnet': None}, + 'is_visible': True}, + 'protocol': {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:regex': PACKET_FILTER_PROTOCOL_REGEX}, + 'is_visible': True}, + 'src_port': {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:regex': PACKET_FILTER_NUMBER_REGEX}, + 'is_visible': True}, + 'dst_port': {'allow_post': True, 'allow_put': True, + 'default': attributes.ATTR_NOT_SPECIFIED, + 'validate': {'type:regex': PACKET_FILTER_NUMBER_REGEX}, + 'is_visible': True}, +} + + +class Packetfilter(object): + + def __init__(self): + pass + + def get_name(self): + return "PacketFilters" + + def get_alias(self): + return "PacketFilters" + + def get_description(self): + return "PacketFilters" + + def get_namespace(self): + return "http://www.nec.co.jp/api/ext/packet_filter/v2.0" + + def get_updated(self): + return "2012-07-24T00:00:00+09:00" + + def get_resources(self): + resource = base.create_resource('packet_filters', 'packet_filter', + QuantumManager.get_plugin(), + PACKET_FILTER_ATTR_MAP) + qresource = quota.CountableResource('packet_filter', + quota._count_resource, + 'quota_packet_filter') + quota.QUOTAS.register_resource(qresource) + return [extensions.ResourceExtension('packet_filters', resource)] diff --git a/quantum/plugins/nec/nec_plugin.py b/quantum/plugins/nec/nec_plugin.py new file mode 100644 index 0000000000..d4d8523792 --- /dev/null +++ b/quantum/plugins/nec/nec_plugin.py @@ -0,0 +1,505 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import logging + +from quantum import context +from quantum.common import topics +from quantum.openstack.common import rpc +from quantum.openstack.common.rpc import dispatcher +from quantum.plugins.nec import ofc_manager +from quantum.plugins.nec.common import config +from quantum.plugins.nec.common import exceptions as nexc +from quantum.plugins.nec.db import api as ndb +from quantum.plugins.nec.db import nec_plugin_base + +LOG = logging.getLogger(__name__) + + +class OperationalStatus: + """Enumeration for operational status. + + ACTIVE: The resource is available. + DOWN: The resource is not operational. This might indicate + admin_status_up=False, or lack of OpenFlow info for the port. + BUILD: The plugin is creating the resource. + ERROR: Some error occured. + """ + ACTIVE = "ACTIVE" + DOWN = "DOWN" + BUILD = "BUILD" + ERROR = "ERROR" + + +class NECPluginV2(nec_plugin_base.NECPluginV2Base): + """NECPluginV2 controls an OpenFlow Controller. + + The Quantum NECPluginV2 maps L2 logical networks to L2 virtualized networks + on an OpenFlow enabled network. An OpenFlow Controller (OFC) provides + L2 network isolation without VLAN and this plugin controls the OFC. + + NOTE: This is for Quantum API V2. Codes for V1.0 and V1.1 are available + at https://github.com/nec-openstack/quantum-openflow-plugin . + """ + + def __init__(self): + ndb.initialize() + self.ofc = ofc_manager.OFCManager() + + self.packet_filter_enabled = (config.OFC.enable_packet_filter and + self.ofc.driver.filter_supported) + if self.packet_filter_enabled: + self.supported_extension_aliases = ["PacketFilters"] + + self.setup_rpc() + + def setup_rpc(self): + self.topic = topics.PLUGIN + self.conn = rpc.create_connection(new=True) + self.callbacks = NECPluginV2RPCCallbacks(self) + self.dispatcher = self.callbacks.create_rpc_dispatcher() + self.conn.create_consumer(self.topic, self.dispatcher, fanout=False) + # Consume from all consumers in a thread + self.conn.consume_in_thread() + + def _update_resource_status(self, context, resource, id, status): + """Update status of specified resource.""" + request = {} + request[resource] = dict(status=status) + obj_updater = getattr(super(NECPluginV2, self), "update_%s" % resource) + obj_updater(context, id, request) + + def activate_port_if_ready(self, context, port, network=None): + """Activate port by creating port on OFC if ready. + + Activate port and packet_filters associated with the port. + Conditions to activate port on OFC are: + * port admin_state is UP + * network admin_state is UP + * portinfo are available (to identify port on OFC) + """ + if not network: + network = super(NECPluginV2, self).get_network(context, + port['network_id']) + + port_status = OperationalStatus.ACTIVE + if not port['admin_state_up']: + LOG.debug("activate_port_if_ready(): skip, " + "port.admin_state_up is False.") + port_status = OperationalStatus.DOWN + elif not network['admin_state_up']: + LOG.debug("activate_port_if_ready(): skip, " + "network.admin_state_up is False.") + port_status = OperationalStatus.DOWN + elif not ndb.get_portinfo(port['id']): + LOG.debug("activate_port_if_ready(): skip, " + "no portinfo for this port.") + port_status = OperationalStatus.DOWN + + # activate packet_filters before creating port on OFC. + if self.packet_filter_enabled: + if port_status is OperationalStatus.ACTIVE: + filters = dict(in_port=[port['id']], + status=[OperationalStatus.DOWN], + admin_state_up=[True]) + pfs = (super(NECPluginV2, self). + get_packet_filters(context, filters=filters)) + for pf in pfs: + self._activate_packet_filter_if_ready(context, pf, + network=network, + in_port=port) + + if port_status in [OperationalStatus.ACTIVE]: + if self.ofc.exists_ofc_port(port['id']): + LOG.debug("activate_port_if_ready(): skip, " + "ofc_port already exists.") + else: + try: + self.ofc.create_ofc_port(port['tenant_id'], + port['network_id'], + port['id']) + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + reason = "create_ofc_port() failed due to %s" % exc + LOG.error(reason) + port_status = OperationalStatus.ERROR + + if port_status is not port['status']: + self._update_resource_status(context, "port", port['id'], + port_status) + + def deactivate_port(self, context, port): + """Deactivate port by deleting port from OFC if exists. + + Deactivate port and packet_filters associated with the port. + """ + port_status = OperationalStatus.DOWN + if self.ofc.exists_ofc_port(port['id']): + try: + self.ofc.delete_ofc_port(port['tenant_id'], + port['network_id'], + port['id']) + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + reason = "delete_ofc_port() failed due to %s" % exc + LOG.error(reason) + port_status = OperationalStatus.ERROR + else: + LOG.debug("deactivate_port(): skip, ofc_port does not " + "exist.") + + if port_status is not port['status']: + self._update_resource_status(context, "port", port['id'], + port_status) + + # deactivate packet_filters after the port has deleted from OFC. + if self.packet_filter_enabled: + filters = dict(in_port=[port['id']], + status=[OperationalStatus.ACTIVE]) + pfs = super(NECPluginV2, self).get_packet_filters(context, + filters=filters) + for pf in pfs: + self._deactivate_packet_filter(context, pf) + + # Quantm Plugin Basic methods + + def create_network(self, context, network): + """Create a new network entry on DB, and create it on OFC.""" + LOG.debug("NECPluginV2.create_network() called, " + "network=%s ." % network) + new_net = super(NECPluginV2, self).create_network(context, network) + self._update_resource_status(context, "network", new_net['id'], + OperationalStatus.BUILD) + + try: + if not self.ofc.exists_ofc_tenant(new_net['tenant_id']): + self.ofc.create_ofc_tenant(new_net['tenant_id']) + self.ofc.create_ofc_network(new_net['tenant_id'], new_net['id'], + new_net['name']) + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + reason = "create_network() failed due to %s" % exc + LOG.error(reason) + self._update_resource_status(context, "network", new_net['id'], + OperationalStatus.ERROR) + else: + self._update_resource_status(context, "network", new_net['id'], + OperationalStatus.ACTIVE) + + return new_net + + def update_network(self, context, id, network): + """Update network and handle resources associated with the network. + + Update network entry on DB. If 'admin_state_up' was changed, activate + or deactivate ports and packetfilters associated with the network. + """ + LOG.debug("NECPluginV2.update_network() called, " + "id=%s network=%s ." % (id, network)) + old_net = super(NECPluginV2, self).get_network(context, id) + new_net = super(NECPluginV2, self).update_network(context, id, network) + + changed = (old_net['admin_state_up'] is not new_net['admin_state_up']) + if changed and not new_net['admin_state_up']: + self._update_resource_status(context, "network", id, + OperationalStatus.DOWN) + # disable all active ports and packet_filters of the network + filters = dict(network_id=[id], status=[OperationalStatus.ACTIVE]) + ports = super(NECPluginV2, self).get_ports(context, + filters=filters) + for port in ports: + self.deactivate_port(context, port) + if self.packet_filter_enabled: + pfs = (super(NECPluginV2, self). + get_packet_filters(context, filters=filters)) + for pf in pfs: + self._deactivate_packet_filter(context, pf) + elif changed and new_net['admin_state_up']: + self._update_resource_status(context, "network", id, + OperationalStatus.ACTIVE) + # enable ports and packet_filters of the network + filters = dict(network_id=[id], status=[OperationalStatus.DOWN], + admin_state_up=[True]) + ports = super(NECPluginV2, self).get_ports(context, + filters=filters) + for port in ports: + self.activate_port_if_ready(context, port, new_net) + if self.packet_filter_enabled: + pfs = (super(NECPluginV2, self). + get_packet_filters(context, filters=filters)) + for pf in pfs: + self._activate_packet_filter_if_ready(context, pf, new_net) + + return new_net + + def delete_network(self, context, id): + """Delete network and packet_filters associated with the network. + + Delete network entry from DB and OFC. Then delete packet_filters + associated with the network. If the network is the last resource + of the tenant, delete unnessary ofc_tenant. + """ + LOG.debug("NECPluginV2.delete_network() called, id=%s .", id) + net = super(NECPluginV2, self).get_network(context, id) + tenant_id = net['tenant_id'] + + # get packet_filters associated with the network + if self.packet_filter_enabled: + filters = dict(network_id=[id]) + pfs = (super(NECPluginV2, self). + get_packet_filters(context, filters=filters)) + + super(NECPluginV2, self).delete_network(context, id) + try: + self.ofc.delete_ofc_network(tenant_id, id) + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + reason = "delete_network() failed due to %s" % exc + # NOTE: The OFC configuration of this network could be remained + # as an orphan resource. But, it does NOT harm any other + # resources, so this plugin just warns. + LOG.warn(reason) + + # delete all packet_filters of the network + if self.packet_filter_enabled: + for pf in pfs: + self.delete_packet_filter(context, pf['id']) + + # delete unnessary ofc_tenant + filters = dict(tenant_id=[tenant_id]) + nets = super(NECPluginV2, self).get_networks(context, filters=filters) + if len(nets) == 0: + try: + self.ofc.delete_ofc_tenant(tenant_id) + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + reason = "delete_ofc_tenant() failed due to %s" % exc + LOG.warn(reason) + + def create_port(self, context, port): + """Create a new port entry on DB, then try to activate it.""" + LOG.debug("NECPluginV2.create_port() called, port=%s ." % port) + new_port = super(NECPluginV2, self).create_port(context, port) + self._update_resource_status(context, "port", new_port['id'], + OperationalStatus.BUILD) + + self.activate_port_if_ready(context, new_port) + + return new_port + + def update_port(self, context, id, port): + """Update port, and handle packetfilters associated with the port. + + Update network entry on DB. If admin_state_up was changed, activate + or deactivate the port and packetfilters associated with it. + """ + LOG.debug("NECPluginV2.update_port() called, " + "id=%s port=%s ." % (id, port)) + old_port = super(NECPluginV2, self).get_port(context, id) + new_port = super(NECPluginV2, self).update_port(context, id, port) + + changed = (old_port['admin_state_up'] is + not new_port['admin_state_up']) + if changed: + if new_port['admin_state_up']: + self.activate_port_if_ready(context, new_port) + else: + self.deactivate_port(context, old_port) + + return new_port + + def delete_port(self, context, id): + """Delete port and packet_filters associated with the port.""" + LOG.debug("NECPluginV2.delete_port() called, id=%s ." % id) + port = super(NECPluginV2, self).get_port(context, id) + + self.deactivate_port(context, port) + + # delete all packet_filters of the port + if self.packet_filter_enabled: + filters = dict(port_id=[id]) + pfs = (super(NECPluginV2, self). + get_packet_filters(context, filters=filters)) + for packet_filter in pfs: + self.delete_packet_filter(context, packet_filter['id']) + + super(NECPluginV2, self).delete_port(context, id) + + # For PacketFilter Extension + + def _activate_packet_filter_if_ready(self, context, packet_filter, + network=None, in_port=None): + """Activate packet_filter by creating filter on OFC if ready. + + Conditions to create packet_filter on OFC are: + * packet_filter admin_state is UP + * network admin_state is UP + * (if 'in_port' is specified) portinfo is available + """ + net_id = packet_filter['network_id'] + if not network: + network = super(NECPluginV2, self).get_network(context, net_id) + in_port_id = packet_filter.get("in_port") + if in_port_id and not in_port: + in_port = super(NECPluginV2, self).get_port(context, in_port_id) + + pf_status = OperationalStatus.ACTIVE + if not packet_filter['admin_state_up']: + LOG.debug("_activate_packet_filter_if_ready(): skip, " + "packet_filter.admin_state_up is False.") + pf_status = OperationalStatus.DOWN + elif not network['admin_state_up']: + LOG.debug("_activate_packet_filter_if_ready(): skip, " + "network.admin_state_up is False.") + pf_status = OperationalStatus.DOWN + elif in_port_id and in_port_id is in_port.get('id'): + LOG.debug("_activate_packet_filter_if_ready(): skip, " + "invalid in_port_id.") + pf_status = OperationalStatus.DOWN + elif in_port_id and not ndb.get_portinfo(in_port_id): + LOG.debug("_activate_packet_filter_if_ready(): skip, " + "no portinfo for in_port.") + pf_status = OperationalStatus.DOWN + + if pf_status in [OperationalStatus.ACTIVE]: + if self.ofc.exists_ofc_packet_filter(packet_filter['id']): + LOG.debug("_activate_packet_filter_if_ready(): skip, " + "ofc_packet_filter already exists.") + else: + try: + (self.ofc. + create_ofc_packet_filter(packet_filter['tenant_id'], + packet_filter['network_id'], + packet_filter['id'], + packet_filter)) + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + reason = ("create_ofc_packet_filter() failed due to " + "%s" % exc) + LOG.error(reason) + pf_status = OperationalStatus.ERROR + + if pf_status is not packet_filter['status']: + self._update_resource_status(context, "packet_filter", + packet_filter['id'], pf_status) + + def _deactivate_packet_filter(self, context, packet_filter): + """Deactivate packet_filter by deleting filter from OFC if exixts.""" + pf_status = OperationalStatus.DOWN + if not self.ofc.exists_ofc_packet_filter(packet_filter['id']): + LOG.debug("_deactivate_packet_filter(): skip, " + "ofc_packet_filter does not exist.") + else: + try: + self.ofc.delete_ofc_packet_filter(packet_filter['tenant_id'], + packet_filter['network_id'], + packet_filter['id']) + except (nexc.OFCException, nexc.OFCConsistencyBroken) as exc: + reason = ("delete_ofc_packet_filter() failed due to " + "%s" % exc) + LOG.error(reason) + pf_status = OperationalStatus.ERROR + + if pf_status is not packet_filter['status']: + self._update_resource_status(context, "packet_filter", + packet_filter['id'], pf_status) + + def create_packet_filter(self, context, packet_filter): + """Create a new packet_filter entry on DB, then try to activate it.""" + LOG.debug("NECPluginV2.create_packet_filter() called, " + "packet_filter=%s ." % packet_filter) + new_pf = super(NECPluginV2, self).create_packet_filter(context, + packet_filter) + self._update_resource_status(context, "packet_filter", new_pf['id'], + OperationalStatus.BUILD) + + self._activate_packet_filter_if_ready(context, new_pf) + + return new_pf + + def update_packet_filter(self, context, id, packet_filter): + """Update packet_filter entry on DB, and recreate it if changed. + + If any rule of the packet_filter was changed, recreate it on OFC. + """ + LOG.debug("NECPluginV2.update_packet_filter() called, " + "id=%s packet_filter=%s ." % (id, packet_filter)) + old_pf = super(NECPluginV2, self).get_packet_filter(context, id) + new_pf = super(NECPluginV2, self).update_packet_filter(context, id, + packet_filter) + + changed = False + exclude_items = ["id", "name", "tenant_id", "network_id", "status"] + for key in new_pf['packet_filter'].keys(): + if key not in exclude_items: + if old_pf[key] is not new_pf[key]: + changed = True + break + + if changed: + self._deactivate_packet_filter(context, old_pf) + self._activate_packet_filter_if_ready(context, new_pf) + + return new_pf + + def delete_packet_filter(self, context, id): + """Deactivate and delete packet_filter.""" + LOG.debug("NECPluginV2.delete_packet_filter() called, id=%s ." % id) + pf = super(NECPluginV2, self).get_packet_filter(context, id) + self._deactivate_packet_filter(context, pf) + + super(NECPluginV2, self).delete_packet_filter(context, id) + + +class NECPluginV2RPCCallbacks(): + + RPC_API_VERSION = '1.0' + + def __init__(self, plugin): + self.plugin = plugin + self.admin_context = context.get_admin_context() + + def create_rpc_dispatcher(self): + '''Get the rpc dispatcher for this manager. + + If a manager would like to set an rpc API version, or support more than + one class as the target of rpc messages, override this method. + ''' + return dispatcher.RpcDispatcher([self]) + + def update_ports(self, rpc_context, **kwargs): + """Update ports' information and activate/deavtivate them. + + Expected input format is: + {'topic': 'q-agent-notifier', + 'agent_id': 'nec-q-agent.' + , + 'datapath_id': , + 'port_added': [,...], + 'port_removed': [,...]} + """ + LOG.debug("NECPluginV2RPCCallbacks.update_ports() called, " + "kwargs=%s ." % kwargs) + topic = kwargs['topic'] + datapath_id = kwargs['datapath_id'] + for p in kwargs.get('port_added', []): + id = p['id'] + port = self.plugin.get_port(self.admin_context, id) + if port and ndb.get_portinfo(id): + ndb.del_portinfo(id) + self.plugin.deactivate_port(self.admin_context, port) + ndb.add_portinfo(id, datapath_id, p['port_no'], + mac=p.get('mac', '')) + self.plugin.activate_port_if_ready(self.admin_context, port) + for id in kwargs.get('port_removed', []): + port = self.plugin.get_port(self.admin_context, id) + if port and ndb.get_portinfo(id): + ndb.del_portinfo(id) + self.plugin.deactivate_port(self.admin_context, port) diff --git a/quantum/plugins/nec/ofc_driver_base.py b/quantum/plugins/nec/ofc_driver_base.py new file mode 100644 index 0000000000..d1c0996787 --- /dev/null +++ b/quantum/plugins/nec/ofc_driver_base.py @@ -0,0 +1,103 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +from abc import ABCMeta, abstractmethod + + +class OFCDriverBase(object): + """OpenFlow Controller (OFC) Driver Specification. + + OFCDriverBase defines the minimum set of methods required by this plugin. + It would be better that other methods like update_* are implemented. + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def create_tenant(self, description, tenant_id=None): + """Create a new tenant at OpenFlow Controller. + + :param description: A description of this tenant. + :param tenant_id: A hint of OFC tenant ID. + A driver could use this id as a OFC id or ignore it. + :returns: ID of the tenant created at OpenFlow Controller. + :raises: quantum.plugin.nec.common.exceptions.OFCException + """ + pass + + @abstractmethod + def delete_tenant(self, ofc_tenant_id): + """Delete a tenant at OpenFlow Controller. + + :raises: quantum.plugin.nec.common.exceptions.OFCException + """ + pass + + @abstractmethod + def create_network(self, ofc_tenant_id, description, network_id=None): + """Create a new network on specified OFC tenant at OpenFlow Controller. + + :param ofc_tenant_id: a OFC tenant ID in which a new network belongs. + :param description: A description of this network. + :param network_id: A hint of a OFC network ID. + :returns: ID of the network created at OpenFlow Controller. + :raises: quantum.plugin.nec.common.exceptions.OFCException + """ + pass + + @abstractmethod + def update_network(self, ofc_tenant_id, ofc_network_id, description): + """Update description of specified network. + + :raises: quantum.plugin.nec.common.exceptions.OFCException + """ + pass + + @abstractmethod + def delete_network(self, ofc_tenant_id, ofc_network_id): + """Delete a netwrok at OpenFlow Controller. + + :raises: quantum.plugin.nec.common.exceptions.OFCException + """ + pass + + @abstractmethod + def create_port(self, ofc_tenant_id, ofc_network_id, portinfo, + port_id=None): + """Create a new port on specified tenant and network at OFC. + + :param ofc_network_id: a OFC tenant ID in which a new port belongs. + :param portinfo: An OpenFlow information of this port. + {'datapath_id': Switch ID that a port connected. + 'port_no': Port Number that a port connected on a Swtich. + 'vlan_id': VLAN ID that a port tagging. + 'mac': Mac address. + } + :param port_id: A hint of a OFC port ID. + + :returns: ID of the port created at OpenFlow Controller. + :raises: quantum.plugin.nec.common.exceptions.OFCException + """ + pass + + @abstractmethod + def delete_port(self, ofc_tenant_id, ofc_network_id, ofc_port_id): + """Delete a port at OpenFlow Controller. + + :raises: quantum.plugin.nec.common.exceptions.OFCException + """ + pass diff --git a/quantum/plugins/nec/ofc_manager.py b/quantum/plugins/nec/ofc_manager.py new file mode 100644 index 0000000000..a7750970d5 --- /dev/null +++ b/quantum/plugins/nec/ofc_manager.py @@ -0,0 +1,150 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +from quantum.plugins.nec import drivers +from quantum.plugins.nec.common import config +from quantum.plugins.nec.common import exceptions as nexc +from quantum.plugins.nec.db import api as ndb +from quantum.plugins.nec.db import models as nmodels + + +class OFCManager(object): + """This class manages an OpenFlow Controller and map resources. + + This class manage an OpenFlow Controller (OFC) with a driver specified in + a configuration of this plugin. This keeps mappings between IDs on Quantum + and OFC for various entities such as Tenant, Network and Filter. A Port on + OFC is identified by a switch ID 'datapath_id' and a port number 'port_no' + of the switch. An ID named as 'ofc_*' is used to identify resource on OFC. + """ + resource_map = {'ofc_tenant': nmodels.OFCTenant, + 'ofc_network': nmodels.OFCNetwork, + 'ofc_port': nmodels.OFCPort, + 'ofc_packet_filter': nmodels.OFCFilter} + + def __init__(self): + self.driver = drivers.get_driver(config.OFC.driver)(config.OFC) + + def _get_ofc_id(self, resource, quantum_id): + model = self.resource_map[resource] + ofc_item = ndb.find_ofc_item(model, quantum_id) + if not ofc_item: + reason = "NotFound %s for quantum_id=%s." % (resource, quantum_id) + raise nexc.OFCConsistencyBroken(reason=reason) + return ofc_item.id + + def _exists_ofc_item(self, resource, quantum_id): + model = self.resource_map[resource] + if ndb.find_ofc_item(model, quantum_id): + return True + else: + return False + + # Tenant + + def create_ofc_tenant(self, tenant_id): + desc = "ID=%s at OpenStack." % tenant_id + ofc_tenant_id = self.driver.create_tenant(desc, tenant_id) + ndb.add_ofc_item(nmodels.OFCTenant, ofc_tenant_id, tenant_id) + + def exists_ofc_tenant(self, tenant_id): + return self._exists_ofc_item("ofc_tenant", tenant_id) + + def delete_ofc_tenant(self, tenant_id): + ofc_tenant_id = self._get_ofc_id("ofc_tenant", tenant_id) + + self.driver.delete_tenant(ofc_tenant_id) + ndb.del_ofc_item(nmodels.OFCTenant, ofc_tenant_id) + + # Network + + def create_ofc_network(self, tenant_id, network_id, network_name=None): + ofc_tenant_id = self._get_ofc_id("ofc_tenant", tenant_id) + + desc = "ID=%s Name=%s at Quantum." % (network_id, network_name) + ofc_net_id = self.driver.create_network(ofc_tenant_id, desc, + network_id) + ndb.add_ofc_item(nmodels.OFCNetwork, ofc_net_id, network_id) + + def update_ofc_network(self, tenant_id, network_id, network_name): + ofc_tenant_id = self._get_ofc_id("ofc_tenant", tenant_id) + ofc_net_id = self._get_ofc_id("ofc_network", network_id) + + desc = "ID=%s Name=%s at Quantum." % (network_id, network_name) + self.driver.update_network(ofc_tenant_id, ofc_net_id, desc) + + def exists_ofc_network(self, network_id): + return self._exists_ofc_item("ofc_network", network_id) + + def delete_ofc_network(self, tenant_id, network_id): + ofc_tenant_id = self._get_ofc_id("ofc_tenant", tenant_id) + ofc_net_id = self._get_ofc_id("ofc_network", network_id) + + self.driver.delete_network(ofc_tenant_id, ofc_net_id) + ndb.del_ofc_item(nmodels.OFCNetwork, ofc_net_id) + + # Port + + def create_ofc_port(self, tenant_id, network_id, port_id): + ofc_tenant_id = self._get_ofc_id("ofc_tenant", tenant_id) + ofc_net_id = self._get_ofc_id("ofc_network", network_id) + portinfo = ndb.get_portinfo(port_id) + if not portinfo: + raise nexc.PortInfoNotFound(id=port_id) + + ofc_port_id = self.driver.create_port(ofc_tenant_id, ofc_net_id, + portinfo, port_id) + ndb.add_ofc_item(nmodels.OFCPort, ofc_port_id, port_id) + + def exists_ofc_port(self, port_id): + return self._exists_ofc_item("ofc_port", port_id) + + def delete_ofc_port(self, tenant_id, network_id, port_id): + ofc_tenant_id = self._get_ofc_id("ofc_tenant", tenant_id) + ofc_net_id = self._get_ofc_id("ofc_network", network_id) + ofc_port_id = self._get_ofc_id("ofc_port", port_id) + + self.driver.delete_port(ofc_tenant_id, ofc_net_id, ofc_port_id) + ndb.del_ofc_item(nmodels.OFCPort, ofc_port_id) + + # PacketFilter + + def create_ofc_packet_filter(self, tenant_id, network_id, filter_id, + filter_dict): + ofc_tenant_id = self._get_ofc_id("ofc_tenant", tenant_id) + ofc_net_id = self._get_ofc_id("ofc_network", network_id) + in_port_id = filter_dict.get('in_port') + portinfo = None + if in_port_id: + portinfo = ndb.get_portinfo(in_port_id) + if not portinfo: + raise nexc.PortInfoNotFound(id=in_port_id) + + ofc_pf_id = self.driver.create_filter(ofc_tenant_id, ofc_net_id, + filter_dict, portinfo, filter_id) + ndb.add_ofc_item(nmodels.OFCFilter, ofc_pf_id, filter_id) + + def exists_ofc_packet_filter(self, filter_id): + return self._exists_ofc_item("ofc_packet_filter", filter_id) + + def delete_ofc_packet_filter(self, tenant_id, network_id, filter_id): + ofc_tenant_id = self._get_ofc_id("ofc_tenant", tenant_id) + ofc_net_id = self._get_ofc_id("ofc_network", network_id) + ofc_pf_id = self._get_ofc_id("ofc_packet_filter", filter_id) + + res = self.driver.delete_filter(ofc_tenant_id, ofc_net_id, ofc_pf_id) + ndb.del_ofc_item(nmodels.OFCFilter, ofc_pf_id) diff --git a/quantum/plugins/nec/run_tests.py b/quantum/plugins/nec/run_tests.py new file mode 100755 index 0000000000..5ef4086b28 --- /dev/null +++ b/quantum/plugins/nec/run_tests.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack, LLC +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Unittest runner for quantum NEC OpenFlow plugin + +This file should be run from the top dir in the quantum directory + +To run all tests:: + PLUGIN_DIR=quantum/plugins/nec ./run_tests.sh +""" + +import os +import sys + +from nose import config +from nose import core + +sys.path.append(os.getcwd()) +sys.path.append(os.path.dirname(__file__)) + +from quantum.common.test_lib import run_tests, test_config +import quantum.tests.unit + + +if __name__ == '__main__': + exit_status = False + + # if a single test case was specified, + # we should only invoked the tests once + invoke_once = len(sys.argv) > 1 + + test_config['plugin_name'] = "nec_plugin.NECPluginV2" + + cwd = os.getcwd() + c = config.Config(stream=sys.stdout, + env=os.environ, + verbosity=3, + includeExe=True, + traverseNamespace=True, + plugins=core.DefaultPluginManager()) + c.configureWhere(quantum.tests.unit.__path__) + exit_status = run_tests(c) + + if invoke_once: + sys.exit(0) + + os.chdir(cwd) + + working_dir = os.path.abspath("quantum/plugins/nec") + c = config.Config(stream=sys.stdout, + env=os.environ, + verbosity=3, + workingDir=working_dir) + exit_status = exit_status or run_tests(c) + + sys.exit(exit_status) diff --git a/quantum/plugins/nec/tests/__init__.py b/quantum/plugins/nec/tests/__init__.py new file mode 100644 index 0000000000..362a360682 --- /dev/null +++ b/quantum/plugins/nec/tests/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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. diff --git a/quantum/plugins/nec/tests/unit/__init__.py b/quantum/plugins/nec/tests/unit/__init__.py new file mode 100644 index 0000000000..362a360682 --- /dev/null +++ b/quantum/plugins/nec/tests/unit/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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. diff --git a/quantum/plugins/nec/tests/unit/stub_ofc_driver.py b/quantum/plugins/nec/tests/unit/stub_ofc_driver.py new file mode 100644 index 0000000000..c5f733229a --- /dev/null +++ b/quantum/plugins/nec/tests/unit/stub_ofc_driver.py @@ -0,0 +1,56 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +from quantum.plugins.nec import ofc_driver_base + + +class StubOFCDriver(ofc_driver_base.OFCDriverBase): + + def __init__(self, conf): + pass + + def create_tenant(self, description, tenant_id=None): + return "ofc-" + tenant_id[:-4] + + def delete_tenant(self, ofc_tenant_id): + pass + + def create_network(self, ofc_tenant_id, description, network_id=None): + return "ofc-" + network_id[:-4] + + def update_network(self, ofc_tenant_id, ofc_network_id, description): + pass + + def delete_network(self, ofc_tenant_id, ofc_network_id): + pass + + def create_port(self, ofc_tenant_id, ofc_network_id, info, port_id=None): + return "ofc-" + port_id[:-4] + + def delete_port(self, ofc_tenant_id, ofc_network_id, ofc_port_id): + pass + + @classmethod + def filter_supported(cls): + return True + + def create_filter(self, ofc_tenant_id, ofc_network_id, filter_dict, + portinfo=None, filter_id=None): + return "ofc-" + filter_id[:-4] + + def delete_filter(self, ofc_tenant_id, ofc_network_id, ofc_filter_id): + pass diff --git a/quantum/plugins/nec/tests/unit/test_config.py b/quantum/plugins/nec/tests/unit/test_config.py new file mode 100644 index 0000000000..ac8d758990 --- /dev/null +++ b/quantum/plugins/nec/tests/unit/test_config.py @@ -0,0 +1,39 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import unittest + +from quantum.plugins.nec.common import config + + +class ConfigurationTest(unittest.TestCase): + + def test_defaults(self): + self.assertEqual('sqlite://', config.DATABASE.sql_connection) + self.assertEqual('sqlite://', config.CONF.DATABASE.sql_connection) + self.assertEqual(-1, config.CONF.DATABASE.sql_max_retries) + self.assertEqual(2, config.CONF.DATABASE.reconnect_interval) + self.assertEqual('br-int', config.CONF.OVS.integration_bridge) + self.assertEqual(2, config.CONF.AGENT.polling_interval) + self.assertEqual('sudo', config.CONF.AGENT.root_helper) + self.assertEqual('127.0.0.1', config.CONF.OFC.host) + self.assertEqual('8888', config.CONF.OFC.port) + self.assertEqual('trema', config.CONF.OFC.driver) + self.assertTrue(config.CONF.OFC.enable_packet_filter) + self.assertFalse(config.CONF.OFC.use_ssl) + self.assertEqual(None, config.CONF.OFC.key_file) + self.assertEqual(None, config.CONF.OFC.cert_file) diff --git a/quantum/plugins/nec/tests/unit/test_db.py b/quantum/plugins/nec/tests/unit/test_db.py new file mode 100644 index 0000000000..d531e3f500 --- /dev/null +++ b/quantum/plugins/nec/tests/unit/test_db.py @@ -0,0 +1,141 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import random +import unittest + +from quantum.common import utils +from quantum.plugins.nec.common import exceptions as nexc +from quantum.plugins.nec.db import api as ndb +from quantum.plugins.nec.db import models as nmodels + + +class NECPluginV2DBTest(unittest.TestCase): + """Class conisting of NECPluginV2 DB unit tests""" + + def setUp(self): + """Setup for tests""" + ndb.initialize() + + def tearDown(self): + """Tear Down""" + ndb.clear_db() + + def get_ofc_item_random_params(self): + """create random parameters for ofc_item test""" + ofc_id = utils.str_uuid() + quantum_id = utils.str_uuid() + none = utils.str_uuid() + return ofc_id, quantum_id, none + + def testa_add_ofc_item(self): + """test add OFC item""" + o, q, n = self.get_ofc_item_random_params() + tenant = ndb.add_ofc_item(nmodels.OFCTenant, o, q) + self.assertEqual(tenant.id, o) + self.assertEqual(tenant.quantum_id, q) + + exception_raised = False + try: + ndb.add_ofc_item(nmodels.OFCTenant, o, q) + except nexc.NECDBException: + exception_raised = True + self.assertTrue(exception_raised) + + def testb_get_ofc_item(self): + """test get OFC item""" + o, q, n = self.get_ofc_item_random_params() + ndb.add_ofc_item(nmodels.OFCTenant, o, q) + tenant = ndb.get_ofc_item(nmodels.OFCTenant, o) + self.assertEqual(tenant.id, o) + self.assertEqual(tenant.quantum_id, q) + + tenant_none = ndb.get_ofc_item(nmodels.OFCTenant, n) + self.assertEqual(None, tenant_none) + + def testc_find_ofc_item(self): + """test find OFC item""" + o, q, n = self.get_ofc_item_random_params() + ndb.add_ofc_item(nmodels.OFCTenant, o, q) + tenant = ndb.find_ofc_item(nmodels.OFCTenant, q) + self.assertEqual(tenant.id, o) + self.assertEqual(tenant.quantum_id, q) + + tenant_none = ndb.find_ofc_item(nmodels.OFCTenant, n) + self.assertEqual(None, tenant_none) + + def testc_del_ofc_item(self): + """test delete OFC item""" + o, q, n = self.get_ofc_item_random_params() + ndb.add_ofc_item(nmodels.OFCTenant, o, q) + ndb.del_ofc_item(nmodels.OFCTenant, o) + + tenant_none = ndb.get_ofc_item(nmodels.OFCTenant, q) + self.assertEqual(None, tenant_none) + tenant_none = ndb.find_ofc_item(nmodels.OFCTenant, q) + self.assertEqual(None, tenant_none) + + def get_portinfo_random_params(self): + """create random parameters for portinfo test""" + port_id = utils.str_uuid() + datapath_id = hex(random.randint(0, 0xffffffff)) + port_no = random.randint(1, 100) + vlan_id = random.randint(0, 4095) + mac = ':'.join(["%02x" % random.randint(0, 0xff) for x in range(6)]) + none = utils.str_uuid() + return port_id, datapath_id, port_no, vlan_id, mac, none + + def testd_add_portinfo(self): + """test add portinfo""" + i, d, p, v, m, n = self.get_portinfo_random_params() + portinfo = ndb.add_portinfo(i, d, p, v, m) + self.assertEqual(portinfo.id, i) + self.assertEqual(portinfo.datapath_id, d) + self.assertEqual(portinfo.port_no, p) + self.assertEqual(portinfo.vlan_id, v) + self.assertEqual(portinfo.mac, m) + + exception_raised = False + try: + ndb.add_portinfo(i, d, p, v, m) + except nexc.NECDBException: + exception_raised = True + self.assertTrue(exception_raised) + + def teste_get_portinfo(self): + """test get portinfo""" + i, d, p, v, m, n = self.get_portinfo_random_params() + ndb.add_portinfo(i, d, p, v, m) + portinfo = ndb.get_portinfo(i) + self.assertEqual(portinfo.id, i) + self.assertEqual(portinfo.datapath_id, d) + self.assertEqual(portinfo.port_no, p) + self.assertEqual(portinfo.vlan_id, v) + self.assertEqual(portinfo.mac, m) + + portinfo_none = ndb.get_portinfo(n) + self.assertEqual(None, portinfo_none) + + def testf_del_portinfo(self): + """test delete portinfo""" + i, d, p, v, m, n = self.get_portinfo_random_params() + ndb.add_portinfo(i, d, p, v, m) + portinfo = ndb.get_portinfo(i) + self.assertEqual(portinfo.id, i) + ndb.del_portinfo(i) + portinfo_none = ndb.get_portinfo(i) + self.assertEqual(None, portinfo_none) diff --git a/quantum/plugins/nec/tests/unit/test_nec_plugin_v2.py b/quantum/plugins/nec/tests/unit/test_nec_plugin_v2.py new file mode 100644 index 0000000000..66fada40ca --- /dev/null +++ b/quantum/plugins/nec/tests/unit/test_nec_plugin_v2.py @@ -0,0 +1,62 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import quantum.openstack.common.rpc +from quantum.plugins.nec.common import config +from quantum.tests.unit import test_db_plugin + + +class NECPluginTestBase(object): + + def setUp(self): + # Make sure at each test a new instance of the plugin is returned + test_db_plugin.QuantumManager._instance = None + + self._tenant_id = 'test-tenant' + + json_deserializer = test_db_plugin.JSONDeserializer() + self._deserializers = { + 'application/json': json_deserializer, + } + + plugin = 'quantum.plugins.nec.nec_plugin.NECPluginV2' + config.CONF.set_override('core_plugin', plugin) + driver = "quantum.plugins.nec.tests.unit.stub_ofc_driver.StubOFCDriver" + config.CONF.set_override('driver', driver, 'OFC') + config.CONF.set_override('rpc_backend', + 'quantum.openstack.common.rpc.impl_fake') + self.api = test_db_plugin.APIRouter() + self._skip_native_bulk = False + + +class TestPortsV2(NECPluginTestBase, test_db_plugin.TestPortsV2): + pass + + +class TestNetworksV2(NECPluginTestBase, test_db_plugin.TestNetworksV2): + pass + + +# NOTE: This plugin does not override methods for subnet. +#class TestSubnetsV2(NECPluginTestBase, test_db_plugin.TestSubnetsV2): +# pass + + +# TODO(r-mibu): write UT for packet_filters. +class TestPacketFiltersV2(NECPluginTestBase, + test_db_plugin.QuantumDbPluginV2TestCase): + pass diff --git a/quantum/plugins/nec/tests/unit/test_ofc_manager.py b/quantum/plugins/nec/tests/unit/test_ofc_manager.py new file mode 100644 index 0000000000..0eb2b79b70 --- /dev/null +++ b/quantum/plugins/nec/tests/unit/test_ofc_manager.py @@ -0,0 +1,160 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import unittest + +from quantum.common import utils +from quantum.plugins.nec.common import config +from quantum.plugins.nec.db import api as ndb +from quantum.plugins.nec.db import models as nmodels +from quantum.plugins.nec import ofc_manager + + +class OFCManagerTest(unittest.TestCase): + """Class conisting of OFCManager unit tests""" + + def setUp(self): + driver = "quantum.plugins.nec.tests.unit.stub_ofc_driver.StubOFCDriver" + config.CONF.set_override('driver', driver, 'OFC') + ndb.initialize() + self.ofc = ofc_manager.OFCManager() + + def tearDown(self): + ndb.clear_db() + + def get_random_params(self): + """create random parameters for portinfo test""" + tenant = utils.str_uuid() + network = utils.str_uuid() + port = utils.str_uuid() + _filter = utils.str_uuid() + none = utils.str_uuid() + return tenant, network, port, _filter, none + + def testa_create_ofc_tenant(self): + """test create ofc_tenant""" + t, n, p, f, none = self.get_random_params() + self.assertFalse(ndb.find_ofc_item(nmodels.OFCTenant, t)) + self.ofc.create_ofc_tenant(t) + self.assertTrue(ndb.find_ofc_item(nmodels.OFCTenant, t)) + tenant = ndb.find_ofc_item(nmodels.OFCTenant, t) + self.assertEqual(tenant.id, "ofc-" + t[:-4]) + + def testb_exists_ofc_tenant(self): + """test exists_ofc_tenant""" + t, n, p, f, none = self.get_random_params() + self.assertFalse(self.ofc.exists_ofc_tenant(t)) + self.ofc.create_ofc_tenant(t) + self.assertTrue(self.ofc.exists_ofc_tenant(t)) + + def testc_delete_ofc_tenant(self): + """test delete ofc_tenant""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.assertTrue(ndb.find_ofc_item(nmodels.OFCTenant, t)) + self.ofc.delete_ofc_tenant(t) + self.assertFalse(ndb.find_ofc_item(nmodels.OFCTenant, t)) + + def testd_create_ofc_network(self): + """test create ofc_network""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.assertFalse(ndb.find_ofc_item(nmodels.OFCNetwork, n)) + self.ofc.create_ofc_network(t, n) + self.assertTrue(ndb.find_ofc_item(nmodels.OFCNetwork, n)) + network = ndb.find_ofc_item(nmodels.OFCNetwork, n) + self.assertEqual(network.id, "ofc-" + n[:-4]) + + def teste_exists_ofc_network(self): + """test exists_ofc_network""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.assertFalse(self.ofc.exists_ofc_network(n)) + self.ofc.create_ofc_network(t, n) + self.assertTrue(self.ofc.exists_ofc_network(n)) + + def testf_delete_ofc_network(self): + """test delete ofc_network""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.ofc.create_ofc_network(t, n) + self.assertTrue(ndb.find_ofc_item(nmodels.OFCNetwork, n)) + self.ofc.delete_ofc_network(t, n) + self.assertFalse(ndb.find_ofc_item(nmodels.OFCNetwork, n)) + + def testg_create_ofc_port(self): + """test create ofc_port""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.ofc.create_ofc_network(t, n) + ndb.add_portinfo(p, "0xabc", 1, 65535, "00:11:22:33:44:55") + self.assertFalse(ndb.find_ofc_item(nmodels.OFCPort, p)) + self.ofc.create_ofc_port(t, n, p) + self.assertTrue(ndb.find_ofc_item(nmodels.OFCPort, p)) + port = ndb.find_ofc_item(nmodels.OFCPort, p) + self.assertEqual(port.id, "ofc-" + p[:-4]) + + def testh_exists_ofc_port(self): + """test exists_ofc_port""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.ofc.create_ofc_network(t, n) + ndb.add_portinfo(p, "0xabc", 2, 65535, "00:12:22:33:44:55") + self.assertFalse(self.ofc.exists_ofc_port(p)) + self.ofc.create_ofc_port(t, n, p) + self.assertTrue(self.ofc.exists_ofc_port(p)) + + def testi_delete_ofc_port(self): + """test delete ofc_port""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.ofc.create_ofc_network(t, n) + ndb.add_portinfo(p, "0xabc", 3, 65535, "00:13:22:33:44:55") + self.ofc.create_ofc_port(t, n, p) + self.assertTrue(ndb.find_ofc_item(nmodels.OFCPort, p)) + self.ofc.delete_ofc_port(t, n, p) + self.assertFalse(ndb.find_ofc_item(nmodels.OFCPort, p)) + + def testj_create_ofc_packet_filter(self): + """test create ofc_filter""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.ofc.create_ofc_network(t, n) + self.assertFalse(ndb.find_ofc_item(nmodels.OFCFilter, f)) + self.ofc.create_ofc_packet_filter(t, n, f, {}) + self.assertTrue(ndb.find_ofc_item(nmodels.OFCFilter, f)) + _filter = ndb.find_ofc_item(nmodels.OFCFilter, f) + self.assertEqual(_filter.id, "ofc-" + f[:-4]) + + def testk_exists_ofc_packet_filter(self): + """test exists_ofc_packet_filter""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.ofc.create_ofc_network(t, n) + self.assertFalse(self.ofc.exists_ofc_packet_filter(f)) + self.ofc.create_ofc_packet_filter(t, n, f, {}) + self.assertTrue(self.ofc.exists_ofc_packet_filter(f)) + + def testl_delete_ofc_packet_filter(self): + """test delete ofc_filter""" + t, n, p, f, none = self.get_random_params() + self.ofc.create_ofc_tenant(t) + self.ofc.create_ofc_network(t, n) + self.ofc.create_ofc_packet_filter(t, n, f, {}) + self.assertTrue(ndb.find_ofc_item(nmodels.OFCFilter, f)) + self.ofc.delete_ofc_packet_filter(t, n, f) + self.assertFalse(ndb.find_ofc_item(nmodels.OFCFilter, f)) diff --git a/quantum/plugins/nec/tests/unit/test_pfc_driver.py b/quantum/plugins/nec/tests/unit/test_pfc_driver.py new file mode 100644 index 0000000000..eb2f064e54 --- /dev/null +++ b/quantum/plugins/nec/tests/unit/test_pfc_driver.py @@ -0,0 +1,158 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import mox +import unittest + +from quantum.common import utils +from quantum.plugins.nec import drivers +from quantum.plugins.nec.db import models as nmodels +from quantum.plugins.nec.common import ofc_client as ofc + + +class TestConfig(object): + """Configuration for this test""" + host = '127.0.0.1' + port = 8888 + use_ssl = False + key_file = None + cert_file = None + + +def _ofc(id): + """OFC ID converter""" + return "ofc-%s" % id + + +class PFCDriverTestBase(unittest.TestCase): + + def setUp(self): + self.mox = mox.Mox() + self.driver = drivers.get_driver("pfc")(TestConfig) + self.mox.StubOutWithMock(ofc.OFCClient, 'do_request') + + def tearDown(self): + self.mox.UnsetStubs() + + def get_ofc_item_random_params(self): + """create random parameters for ofc_item test""" + tenant_id = utils.str_uuid() + network_id = utils.str_uuid() + port_id = utils.str_uuid() + portinfo = nmodels.PortInfo(id=port_id, datapath_id="0x123456789", + port_no=1234, vlan_id=321, + mac="11:22:33:44:55:66") + return tenant_id, network_id, portinfo + + def testa_create_tenant(self): + t, n, p = self.get_ofc_item_random_params() + description = "desc of %s" % t + + path = "/tenants" + body = {'id': t, 'description': description} + tenant = {'id': _ofc(t)} + ofc.OFCClient.do_request("POST", path, body=body).AndReturn(tenant) + self.mox.ReplayAll() + + ret = self.driver.create_tenant(description, t) + self.mox.VerifyAll() + self.assertEqual(ret, _ofc(t)) + + def testb_update_tenant(self): + t, n, p = self.get_ofc_item_random_params() + description = "new desc of %s" % t + + path = "/tenants/%s" % _ofc(t) + body = {'description': description} + ofc.OFCClient.do_request("PUT", path, body=body) + self.mox.ReplayAll() + + self.driver.update_tenant(_ofc(t), description) + self.mox.VerifyAll() + + def testc_delete_tenant(self): + t, n, p = self.get_ofc_item_random_params() + + path = "/tenants/%s" % _ofc(t) + ofc.OFCClient.do_request("DELETE", path) + self.mox.ReplayAll() + + self.driver.delete_tenant(_ofc(t)) + self.mox.VerifyAll() + + def testd_create_network(self): + t, n, p = self.get_ofc_item_random_params() + description = "desc of %s" % n + + path = "/tenants/%s/networks" % _ofc(t) + body = {'id': n, 'description': description} + network = {'id': _ofc(n)} + ofc.OFCClient.do_request("POST", path, body=body).AndReturn(network) + self.mox.ReplayAll() + + ret = self.driver.create_network(_ofc(t), description, n) + self.mox.VerifyAll() + self.assertEqual(ret, _ofc(n)) + + def teste_update_network(self): + t, n, p = self.get_ofc_item_random_params() + description = "desc of %s" % n + + path = "/tenants/%s/networks/%s" % (_ofc(t), _ofc(n)) + body = {'description': description} + ofc.OFCClient.do_request("PUT", path, body=body) + self.mox.ReplayAll() + + self.driver.update_network(_ofc(t), _ofc(n), description) + self.mox.VerifyAll() + + def testf_delete_network(self): + t, n, p = self.get_ofc_item_random_params() + + path = "/tenants/%s/networks/%s" % (_ofc(t), _ofc(n)) + ofc.OFCClient.do_request("DELETE", path) + self.mox.ReplayAll() + + self.driver.delete_network(_ofc(t), _ofc(n)) + self.mox.VerifyAll() + + def testg_create_port(self): + t, n, p = self.get_ofc_item_random_params() + + path = "/tenants/%s/networks/%s/ports" % (_ofc(t), _ofc(n)) + body = {'id': p.id, + 'datapath_id': p.datapath_id, + 'port': str(p.port_no), + 'vid': str(p.vlan_id)} + port = {'id': _ofc(p.id)} + ofc.OFCClient.do_request("POST", path, body=body).AndReturn(port) + self.mox.ReplayAll() + + ret = self.driver.create_port(_ofc(t), _ofc(n), p, p.id) + self.mox.VerifyAll() + self.assertEqual(ret, _ofc(p.id)) + + def testh_delete_port(self): + t, n, p = self.get_ofc_item_random_params() + + path = "/tenants/%s/networks/%s/ports/%s" % (_ofc(t), _ofc(n), + _ofc(p.id)) + ofc.OFCClient.do_request("DELETE", path) + self.mox.ReplayAll() + + self.driver.delete_port(_ofc(t), _ofc(n), _ofc(p.id)) + self.mox.VerifyAll() diff --git a/quantum/plugins/nec/tests/unit/test_trema_driver.py b/quantum/plugins/nec/tests/unit/test_trema_driver.py new file mode 100644 index 0000000000..29293864d4 --- /dev/null +++ b/quantum/plugins/nec/tests/unit/test_trema_driver.py @@ -0,0 +1,236 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 NEC Corporation. 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: Ryota MIBU + +import mox +import unittest + +from quantum.common import utils +from quantum.plugins.nec import drivers +from quantum.plugins.nec.db import models as nmodels +from quantum.plugins.nec.common import ofc_client + + +class TestConfig(object): + """Configuration for this test""" + host = '127.0.0.1' + port = 8888 + + +class TremaDriverTestBase(): + + driver_name = "trema" + + def setUp(self): + self.mox = mox.Mox() + self.driver = drivers.get_driver(self.driver_name)(TestConfig) + self.mox.StubOutWithMock(ofc_client.OFCClient, 'do_request') + + def tearDown(self): + self.mox.UnsetStubs() + + def get_ofc_item_random_params(self): + """create random parameters for ofc_item test""" + tenant_id = utils.str_uuid() + network_id = utils.str_uuid() + port_id = utils.str_uuid() + portinfo = nmodels.PortInfo(id=port_id, datapath_id="0x123456789", + port_no=1234, vlan_id=321, + mac="11:22:33:44:55:66") + return tenant_id, network_id, portinfo + + +class TremaDriverNetworkTestBase(TremaDriverTestBase): + + def testa_create_network(self): + t, n, p = self.get_ofc_item_random_params() + description = "desc of %s" % n + + body = {'id': n, 'description': description} + ofc_client.OFCClient.do_request("POST", "/networks", body=body) + self.mox.ReplayAll() + + self.driver.create_network(t, description, n) + self.mox.VerifyAll() + + def testb_update_network(self): + t, n, p = self.get_ofc_item_random_params() + description = "desc of %s" % n + + body = {'description': description} + ofc_client.OFCClient.do_request("PUT", "/networks/%s" % n, body=body) + self.mox.ReplayAll() + + self.driver.update_network(t, n, description) + self.mox.VerifyAll() + + def testc_delete_network(self): + t, n, p = self.get_ofc_item_random_params() + + ofc_client.OFCClient.do_request("DELETE", "/networks/%s" % n) + self.mox.ReplayAll() + + self.driver.delete_network(t, n) + self.mox.VerifyAll() + + +class TremaPortBaseDriverTest(TremaDriverNetworkTestBase, unittest.TestCase): + + driver_name = "trema_port" + + def testd_create_port(self): + t, n, p = self.get_ofc_item_random_params() + + body = {'id': p.id, + 'datapath_id': p.datapath_id, + 'port': str(p.port_no), + 'vid': str(p.vlan_id)} + ofc_client.OFCClient.do_request("POST", + "/networks/%s/ports" % n, body=body) + self.mox.ReplayAll() + + self.driver.create_port(t, n, p, p.id) + self.mox.VerifyAll() + + def testd_delete_port(self): + t, n, p = self.get_ofc_item_random_params() + + ofc_client.OFCClient.do_request("DELETE", + "/networks/%s/ports/%s" % (n, p.id)) + self.mox.ReplayAll() + + self.driver.delete_port(t, n, p.id) + self.mox.VerifyAll() + + +class TremaPortMACBaseDriverTest(TremaDriverNetworkTestBase, + unittest.TestCase): + + driver_name = "trema_portmac" + + def testd_create_port(self): + t, n, p = self.get_ofc_item_random_params() + dummy_port = "dummy-%s" % p.id + + path_1 = "/networks/%s/ports" % n + body_1 = {'id': dummy_port, + 'datapath_id': p.datapath_id, + 'port': str(p.port_no), + 'vid': str(p.vlan_id)} + ofc_client.OFCClient.do_request("POST", path_1, body=body_1) + path_2 = "/networks/%s/ports/%s/attachments" % (n, dummy_port) + body_2 = {'id': p.id, 'mac': p.mac} + ofc_client.OFCClient.do_request("POST", path_2, body=body_2) + path_3 = "/networks/%s/ports/%s" % (n, dummy_port) + ofc_client.OFCClient.do_request("DELETE", path_3) + self.mox.ReplayAll() + + self.driver.create_port(t, n, p, p.id) + self.mox.VerifyAll() + + def testd_delete_port(self): + t, n, p = self.get_ofc_item_random_params() + dummy_port = "dummy-%s" % p.id + + path = "/networks/%s/ports/%s/attachments/%s" % (n, dummy_port, p.id) + ofc_client.OFCClient.do_request("DELETE", path) + self.mox.ReplayAll() + + self.driver.delete_port(t, n, p.id) + self.mox.VerifyAll() + + +class TremaMACBaseDriverTest(TremaDriverNetworkTestBase, unittest.TestCase): + + driver_name = "trema_mac" + + def testd_create_port(self): + t, n, p = self.get_ofc_item_random_params() + + path = "/networks/%s/attachments" % n + body = {'id': p.id, 'mac': p.mac} + ofc_client.OFCClient.do_request("POST", path, body=body) + self.mox.ReplayAll() + + self.driver.create_port(t, n, p, p.id) + self.mox.VerifyAll() + + def testd_delete_port(self): + t, n, p = self.get_ofc_item_random_params() + + path = "/networks/%s/attachments/%s" % (n, p.id) + ofc_client.OFCClient.do_request("DELETE", path) + self.mox.ReplayAll() + + self.driver.delete_port(t, n, p.id) + self.mox.VerifyAll() + + +class TremaFilterDriverTest(TremaDriverTestBase, unittest.TestCase): + + def get_ofc_item_random_params(self): + """create random parameters for ofc_item test""" + t, n, p = (super(TremaFilterDriverTest, self). + get_ofc_item_random_params()) + filter_id = utils.str_uuid() + filter_dict = {'tenant_id': t, + 'id': filter_id, + 'network_id': n, + 'priority': 123, + 'action': "ACCEPT", + 'in_port': p.id, + 'src_mac': p.mac, + 'dst_mac': "", + 'eth_type': 0, + 'src_cidr': "", + 'dst_cidr': "", + 'src_port': 0, + 'dst_port': 0, + 'protocol': "TCP", + 'admin_state_up': True, + 'status': "ACTIVE"} + filter_item = nmodels.PacketFilter(**filter_dict) + return t, n, p, filter_item + + def testa_create_filter(self): + t, n, p, f = self.get_ofc_item_random_params() + + ofp_wildcards = 'dl_vlan,dl_vlan_pcp,nw_tos,dl_dst,' + \ + 'nw_src:32,nw_dst:32,tp_src,tp_dst' + body = {'id': f.id, + 'action': 'ALLOW', + 'priority': 123, + 'slice': n, + 'in_datapath_id': '0x123456789', + 'in_port': 1234, + 'nw_proto': '0x6', + 'dl_type': '0x800', + 'dl_src': p.mac, + 'ofp_wildcards': ofp_wildcards} + ofc_client.OFCClient.do_request("POST", "/filters", body=body) + self.mox.ReplayAll() + + self.driver.create_filter(t, n, f, p, f.id) + self.mox.VerifyAll() + + def testb_delete_filter(self): + t, n, p, f = self.get_ofc_item_random_params() + + ofc_client.OFCClient.do_request("DELETE", "/filters/%s" % f.id) + self.mox.ReplayAll() + + self.driver.delete_filter(t, n, f.id) + self.mox.VerifyAll() diff --git a/setup.py b/setup.py index 78238fd6f5..143c2c6c91 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ linuxbridge_plugin_config_path = 'etc/quantum/plugins/linuxbridge' nvp_plugin_config_path = 'etc/quantum/plugins/nicira' ryu_plugin_config_path = 'etc/quantum/plugins/ryu' meta_plugin_config_path = 'etc/quantum/plugins/metaplugin' +nec_plugin_config_path = 'etc/quantum/plugins/nec' DataFiles = [ (config_path, @@ -72,7 +73,8 @@ DataFiles = [ ['etc/quantum/plugins/nicira/nvp.ini']), (ryu_plugin_config_path, ['etc/quantum/plugins/ryu/ryu.ini']), (meta_plugin_config_path, - ['etc/quantum/plugins/metaplugin/metaplugin.ini']) + ['etc/quantum/plugins/metaplugin/metaplugin.ini']), + (nec_plugin_config_path, ['etc/quantum/plugins/nec/nec.ini']), ] setuptools.setup( @@ -102,6 +104,8 @@ setuptools.setup( 'quantum.plugins.openvswitch.agent.ovs_quantum_agent:main', 'quantum-ryu-agent = ' 'quantum.plugins.ryu.agent.ryu_quantum_agent:main', + 'quantum-nec-agent = ' + 'quantum.plugins.nec.agent.nec_quantum_agent:main', 'quantum-server = quantum.server:main', ] },