]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
blueprint mellanox-quantum-plugin
authorIrena Berezovsky <irenab@mellanox.com>
Mon, 11 Mar 2013 10:10:15 +0000 (12:10 +0200)
committerIrena Berezovsky <irenab@mellanox.com>
Mon, 13 May 2013 12:36:14 +0000 (15:36 +0300)
Implements Mellanox Quantum plugin.
This plugin implements Quantum v2 APIs with support for Mellanox embedded
switch functionality as part of the VPI (Ethernet/InfiniBand) HCA.

Change-Id: I22907dfec5b6cb8f6ad8c3b6e390abc4f8e0ac10

23 files changed:
bin/quantum-mlnx-agent [new file with mode: 0644]
etc/quantum/plugins/mlnx/mlnx_conf.ini [new file with mode: 0644]
quantum/plugins/mlnx/README [new file with mode: 0644]
quantum/plugins/mlnx/__init__.py [new file with mode: 0644]
quantum/plugins/mlnx/agent/__init__.py [new file with mode: 0644]
quantum/plugins/mlnx/agent/eswitch_quantum_agent.py [new file with mode: 0644]
quantum/plugins/mlnx/agent/utils.py [new file with mode: 0644]
quantum/plugins/mlnx/agent_notify_api.py [new file with mode: 0644]
quantum/plugins/mlnx/common/__init__.py [new file with mode: 0644]
quantum/plugins/mlnx/common/config.py [new file with mode: 0644]
quantum/plugins/mlnx/common/constants.py [new file with mode: 0644]
quantum/plugins/mlnx/common/exceptions.py [new file with mode: 0644]
quantum/plugins/mlnx/db/__init__.py [new file with mode: 0644]
quantum/plugins/mlnx/db/mlnx_db_v2.py [new file with mode: 0644]
quantum/plugins/mlnx/db/mlnx_models_v2.py [new file with mode: 0644]
quantum/plugins/mlnx/mlnx_plugin.py [new file with mode: 0644]
quantum/plugins/mlnx/rpc_callbacks.py [new file with mode: 0644]
quantum/tests/unit/mlnx/__init__.py [new file with mode: 0644]
quantum/tests/unit/mlnx/test_defaults.py [new file with mode: 0644]
quantum/tests/unit/mlnx/test_mlnx_db.py [new file with mode: 0644]
quantum/tests/unit/mlnx/test_mlnx_plugin.py [new file with mode: 0644]
quantum/tests/unit/mlnx/test_rpcapi.py [new file with mode: 0644]
setup.py

diff --git a/bin/quantum-mlnx-agent b/bin/quantum-mlnx-agent
new file mode 100644 (file)
index 0000000..263e103
--- /dev/null
@@ -0,0 +1,25 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import sys
+sys.path.insert(0, os.getcwd())
+
+from quantum.plugins.mlnx.agent.eswitch_quantum_agent import main
+
+
+main()
diff --git a/etc/quantum/plugins/mlnx/mlnx_conf.ini b/etc/quantum/plugins/mlnx/mlnx_conf.ini
new file mode 100644 (file)
index 0000000..f842fe4
--- /dev/null
@@ -0,0 +1,59 @@
+[MLNX]
+# (StrOpt) Type of network to allocate for tenant networks. The
+# default value is 'vlan'  You MUST configure network_vlan_ranges below
+# in order for tenant networks to provide connectivity between hosts.
+# Set to 'none' to disable creation of tenant networks.
+#
+# Default: tenant_network_type = vlan
+# Example: tenant_network_type = vlan
+
+# (ListOpt) Comma-separated list of
+# <physical_network>[:<vlan_min>:<vlan_max>] tuples enumerating ranges
+# of VLAN IDs on named physical networks that are available for
+# allocation. All physical networks listed are available for flat and
+# VLAN provider network creation. Specified ranges of VLAN IDs are
+# available for tenant network allocation if tenant_network_type is
+# 'vlan'. If empty, only local networks may be created.
+#
+# Default: network_vlan_ranges =
+# Example: network_vlan_ranges = default:1:100
+
+[DATABASE]
+# This line MUST be changed to actually run the plugin.
+# Example:
+# sql_connection = mysql://root:nova@127.0.0.1:3306/quantum_linux_bridge
+# 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
+
+[ESWITCH]
+# (ListOpt) Comma-separated list of
+# <physical_network>:<physical_interface> tuples mapping physical
+# network names to the agent's node-specific physical network
+# interfaces to be used for flat and VLAN networks. All physical
+# networks listed in network_vlan_ranges on the server should have
+# mappings to appropriate interfaces on each agent.
+#
+# Default: physical_interface_mappings =
+# Example: physical_interface_mappings = default:eth2
+
+# (StrOpt) Type of Network Interface to allocate for VM:
+# direct or hosdev according to libvirt terminology
+# Default: vnic_type = direct
+
+# (StrOpt) Eswitch daemon end point connection url
+# Default: daemon_endpoint = 'tcp://127.0.0.1:5001'
+
+# The number of milliseconds the agent will wait for
+# response on request to daemon
+# Default: request_timeout = 3000
+
+
+[AGENT]
+# Agent's polling interval in seconds
+polling_interval = 2
diff --git a/quantum/plugins/mlnx/README b/quantum/plugins/mlnx/README
new file mode 100644 (file)
index 0000000..b61ad47
--- /dev/null
@@ -0,0 +1,8 @@
+Mellanox Quantum Plugin
+
+This plugin implements Quantum v2 APIs with support for
+Mellanox embedded switch functionality as part of the
+VPI (Ethernet/InfiniBand) HCA.
+
+For more details on the plugin, please refer to the following link:
+https://wiki.openstack.org/wiki/Mellanox-Quantum
\ No newline at end of file
diff --git a/quantum/plugins/mlnx/__init__.py b/quantum/plugins/mlnx/__init__.py
new file mode 100644 (file)
index 0000000..c818bfe
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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/mlnx/agent/__init__.py b/quantum/plugins/mlnx/agent/__init__.py
new file mode 100644 (file)
index 0000000..c818bfe
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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/mlnx/agent/eswitch_quantum_agent.py b/quantum/plugins/mlnx/agent/eswitch_quantum_agent.py
new file mode 100644 (file)
index 0000000..b58072f
--- /dev/null
@@ -0,0 +1,405 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+import socket
+import sys
+import time
+
+import eventlet
+from oslo.config import cfg
+
+from quantum.agent import rpc as agent_rpc
+from quantum.common import config as logging_config
+from quantum.common import constants as q_constants
+from quantum.common import topics
+from quantum.common import utils as q_utils
+from quantum import context
+from quantum.openstack.common import log as logging
+from quantum.openstack.common import loopingcall
+from quantum.openstack.common.rpc import dispatcher
+from quantum.plugins.mlnx.agent import utils
+from quantum.plugins.mlnx.common import config  # noqa
+from quantum.plugins.mlnx.common import constants
+from quantum.plugins.mlnx.common import exceptions
+
+LOG = logging.getLogger(__name__)
+
+
+class EswitchManager(object):
+    def __init__(self, interface_mappings, endpoint, timeout):
+        self.utils = utils.EswitchUtils(endpoint, timeout)
+        self.interface_mappings = interface_mappings
+        self.network_map = {}
+        self.utils.define_fabric_mappings(interface_mappings)
+
+    def get_port_id_by_mac(self, port_mac):
+        for network_id, data in self.network_map.iteritems():
+            for port in data['ports']:
+                if port['port_mac'] == port_mac:
+                    return port['port_id']
+        err_msg = _("Agent cache inconsistency - port id "
+                    "is not stored for %s") % port_mac
+        LOG.error(err_msg)
+        raise exceptions.MlnxException(err_msg)
+
+    def get_vnics_mac(self):
+        return set(self.utils.get_attached_vnics().keys())
+
+    def vnic_port_exists(self, port_mac):
+        return port_mac in self.utils.get_attached_vnics()
+
+    def remove_network(self, network_id):
+        if network_id in self.network_map:
+            del self.network_map[network_id]
+        else:
+            LOG.debug(_("Network %s not defined on Agent."), network_id)
+
+    def port_down(self, network_id, physical_network, port_mac):
+        """Sets port to down.
+
+        Check  internal network map for port data.
+        If port exists set port to Down
+        """
+        for network_id, data in self.network_map.iteritems():
+            for port in data['ports']:
+                if port['port_mac'] == port_mac:
+                    self.utils.port_down(physical_network, port_mac)
+                    return
+        LOG.info(_('Network %s is not available on this agent'), network_id)
+
+    def port_up(self, network_id, network_type,
+                physical_network, seg_id, port_id, port_mac):
+        """Sets port to up.
+
+        Update internal network map with port data.
+        -Check if vnic defined
+        - configure eswitch vport
+        - set port to Up
+        """
+        LOG.debug(_("Connecting port %s"), port_id)
+
+        if network_id not in self.network_map:
+            self.provision_network(port_id, port_mac,
+                                   network_id, network_type,
+                                   physical_network, seg_id)
+        net_map = self.network_map[network_id]
+        net_map['ports'].append({'port_id': port_id, 'port_mac': port_mac})
+
+        if network_type == constants.TYPE_VLAN:
+            LOG.info(_('Binding VLAN ID %(seg_id)s'
+                       'to eSwitch for vNIC mac_address %(mac)s'),
+                     {'seg_id': seg_id,
+                      'mac': port_mac})
+            self.utils.set_port_vlan_id(physical_network,
+                                        seg_id,
+                                        port_mac)
+            self.utils.port_up(physical_network, port_mac)
+        elif network_type == constants.TYPE_IB:
+            LOG.debug(_('Network Type IB currently not supported'))
+        else:
+            LOG.error(_('Unsupported network type %s'), network_type)
+
+    def port_release(self, port_mac):
+        """Clear port configuration from eSwitch."""
+        for network_id, net_data in self.network_map.iteritems():
+            for port in net_data['ports']:
+                if port['port_mac'] == port_mac:
+                    self.utils.port_release(net_data['physical_network'],
+                                            port['port_mac'])
+                    return
+        LOG.info(_('Port_mac %s is not available on this agent'), port_mac)
+
+    def provision_network(self, port_id, port_mac,
+                          network_id, network_type,
+                          physical_network, segmentation_id):
+        LOG.info(_("Provisioning network %s"), network_id)
+        if network_type == constants.TYPE_VLAN:
+            LOG.debug(_("creating VLAN Network"))
+        elif network_type == constants.TYPE_IB:
+            LOG.debug(_("currently IB network provisioning is not supported"))
+        else:
+            LOG.error(_("Unknown network type %(network_type) "
+                        "for network %(network_id)"),
+                      {'network_type': network_type,
+                       'network_id': network_id})
+            return
+        data = {
+            'physical_network': physical_network,
+            'network_type': network_type,
+            'ports': [],
+            'vlan_id': segmentation_id}
+        self.network_map[network_id] = data
+
+
+class MlnxEswitchRpcCallbacks():
+
+    # Set RPC API version to 1.0 by default.
+    RPC_API_VERSION = '1.0'
+
+    def __init__(self, context, eswitch):
+        self.context = context
+        self.eswitch = eswitch
+
+    def network_delete(self, context, **kwargs):
+        LOG.debug(_("network_delete received"))
+        network_id = kwargs.get('network_id')
+        if not network_id:
+            LOG.warning(_("Invalid Network ID, cannot remove Network"))
+        else:
+            LOG.debug(_("Delete network %s"), network_id)
+            self.eswitch.remove_network(network_id)
+
+    def port_update(self, context, **kwargs):
+        LOG.debug(_("port_update received"))
+        port = kwargs.get('port')
+        vlan_id = kwargs.get('vlan_id')
+        physical_network = kwargs.get('physical_network')
+        net_type = kwargs.get('network_type')
+        net_id = port['network_id']
+        if self.eswitch.vnic_port_exists(port['mac_address']):
+            if port['admin_state_up']:
+                self.eswitch.port_up(net_id,
+                                     net_type,
+                                     physical_network,
+                                     vlan_id,
+                                     port['id'],
+                                     port['mac_address'])
+            else:
+                self.eswitch.port_down(net_id,
+                                       physical_network,
+                                       port['mac_address'])
+        else:
+            LOG.debug(_("No port %s defined on agent."), port['id'])
+
+    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])
+
+
+class MlnxEswitchQuantumAgent(object):
+    # Set RPC API version to 1.0 by default.
+    RPC_API_VERSION = '1.0'
+
+    def __init__(self, interface_mapping):
+        self._polling_interval = cfg.CONF.AGENT.polling_interval
+        self._setup_eswitches(interface_mapping)
+        self.agent_state = {
+            'binary': 'quantum-mlnx-agent',
+            'host': cfg.CONF.host,
+            'topic': q_constants.L2_AGENT_TOPIC,
+            'configurations': interface_mapping,
+            'agent_type': 'eSwitch agent',
+            'start_flag': True}
+        self._setup_rpc()
+
+    def _setup_eswitches(self, interface_mapping):
+        daemon = cfg.CONF.ESWITCH.daemon_endpoint
+        timeout = cfg.CONF.ESWITCH.request_timeout
+        self.eswitch = EswitchManager(interface_mapping, daemon, timeout)
+
+    def _report_state(self):
+        try:
+            devices = len(self.eswitch.get_vnics_mac())
+            self.agent_state['configurations']['devices'] = devices
+            self.state_rpc.report_state(self.context,
+                                        self.agent_state)
+            self.agent_state.pop('start_flag', None)
+        except Exception:
+            LOG.exception(_("Failed reporting state!"))
+
+    def _setup_rpc(self):
+        self.agent_id = 'mlnx-agent.%s' % socket.gethostname()
+        self.topic = topics.AGENT
+        self.plugin_rpc = agent_rpc.PluginApi(topics.PLUGIN)
+        self.state_rpc = agent_rpc.PluginReportStateAPI(topics.PLUGIN)
+        # RPC network init
+        self.context = context.get_admin_context_without_session()
+        # Handle updates from service
+        self.callbacks = MlnxEswitchRpcCallbacks(self.context, self.eswitch)
+        self.dispatcher = self.callbacks.create_rpc_dispatcher()
+        # Define the listening consumers for the agent
+        consumers = [[topics.PORT, topics.UPDATE],
+                     [topics.NETWORK, topics.DELETE]]
+        self.connection = agent_rpc.create_consumers(self.dispatcher,
+                                                     self.topic,
+                                                     consumers)
+
+        report_interval = cfg.CONF.AGENT.report_interval
+        if report_interval:
+            heartbeat = loopingcall.LoopingCall(self._report_state)
+            heartbeat.start(interval=report_interval)
+
+    def update_ports(self, registered_ports):
+        ports = self.eswitch.get_vnics_mac()
+        if ports == registered_ports:
+            return
+        added = ports - registered_ports
+        removed = registered_ports - ports
+        return {'current': ports,
+                'added': added,
+                'removed': removed}
+
+    def process_network_ports(self, port_info):
+        resync_a = False
+        resync_b = False
+        if 'added' in port_info:
+            LOG.debug(_("ports added!"))
+            resync_a = self.treat_devices_added(port_info['added'])
+        if 'removed' in port_info:
+            LOG.debug(_("ports removed!"))
+            resync_b = self.treat_devices_removed(port_info['removed'])
+        # If one of the above opertaions fails => resync with plugin
+        return (resync_a | resync_b)
+
+    def treat_vif_port(self, port_id, port_mac,
+                       network_id, network_type,
+                       physical_network, segmentation_id,
+                       admin_state_up):
+        if self.eswitch.vnic_port_exists(port_mac):
+            if admin_state_up:
+                self.eswitch.port_up(network_id,
+                                     network_type,
+                                     physical_network,
+                                     segmentation_id,
+                                     port_id,
+                                     port_mac)
+            else:
+                self.eswitch.port_down(network_id, physical_network, port_mac)
+        else:
+            LOG.debug(_("No port %s defined on agent."), port_id)
+
+    def treat_devices_added(self, devices):
+        resync = False
+        for device in devices:
+            LOG.info(_("Adding port with mac %s"), device)
+            try:
+                dev_details = self.plugin_rpc.get_device_details(
+                    self.context,
+                    device,
+                    self.agent_id)
+            except Exception as e:
+                LOG.debug(_("Unable to get device dev_details for device "
+                          "with mac_address %(device)s: due to %(exc)s"),
+                          {'device': device, 'exc': e})
+                resync = True
+                continue
+            if 'port_id' in dev_details:
+                LOG.info(_("Port %s updated"), device)
+                LOG.debug(_("Device details %s"), str(dev_details))
+                self.treat_vif_port(dev_details['port_id'],
+                                    dev_details['port_mac'],
+                                    dev_details['network_id'],
+                                    dev_details['network_type'],
+                                    dev_details['physical_network'],
+                                    dev_details['vlan_id'],
+                                    dev_details['admin_state_up'])
+            else:
+                LOG.debug(_("Device with mac_address %s not defined "
+                          "on Quantum Plugin"), device)
+        return resync
+
+    def treat_devices_removed(self, devices):
+        resync = False
+        for device in devices:
+            LOG.info(_("Removing device with mac_address %s"), device)
+            try:
+                port_id = self.eswitch.get_port_id_by_mac(device)
+                dev_details = self.plugin_rpc.update_device_down(self.context,
+                                                                 port_id,
+                                                                 self.agent_id)
+            except Exception as e:
+                LOG.debug(_("Removing port failed for device %(device)s "
+                          "due to %(exc)s"), {'device': device, 'exc': e})
+                resync = True
+                continue
+            if dev_details['exists']:
+                LOG.info(_("Port %s updated."), device)
+                self.eswitch.port_release(device)
+            else:
+                LOG.debug(_("Device %s not defined on plugin"), device)
+        return resync
+
+    def daemon_loop(self):
+        sync = True
+        ports = set()
+
+        LOG.info(_("eSwitch Agent Started!"))
+
+        while True:
+            try:
+                start = time.time()
+                if sync:
+                    LOG.info(_("Agent out of sync with plugin!"))
+                    ports.clear()
+                    sync = False
+
+                port_info = self.update_ports(ports)
+                # notify plugin about port deltas
+                if port_info:
+                    LOG.debug(_("Agent loop has new devices!"))
+                    # If treat devices fails - must resync with plugin
+                    sync = self.process_network_ports(port_info)
+                    ports = port_info['current']
+            except Exception:
+                LOG.exception(_("Error in agent event loop"))
+                sync = True
+            # sleep till end of polling interval
+            elapsed = (time.time() - start)
+            if (elapsed < self._polling_interval):
+                time.sleep(self._polling_interval - elapsed)
+            else:
+                LOG.debug(_("Loop iteration exceeded interval "
+                            "(%(polling_interval)s vs. %(elapsed)s)"),
+                          {'polling_interval': self._polling_interval,
+                           'elapsed': elapsed})
+
+
+def main():
+    eventlet.monkey_patch()
+    cfg.CONF(project='quantum')
+    logging_config.setup_logging(cfg.CONF)
+
+    try:
+        interface_mappings = q_utils.parse_mappings(
+            cfg.CONF.ESWITCH.physical_interface_mappings)
+    except ValueError as e:
+        LOG.error(_("Parsing physical_interface_mappings failed: %s."
+                    " Agent terminated!"), e)
+        sys.exit(1)
+    LOG.info(_("Interface mappings: %s"), interface_mappings)
+
+    try:
+        agent = MlnxEswitchQuantumAgent(interface_mappings)
+    except Exception as e:
+        LOG.error(_("Failed on Agent initialisation : %s."
+                    " Agent terminated!"), e)
+        sys.exit(1)
+
+    # Start everything.
+    LOG.info(_("Agent initialised successfully, now running... "))
+    agent.daemon_loop()
+    sys.exit(0)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/quantum/plugins/mlnx/agent/utils.py b/quantum/plugins/mlnx/agent/utils.py
new file mode 100644 (file)
index 0000000..8e3adbb
--- /dev/null
@@ -0,0 +1,136 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import zmq
+
+from quantum.openstack.common import jsonutils
+from quantum.openstack.common import log as logging
+from quantum.plugins.mlnx.common import exceptions
+
+LOG = logging.getLogger(__name__)
+
+
+class EswitchUtils(object):
+    def __init__(self, daemon_endpoint, timeout):
+        self.__conn = None
+        self.daemon = daemon_endpoint
+        self.timeout = timeout
+
+    @property
+    def _conn(self):
+        if self.__conn is None:
+            context = zmq.Context()
+            socket = context.socket(zmq.REQ)
+            socket.setsockopt(zmq.LINGER, 0)
+            socket.connect(self.daemon)
+            self.__conn = socket
+            self.poller = zmq.Poller()
+            self.poller.register(self._conn, zmq.POLLIN)
+        return self.__conn
+
+    def send_msg(self, msg):
+        self._conn.send(msg)
+
+        socks = dict(self.poller.poll(self.timeout))
+        if socks.get(self._conn) == zmq.POLLIN:
+            recv_msg = self._conn.recv()
+            response = self.parse_response_msg(recv_msg)
+            return response
+        else:
+            self._conn.setsockopt(zmq.LINGER, 0)
+            self._conn.close()
+            self.poller.unregister(self._conn)
+            self.__conn = None
+            raise exceptions.MlnxException(_("eSwitchD: Request timeout"))
+
+    def parse_response_msg(self, recv_msg):
+        msg = jsonutils.loads(recv_msg)
+        if msg['status'] == 'OK':
+            if 'response' in msg:
+                return msg.get('response')
+            return
+        elif msg['status'] == 'FAIL':
+            msg_dict = dict(action=msg['action'], reason=msg['reason'])
+            error_msg = _("Action %(action)s failed: %(reason)s") % msg_dict
+        else:
+            error_msg = _("Unknown operation status %s") % msg['status']
+        LOG.error(error_msg)
+        raise exceptions.MlnxException(error_msg)
+
+    def get_attached_vnics(self):
+        LOG.debug(_("get_attached_vnics"))
+        msg = jsonutils.dumps({'action': 'get_vnics', 'fabric': '*'})
+        vnics = self.send_msg(msg)
+        return vnics
+
+    def set_port_vlan_id(self, physical_network,
+                         segmentation_id, port_mac):
+        LOG.debug(_("Set Vlan  %(segmentation_id)s on Port %(port_mac)s "
+                    "on Fabric %(physical_network)s"),
+                  {'port_mac': port_mac,
+                   'segmentation_id': segmentation_id,
+                   'physical_network': physical_network})
+        msg = jsonutils.dumps({'action': 'set_vlan',
+                               'fabric': physical_network,
+                               'port_mac': port_mac,
+                               'vlan': segmentation_id})
+        self.send_msg(msg)
+
+    def define_fabric_mappings(self, interface_mapping):
+        for fabric, phy_interface in interface_mapping.iteritems():
+            LOG.debug(_("Define Fabric %(fabric)s on interface %(ifc)s"),
+                      {'fabric': fabric,
+                       'ifc': phy_interface})
+            msg = jsonutils.dumps({'action': 'define_fabric_mapping',
+                                   'fabric': fabric,
+                                   'interface': phy_interface})
+            self.send_msg(msg)
+
+    def port_up(self, fabric, port_mac):
+        LOG.debug(_("Port Up for %(port_mac)s on fabric %(fabric)s"),
+                  {'port_mac': port_mac, 'fabric': fabric})
+        msg = jsonutils.dumps({'action': 'port_up',
+                               'fabric': fabric,
+                               'ref_by': 'mac_address',
+                               'mac': 'port_mac'})
+        self.send_msg(msg)
+
+    def port_down(self, fabric, port_mac):
+        LOG.debug(_("Port Down for %(port_mac)s on fabric %(fabric)s"),
+                  {'port_mac': port_mac, 'fabric': fabric})
+        msg = jsonutils.dumps({'action': 'port_down',
+                               'fabric': fabric,
+                               'ref_by': 'mac_address',
+                               'mac': port_mac})
+        self.send_msg(msg)
+
+    def port_release(self, fabric, port_mac):
+        LOG.debug(_("Port Release for %(port_mac)s on fabric %(fabric)s"),
+                  {'port_mac': port_mac, 'fabric': fabric})
+        msg = jsonutils.dumps({'action': 'port_release',
+                               'fabric': fabric,
+                               'ref_by': 'mac_address',
+                               'mac': port_mac})
+        self.send_msg(msg)
+
+    def get_eswitch_ports(self, fabric):
+        # TODO(irena) - to implement for next phase
+        return {}
+
+    def get_eswitch_id(self, fabric):
+        # TODO(irena) - to implement for next phase
+        return ""
diff --git a/quantum/plugins/mlnx/agent_notify_api.py b/quantum/plugins/mlnx/agent_notify_api.py
new file mode 100644 (file)
index 0000000..29d7c23
--- /dev/null
@@ -0,0 +1,59 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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.
+
+from quantum.common import topics
+from quantum.openstack.common import log as logging
+from quantum.openstack.common.rpc import proxy
+
+LOG = logging.getLogger(__name__)
+
+
+class AgentNotifierApi(proxy.RpcProxy):
+    """Agent side of the Embedded Switch RPC API.
+
+       API version history:
+       1.0 - Initial version.
+    """
+    BASE_RPC_API_VERSION = '1.0'
+
+    def __init__(self, topic):
+        super(AgentNotifierApi, self).__init__(
+            topic=topic, default_version=self.BASE_RPC_API_VERSION)
+        self.topic_network_delete = topics.get_topic_name(topic,
+                                                          topics.NETWORK,
+                                                          topics.DELETE)
+        self.topic_port_update = topics.get_topic_name(topic,
+                                                       topics.PORT,
+                                                       topics.UPDATE)
+
+    def network_delete(self, context, network_id):
+        LOG.debug(_("Sending delete network message"))
+        self.fanout_cast(context,
+                         self.make_msg('network_delete',
+                                       network_id=network_id),
+                         topic=self.topic_network_delete)
+
+    def port_update(self, context, port, physical_network,
+                    network_type, vlan_id):
+        LOG.debug(_("Sending update port message"))
+        self.fanout_cast(context,
+                         self.make_msg('port_update',
+                                       port=port,
+                                       physical_network=physical_network,
+                                       network_type=network_type,
+                                       vlan_id=vlan_id),
+                         topic=self.topic_port_update)
diff --git a/quantum/plugins/mlnx/common/__init__.py b/quantum/plugins/mlnx/common/__init__.py
new file mode 100644 (file)
index 0000000..c818bfe
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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/mlnx/common/config.py b/quantum/plugins/mlnx/common/config.py
new file mode 100644 (file)
index 0000000..672e256
--- /dev/null
@@ -0,0 +1,63 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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.
+
+from oslo.config import cfg
+
+from quantum.agent.common import config
+from quantum.plugins.mlnx.common import constants
+
+DEFAULT_VLAN_RANGES = ['default:1:1000']
+DEFAULT_INTERFACE_MAPPINGS = []
+
+vlan_opts = [
+    cfg.StrOpt('tenant_network_type', default='vlan',
+               help=_("Network type for tenant networks "
+               "(local, ib, vlan, or none)")),
+    cfg.ListOpt('network_vlan_ranges',
+                default=DEFAULT_VLAN_RANGES,
+                help=_("List of <physical_network>:<vlan_min>:<vlan_max> "
+                       "or <physical_network>")),
+]
+
+
+eswitch_opts = [
+    cfg.ListOpt('physical_interface_mappings',
+                default=DEFAULT_INTERFACE_MAPPINGS,
+                help=_("List of <physical_network>:<physical_interface>")),
+    cfg.StrOpt('vnic_type',
+               default=constants.VIF_TYPE_DIRECT,
+               help=_("type of VM network interface: direct or hosdev")),
+    cfg.StrOpt('daemon_endpoint',
+               default='tcp://127.0.0.1:5001',
+               help=_('eswitch daemon end point')),
+    cfg.IntOpt('request_timeout', default=3000,
+               help=_("The number of milliseconds the agent will wait for "
+                      "response on request to daemon.")),
+]
+
+agent_opts = [
+    cfg.IntOpt('polling_interval', default=2,
+               help=_("The number of seconds the agent will wait between "
+                      "polling for local device changes.")),
+]
+
+
+cfg.CONF.register_opts(vlan_opts, "MLNX")
+cfg.CONF.register_opts(eswitch_opts, "ESWITCH")
+cfg.CONF.register_opts(agent_opts, "AGENT")
+config.register_agent_state_opts_helper(cfg.CONF)
+config.register_root_helper(cfg.CONF)
diff --git a/quantum/plugins/mlnx/common/constants.py b/quantum/plugins/mlnx/common/constants.py
new file mode 100644 (file)
index 0000000..eb6c1b8
--- /dev/null
@@ -0,0 +1,33 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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.
+
+LOCAL_VLAN_ID = -2
+FLAT_VLAN_ID = -1
+VLAN_ID_MIN = 1
+VLAN_ID_MAX = 4096
+
+# Values for network_type
+TYPE_LOCAL = 'local'
+TYPE_FLAT = 'flat'
+TYPE_VLAN = 'vlan'
+TYPE_IB = 'ib'
+TYPE_NONE = 'none'
+
+VIF_TYPE_DIRECT = 'direct'
+VIF_TYPE_HOSTDEV = 'hostdev'
+
+VNIC_TYPE = 'vnic_type'
diff --git a/quantum/plugins/mlnx/common/exceptions.py b/quantum/plugins/mlnx/common/exceptions.py
new file mode 100644 (file)
index 0000000..e4c45b2
--- /dev/null
@@ -0,0 +1,22 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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.
+
+from quantum.common import exceptions as qexc
+
+
+class MlnxException(qexc.QuantumException):
+    message = _("Mlnx Exception: %(err_msg)s")
diff --git a/quantum/plugins/mlnx/db/__init__.py b/quantum/plugins/mlnx/db/__init__.py
new file mode 100644 (file)
index 0000000..c818bfe
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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/mlnx/db/mlnx_db_v2.py b/quantum/plugins/mlnx/db/mlnx_db_v2.py
new file mode 100644 (file)
index 0000000..e2c4a74
--- /dev/null
@@ -0,0 +1,241 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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.
+
+from sqlalchemy.orm import exc
+
+from quantum.common import exceptions as q_exc
+import quantum.db.api as db
+from quantum.db import models_v2
+from quantum.openstack.common import log as logging
+from quantum.plugins.mlnx.common import config  # noqa
+from quantum.plugins.mlnx.db import mlnx_models_v2
+
+LOG = logging.getLogger(__name__)
+
+
+def initialize():
+    db.configure_db()
+
+
+def _remove_non_allocatable_vlans(session, allocations,
+                                  physical_network, vlan_ids):
+    if physical_network in allocations:
+        for entry in allocations[physical_network]:
+            try:
+                # see if vlan is allocatable
+                vlan_ids.remove(entry.segmentation_id)
+            except KeyError:
+                # it's not allocatable, so check if its allocated
+                if not entry.allocated:
+                    # it's not, so remove it from table
+                    LOG.debug(_(
+                        "Removing vlan %(seg_id)s on "
+                        "physical network "
+                        "%(net)s from pool"),
+                        {'seg_id': entry.segmentation_id,
+                         'net': physical_network})
+                    session.delete(entry)
+        del allocations[physical_network]
+
+
+def _add_missing_allocatable_vlans(session, physical_network, vlan_ids):
+    for vlan_id in sorted(vlan_ids):
+        entry = mlnx_models_v2.SegmentationIdAllocation(physical_network,
+                                                        vlan_id)
+        session.add(entry)
+
+
+def _remove_unconfigured_vlans(session, allocations):
+    for entries in allocations.itervalues():
+        for entry in entries:
+            if not entry.allocated:
+                LOG.debug(_("removing vlan %(seg_id)s on physical "
+                            "network %(net)s from pool"),
+                          {'seg_id': entry.segmentation_id,
+                           'net': entry.physical_network})
+                session.delete(entry)
+
+
+def sync_network_states(network_vlan_ranges):
+    """Synchronize network_states table with current configured VLAN ranges."""
+
+    session = db.get_session()
+    with session.begin():
+        # get existing allocations for all physical networks
+        allocations = dict()
+        entries = (session.query(mlnx_models_v2.SegmentationIdAllocation).
+                   all())
+        for entry in entries:
+            allocations.setdefault(entry.physical_network, set()).add(entry)
+
+        # process vlan ranges for each configured physical network
+        for physical_network, vlan_ranges in network_vlan_ranges.iteritems():
+            # determine current configured allocatable vlans for this
+            # physical network
+            vlan_ids = set()
+            for vlan_range in vlan_ranges:
+                vlan_ids |= set(xrange(vlan_range[0], vlan_range[1] + 1))
+
+            # remove from table unallocated vlans not currently allocatable
+            _remove_non_allocatable_vlans(session, allocations,
+                                          physical_network, vlan_ids)
+
+            # add missing allocatable vlans to table
+            _add_missing_allocatable_vlans(session, physical_network, vlan_ids)
+
+        # remove from table unallocated vlans for any unconfigured physical
+        # networks
+        _remove_unconfigured_vlans(session, allocations)
+
+
+def get_network_state(physical_network, segmentation_id):
+    """Get entry of specified network."""
+    session = db.get_session()
+    qry = session.query(mlnx_models_v2.SegmentationIdAllocation)
+    qry = qry.filter_by(physical_network=physical_network,
+                        segmentation_id=segmentation_id)
+    return qry.first()
+
+
+def reserve_network(session):
+    with session.begin(subtransactions=True):
+        entry = (session.query(mlnx_models_v2.SegmentationIdAllocation).
+                 filter_by(allocated=False).
+                 first())
+        if not entry:
+            raise q_exc.NoNetworkAvailable()
+        LOG.debug(_("Reserving vlan %(seg_id)s on physical network "
+                    "%(net)s from pool"),
+                  {'seg_id': entry.segmentation_id,
+                   'net': entry.physical_network})
+        entry.allocated = True
+        return (entry.physical_network, entry.segmentation_id)
+
+
+def reserve_specific_network(session, physical_network, segmentation_id):
+    with session.begin(subtransactions=True):
+        log_args = {'seg_id': segmentation_id, 'phy_net': physical_network}
+        try:
+            entry = (session.query(mlnx_models_v2.SegmentationIdAllocation).
+                     filter_by(physical_network=physical_network,
+                     segmentation_id=segmentation_id).
+                     one())
+            if entry.allocated:
+                raise q_exc.VlanIdInUse(vlan_id=segmentation_id,
+                                        physical_network=physical_network)
+            LOG.debug(_("Reserving specific vlan %(seg_id)s "
+                        "on physical network %(phy_net)s from pool"),
+                      log_args)
+            entry.allocated = True
+        except exc.NoResultFound:
+            LOG.debug(_("Reserving specific vlan %(seg_id)s on "
+                        "physical network %(phy_net)s outside pool"),
+                      log_args)
+            entry = mlnx_models_v2.SegmentationIdAllocation(physical_network,
+                                                            segmentation_id)
+            entry.allocated = True
+            session.add(entry)
+
+
+def release_network(session, physical_network,
+                    segmentation_id, network_vlan_ranges):
+    with session.begin(subtransactions=True):
+        log_args = {'seg_id': segmentation_id, 'phy_net': physical_network}
+        try:
+            state = (session.query(mlnx_models_v2.SegmentationIdAllocation).
+                     filter_by(physical_network=physical_network,
+                               segmentation_id=segmentation_id).
+                     with_lockmode('update').
+                     one())
+            state.allocated = False
+            inside = False
+            for vlan_range in network_vlan_ranges.get(physical_network, []):
+                if (segmentation_id >= vlan_range[0] and
+                    segmentation_id <= vlan_range[1]):
+                    inside = True
+                    break
+            if inside:
+                LOG.debug(_("Releasing vlan %(seg_id)s "
+                            "on physical network "
+                            "%(phy_net)s to pool"),
+                          log_args)
+            else:
+                LOG.debug(_("Releasing vlan %(seg_id)s "
+                            "on physical network "
+                            "%(phy_net)s outside pool"),
+                          log_args)
+                session.delete(state)
+        except exc.NoResultFound:
+            LOG.warning(_("vlan_id %(seg_id)s on physical network "
+                          "%(phy_net)s not found"),
+                        log_args)
+
+
+def add_network_binding(session, network_id, network_type,
+                        physical_network, vlan_id):
+    with session.begin(subtransactions=True):
+        binding = mlnx_models_v2.NetworkBinding(network_id, network_type,
+                                                physical_network, vlan_id)
+        session.add(binding)
+
+
+def get_network_binding(session, network_id):
+    qry = session.query(mlnx_models_v2.NetworkBinding)
+    qry = qry.filter_by(network_id=network_id)
+    return qry.first()
+
+
+def add_port_profile_binding(session, port_id, vnic_type):
+    with session.begin(subtransactions=True):
+        binding = mlnx_models_v2.PortProfileBinding(port_id, vnic_type)
+        session.add(binding)
+
+
+def get_port_profile_binding(session, port_id):
+    qry = session.query(mlnx_models_v2.PortProfileBinding)
+    return qry.filter_by(port_id=port_id).first()
+
+
+def get_port_from_device(device):
+    """Get port from database."""
+    LOG.debug(_("get_port_from_device() called"))
+    session = db.get_session()
+    ports = session.query(models_v2.Port).all()
+    for port in ports:
+        if port['id'].startswith(device):
+            return port
+
+
+def get_port_from_device_mac(device_mac):
+    """Get port from database."""
+    LOG.debug(_("Get_port_from_device_mac() called"))
+    session = db.get_session()
+    qry = session.query(models_v2.Port).filter_by(mac_address=device_mac)
+    return qry.first()
+
+
+def set_port_status(port_id, status):
+    """Set the port status."""
+    LOG.debug(_("Set_port_status as %s called"), status)
+    session = db.get_session()
+    try:
+        port = session.query(models_v2.Port).filter_by(id=port_id).one()
+        port['status'] = status
+        session.merge(port)
+        session.flush()
+    except exc.NoResultFound:
+        raise q_exc.PortNotFound(port_id=port_id)
diff --git a/quantum/plugins/mlnx/db/mlnx_models_v2.py b/quantum/plugins/mlnx/db/mlnx_models_v2.py
new file mode 100644 (file)
index 0000000..9c026a4
--- /dev/null
@@ -0,0 +1,86 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sqlalchemy as sa
+
+from quantum.db import model_base
+
+
+class SegmentationIdAllocation(model_base.BASEV2):
+    """Represents allocation state of segmentation_id on physical network."""
+    __tablename__ = 'segmentation_id_allocation'
+
+    physical_network = sa.Column(sa.String(64), nullable=False,
+                                 primary_key=True)
+    segmentation_id = sa.Column(sa.Integer, nullable=False, primary_key=True,
+                                autoincrement=False)
+    allocated = sa.Column(sa.Boolean, nullable=False, default=False)
+
+    def __init__(self, physical_network, segmentation_id):
+        self.physical_network = physical_network
+        self.segmentation_id = segmentation_id
+        self.allocated = False
+
+    def __repr__(self):
+        return "<SegmentationIdAllocation(%s,%d,%s)>" % (self.physical_network,
+                                                         self.segmentation_id,
+                                                         self.allocated)
+
+
+class NetworkBinding(model_base.BASEV2):
+    """Represents binding of virtual network.
+
+    Binds network to physical_network and segmentation_id
+    """
+    __tablename__ = 'mlnx_network_bindings'
+
+    network_id = sa.Column(sa.String(36),
+                           sa.ForeignKey('networks.id', ondelete="CASCADE"),
+                           primary_key=True)
+    network_type = sa.Column(sa.String(32), nullable=False)
+    physical_network = sa.Column(sa.String(64))
+    segmentation_id = sa.Column(sa.Integer, nullable=False)
+
+    def __init__(self, network_id, network_type, physical_network, vlan_id):
+        self.network_id = network_id
+        self.network_type = network_type
+        self.physical_network = physical_network
+        self.segmentation_id = vlan_id
+
+    def __repr__(self):
+        return "<NetworkBinding(%s,%s,%s,%d)>" % (self.network_id,
+                                                  self.network_type,
+                                                  self.physical_network,
+                                                  self.segmentation_id)
+
+
+class PortProfileBinding(model_base.BASEV2):
+    """Represents port profile binding to the port on virtual network."""
+    __tablename__ = 'port_profile'
+
+    port_id = sa.Column(sa.String(36),
+                        sa.ForeignKey('ports.id', ondelete="CASCADE"),
+                        primary_key=True)
+    vnic_type = sa.Column(sa.String(32), nullable=False)
+
+    def __init__(self, port_id, vnic_type):
+        self.port_id = port_id
+        self.vnic_type = vnic_type
+
+    def __repr__(self):
+        return "<PortProfileBinding(%s,%s,%s,%d)>" % (self.port_id,
+                                                      self.vnic_type)
diff --git a/quantum/plugins/mlnx/mlnx_plugin.py b/quantum/plugins/mlnx/mlnx_plugin.py
new file mode 100644 (file)
index 0000000..b258ad8
--- /dev/null
@@ -0,0 +1,409 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+
+from oslo.config import cfg
+
+from quantum.agent import securitygroups_rpc as sg_rpc
+from quantum.api.v2 import attributes
+from quantum.common import exceptions as q_exc
+from quantum.common import topics
+from quantum.db import agents_db
+from quantum.db import db_base_plugin_v2
+from quantum.db import l3_db
+from quantum.db import quota_db  # noqa
+from quantum.db import securitygroups_rpc_base as sg_db_rpc
+from quantum.extensions import portbindings
+from quantum.extensions import providernet as provider
+from quantum.openstack.common import log as logging
+from quantum.openstack.common import rpc
+from quantum.plugins.common import utils as plugin_utils
+from quantum.plugins.mlnx import agent_notify_api
+from quantum.plugins.mlnx.common import constants
+from quantum.plugins.mlnx.db import mlnx_db_v2 as db
+from quantum.plugins.mlnx import rpc_callbacks
+from quantum import policy
+
+LOG = logging.getLogger(__name__)
+
+
+class MellanoxEswitchPlugin(db_base_plugin_v2.QuantumDbPluginV2,
+                            l3_db.L3_NAT_db_mixin,
+                            agents_db.AgentDbMixin,
+                            sg_db_rpc.SecurityGroupServerRpcMixin):
+    """Realization of Quantum API on Mellanox HCA embedded switch technology.
+
+       Current plugin provides embedded HCA Switch connectivity.
+       Code is based on the Linux Bridge plugin content to
+       support consistency with L3 & DHCP Agents.
+    """
+
+    # This attribute specifies whether the plugin supports or not
+    # bulk operations. Name mangling is used in order to ensure it
+    # is qualified by class
+    __native_bulk_support = True
+
+    _supported_extension_aliases = ["provider", "router", "binding",
+                                    "agent", "quotas", "security-group"]
+
+    @property
+    def supported_extension_aliases(self):
+        if not hasattr(self, '_aliases'):
+            aliases = self._supported_extension_aliases[:]
+            sg_rpc.disable_security_group_extension_if_noop_driver(aliases)
+            self._aliases = aliases
+        return self._aliases
+
+    network_view = "extension:provider_network:view"
+    network_set = "extension:provider_network:set"
+    binding_view = "extension:port_binding:view"
+    binding_set = "extension:port_binding:set"
+
+    def __init__(self):
+        """Start Mellanox Quantum Plugin."""
+        db.initialize()
+        self._parse_network_vlan_ranges()
+        db.sync_network_states(self.network_vlan_ranges)
+        self._set_tenant_network_type()
+        self.vnic_type = cfg.CONF.ESWITCH.vnic_type
+        self._setup_rpc()
+        LOG.debug(_("Mellanox Embedded Switch Plugin initialisation complete"))
+
+    def _setup_rpc(self):
+        # RPC support
+        self.topic = topics.PLUGIN
+        self.conn = rpc.create_connection(new=True)
+        self.notifier = agent_notify_api.AgentNotifierApi(topics.AGENT)
+        self.callbacks = rpc_callbacks.MlnxRpcCallbacks()
+        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 _parse_network_vlan_ranges(self):
+        try:
+            self.network_vlan_ranges = plugin_utils.parse_network_vlan_ranges(
+                cfg.CONF.MLNX.network_vlan_ranges)
+        except Exception as ex:
+            LOG.error(_("%s. Server terminated!"), ex)
+            sys.exit(1)
+        LOG.info(_("Network VLAN ranges: %s"), self.network_vlan_ranges)
+
+    def _check_view_auth(self, context, resource, action):
+        return policy.check(context, action, resource)
+
+    def _enforce_set_auth(self, context, resource, action):
+        policy.enforce(context, action, resource)
+
+    def _add_network_vlan_range(self, physical_network, vlan_min, vlan_max):
+        self._add_network(physical_network)
+        self.network_vlan_ranges[physical_network].append((vlan_min, vlan_max))
+
+    def _add_network(self, physical_network):
+        if physical_network not in self.network_vlan_ranges:
+            self.network_vlan_ranges[physical_network] = []
+
+    def _extend_network_dict_provider(self, context, network):
+        if self._check_view_auth(context, network, self.network_view):
+            binding = db.get_network_binding(context.session, network['id'])
+            network[provider.NETWORK_TYPE] = binding.network_type
+            if binding.network_type == constants.TYPE_FLAT:
+                network[provider.PHYSICAL_NETWORK] = binding.physical_network
+                network[provider.SEGMENTATION_ID] = None
+            elif binding.network_type == constants.TYPE_LOCAL:
+                network[provider.PHYSICAL_NETWORK] = None
+                network[provider.SEGMENTATION_ID] = None
+            else:
+                network[provider.PHYSICAL_NETWORK] = binding.physical_network
+                network[provider.SEGMENTATION_ID] = binding.segmentation_id
+
+    def _set_tenant_network_type(self):
+        self.tenant_network_type = cfg.CONF.MLNX.tenant_network_type
+        if self.tenant_network_type not in [constants.TYPE_VLAN,
+                                            constants.TYPE_IB,
+                                            constants.TYPE_LOCAL,
+                                            constants.TYPE_NONE]:
+            LOG.error(_("Invalid tenant_network_type: %s. "
+                        "Service terminated!"),
+                      self.tenant_network_type)
+            sys.exit(1)
+
+    def _process_provider_create(self, context, attrs):
+        network_type = attrs.get(provider.NETWORK_TYPE)
+        physical_network = attrs.get(provider.PHYSICAL_NETWORK)
+        segmentation_id = attrs.get(provider.SEGMENTATION_ID)
+
+        network_type_set = attributes.is_attr_set(network_type)
+        physical_network_set = attributes.is_attr_set(physical_network)
+        segmentation_id_set = attributes.is_attr_set(segmentation_id)
+
+        if not (network_type_set or physical_network_set or
+                segmentation_id_set):
+            return (None, None, None)
+        # Authorize before exposing plugin details to client
+        self._enforce_set_auth(context, attrs, self.network_set)
+
+        if not network_type_set:
+            msg = _("provider:network_type required")
+            raise q_exc.InvalidInput(error_message=msg)
+        elif network_type == constants.TYPE_FLAT:
+            self._process_flat_net(segmentation_id_set)
+            segmentation_id = constants.FLAT_VLAN_ID
+
+        elif network_type in [constants.TYPE_VLAN, constants.TYPE_IB]:
+            self._process_vlan_net(segmentation_id, segmentation_id_set)
+
+        elif network_type == constants.TYPE_LOCAL:
+            self._process_local_net(physical_network_set,
+                                    segmentation_id_set)
+            segmentation_id = constants.LOCAL_VLAN_ID
+            physical_network = None
+
+        else:
+            msg = _("provider:network_type %s not supported") % network_type
+            raise q_exc.InvalidInput(error_message=msg)
+        physical_network = self._process_net_type(network_type,
+                                                  physical_network,
+                                                  physical_network_set)
+        return (network_type, physical_network, segmentation_id)
+
+    def _process_flat_net(self, segmentation_id_set):
+        if segmentation_id_set:
+            msg = _("provider:segmentation_id specified for flat network")
+            raise q_exc.InvalidInput(error_message=msg)
+
+    def _process_vlan_net(self, segmentation_id, segmentation_id_set):
+        if not segmentation_id_set:
+            msg = _("provider:segmentation_id required")
+            raise q_exc.InvalidInput(error_message=msg)
+        if segmentation_id < 1 or segmentation_id > 4094:
+            msg = _("provider:segmentation_id out of range "
+                    "(1 through 4094)")
+            raise q_exc.InvalidInput(error_message=msg)
+
+    def _process_local_net(self, physical_network_set, segmentation_id_set):
+        if physical_network_set:
+            msg = _("provider:physical_network specified for local "
+                    "network")
+            raise q_exc.InvalidInput(error_message=msg)
+        if segmentation_id_set:
+            msg = _("provider:segmentation_id specified for local "
+                    "network")
+            raise q_exc.InvalidInput(error_message=msg)
+
+    def _process_net_type(self, network_type,
+                          physical_network,
+                          physical_network_set):
+        if network_type in [constants.TYPE_VLAN,
+                            constants.TYPE_IB,
+                            constants.TYPE_FLAT]:
+            if physical_network_set:
+                if physical_network not in self.network_vlan_ranges:
+                    msg = _("unknown provider:physical_network "
+                            "%s") % physical_network
+                    raise q_exc.InvalidInput(error_message=msg)
+            elif 'default' in self.network_vlan_ranges:
+                physical_network = 'default'
+            else:
+                msg = _("provider:physical_network required")
+                raise q_exc.InvalidInput(error_message=msg)
+        return physical_network
+
+    def _check_provider_update(self, context, attrs):
+        network_type = attrs.get(provider.NETWORK_TYPE)
+        physical_network = attrs.get(provider.PHYSICAL_NETWORK)
+        segmentation_id = attrs.get(provider.SEGMENTATION_ID)
+
+        network_type_set = attributes.is_attr_set(network_type)
+        physical_network_set = attributes.is_attr_set(physical_network)
+        segmentation_id_set = attributes.is_attr_set(segmentation_id)
+
+        if not (network_type_set or physical_network_set or
+                segmentation_id_set):
+            return
+        # Authorize before exposing plugin details to client
+        self._enforce_set_auth(context, attrs, self.network_set)
+        msg = _("Plugin does not support updating provider attributes")
+        raise q_exc.InvalidInput(error_message=msg)
+
+    def _process_port_binding_create(self, context, attrs):
+        binding_profile = attrs.get(portbindings.PROFILE)
+        binding_profile_set = attributes.is_attr_set(binding_profile)
+        if not binding_profile_set:
+            return self.vnic_type
+        if constants.VNIC_TYPE in binding_profile:
+            req_vnic_type = binding_profile[constants.VNIC_TYPE]
+            if req_vnic_type in (constants.VIF_TYPE_DIRECT,
+                                 constants.VIF_TYPE_HOSTDEV):
+                return req_vnic_type
+            else:
+                msg = _("invalid vnic_type on port_create")
+        else:
+            msg = _("vnic_type is not defined in port profile")
+        raise q_exc.InvalidInput(error_message=msg)
+
+    def create_network(self, context, network):
+        (network_type, physical_network,
+         vlan_id) = self._process_provider_create(context,
+                                                  network['network'])
+        session = context.session
+        with session.begin(subtransactions=True):
+            if not network_type:
+                # tenant network
+                network_type = self.tenant_network_type
+                if network_type == constants.TYPE_NONE:
+                    raise q_exc.TenantNetworksDisabled()
+                elif network_type in [constants.TYPE_VLAN, constants.TYPE_IB]:
+                    physical_network, vlan_id = db.reserve_network(session)
+                else:  # TYPE_LOCAL
+                    vlan_id = constants.LOCAL_VLAN_ID
+            else:
+                # provider network
+                if network_type in [constants.TYPE_VLAN,
+                                    constants.TYPE_IB,
+                                    constants.TYPE_FLAT]:
+                    db.reserve_specific_network(session,
+                                                physical_network,
+                                                vlan_id)
+            net = super(MellanoxEswitchPlugin, self).create_network(context,
+                                                                    network)
+            db.add_network_binding(session, net['id'],
+                                   network_type,
+                                   physical_network,
+                                   vlan_id)
+
+            self._process_l3_create(context, network['network'], net['id'])
+            self._extend_network_dict_provider(context, net)
+            self._extend_network_dict_l3(context, net)
+            # note - exception will rollback entire transaction
+            LOG.debug(_("Created network: %s"), net['id'])
+            return net
+
+    def update_network(self, context, net_id, network):
+        self._check_provider_update(context, network['network'])
+        session = context.session
+        with session.begin(subtransactions=True):
+            net = super(MellanoxEswitchPlugin, self).update_network(context,
+                                                                    net_id,
+                                                                    network)
+            self._process_l3_update(context, network['network'], net_id)
+            self._extend_network_dict_provider(context, net)
+            self._extend_network_dict_l3(context, net)
+        return net
+
+    def delete_network(self, context, net_id):
+        LOG.debug(_("delete network"))
+        session = context.session
+        with session.begin(subtransactions=True):
+            binding = db.get_network_binding(session, net_id)
+            super(MellanoxEswitchPlugin, self).delete_network(context,
+                                                              net_id)
+            if binding.segmentation_id != constants.LOCAL_VLAN_ID:
+                db.release_network(session, binding.physical_network,
+                                   binding.segmentation_id,
+                                   self.network_vlan_ranges)
+            # the network_binding record is deleted via cascade from
+            # the network record, so explicit removal is not necessary
+        self.notifier.network_delete(context, net_id)
+
+    def get_network(self, context, net_id, fields=None):
+        session = context.session
+        with session.begin(subtransactions=True):
+            net = super(MellanoxEswitchPlugin, self).get_network(context,
+                                                                 net_id,
+                                                                 None)
+            self._extend_network_dict_provider(context, net)
+            self._extend_network_dict_l3(context, net)
+        return self._fields(net, fields)
+
+    def get_networks(self, context, filters=None, fields=None):
+        session = context.session
+        with session.begin(subtransactions=True):
+            nets = super(MellanoxEswitchPlugin, self).get_networks(context,
+                                                                   filters,
+                                                                   None)
+            for net in nets:
+                self._extend_network_dict_provider(context, net)
+                self._extend_network_dict_l3(context, net)
+            # TODO(rkukura): Filter on extended provider attributes.
+            nets = self._filter_nets_l3(context, nets, filters)
+        return [self._fields(net, fields) for net in nets]
+
+    def _extend_port_dict_binding(self, context, port):
+        if self._check_view_auth(context, port, self.binding_view):
+            port_binding = db.get_port_profile_binding(context.session,
+                                                       port['id'])
+            if port_binding:
+                port[portbindings.VIF_TYPE] = port_binding.vnic_type
+            port[portbindings.CAPABILITIES] = {
+                portbindings.CAP_PORT_FILTER:
+                'security-group' in self.supported_extension_aliases}
+            binding = db.get_network_binding(context.session,
+                                             port['network_id'])
+            fabric = binding.physical_network
+            port[portbindings.PROFILE] = {'physical_network': fabric}
+        return port
+
+    def create_port(self, context, port):
+        LOG.debug(_("create_port with %s"), port)
+        vnic_type = self._process_port_binding_create(context, port['port'])
+        port = super(MellanoxEswitchPlugin, self).create_port(context, port)
+        db.add_port_profile_binding(context.session, port['id'], vnic_type)
+        return self._extend_port_dict_binding(context, port)
+
+    def get_port(self, context, id, fields=None):
+        port = super(MellanoxEswitchPlugin, self).get_port(context, id, fields)
+        return self._fields(self._extend_port_dict_binding(context, port),
+                            fields)
+
+    def get_ports(self, context, filters=None, fields=None):
+        ports = super(MellanoxEswitchPlugin, self).get_ports(
+            context, filters, fields)
+        return [self._fields(self._extend_port_dict_binding(context, port),
+                             fields) for port in ports]
+
+    def update_port(self, context, port_id, port):
+        original_port = super(MellanoxEswitchPlugin, self).get_port(context,
+                                                                    port_id)
+        session = context.session
+        with session.begin(subtransactions=True):
+            port = super(MellanoxEswitchPlugin, self).update_port(context,
+                                                                  port_id,
+                                                                  port)
+        if original_port['admin_state_up'] != port['admin_state_up']:
+            binding = db.get_network_binding(context.session,
+                                             port['network_id'])
+            self.notifier.port_update(context, port,
+                                      binding.physical_network,
+                                      binding.network_type,
+                                      binding.segmentation_id)
+        return self._extend_port_dict_binding(context, port)
+
+    def delete_port(self, context, port_id, l3_port_check=True):
+        # if needed, check to see if this is a port owned by
+        # and l3-router.  If so, we should prevent deletion.
+        if l3_port_check:
+            self.prevent_l3_port_deletion(context, port_id)
+
+        session = context.session
+        with session.begin(subtransactions=True):
+            self.disassociate_floatingips(context, port_id)
+
+            return super(MellanoxEswitchPlugin, self).delete_port(context,
+                                                                  port_id)
diff --git a/quantum/plugins/mlnx/rpc_callbacks.py b/quantum/plugins/mlnx/rpc_callbacks.py
new file mode 100644 (file)
index 0000000..1d8623b
--- /dev/null
@@ -0,0 +1,123 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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.
+
+from quantum.common import constants as q_const
+from quantum.common import rpc as q_rpc
+from quantum.db import agents_db
+from quantum.db import api as db_api
+from quantum.db import dhcp_rpc_base
+from quantum.db import l3_rpc_base
+from quantum.db import securitygroups_rpc_base as sg_db_rpc
+from quantum.openstack.common import log as logging
+from quantum.plugins.mlnx.db import mlnx_db_v2 as db
+
+LOG = logging.getLogger(__name__)
+
+
+class MlnxRpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin,
+                       l3_rpc_base.L3RpcCallbackMixin,
+                       sg_db_rpc.SecurityGroupServerRpcCallbackMixin):
+    # History
+    #  1.1 Support Security Group RPC
+    RPC_API_VERSION = '1.1'
+
+    #to be compatible with Linux Bridge Agent on Network Node
+    TAP_PREFIX_LEN = 3
+
+    def __init__(self):
+        pass
+
+    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 q_rpc.PluginRpcDispatcher([self,
+                                          agents_db.AgentExtRpcCallback()])
+
+    @classmethod
+    def get_port_from_device(cls, device):
+        """Get port according to device.
+
+        To maintain compatibility with Linux Bridge L2 Agent for DHCP/L3
+        services get device either by linux bridge plugin
+        device name convention or by mac address
+        """
+        port = db.get_port_from_device(device[cls.TAP_PREFIX_LEN:])
+        if port:
+            port['device'] = device
+        else:
+            port = db.get_port_from_device_mac(device)
+        return port
+
+    def get_device_details(self, rpc_context, **kwargs):
+        """Agent requests device details."""
+        agent_id = kwargs.get('agent_id')
+        device = kwargs.get('device')
+        LOG.debug("Device %s details requested from %s", device, agent_id)
+        port = self.get_port_from_device(device)
+        if port:
+            binding = db.get_network_binding(db_api.get_session(),
+                                             port['network_id'])
+            entry = {'device': device,
+                     'physical_network': binding.physical_network,
+                     'network_type': binding.network_type,
+                     'vlan_id': binding.segmentation_id,
+                     'network_id': port['network_id'],
+                     'port_mac': port['mac_address'],
+                     'port_id': port['id'],
+                     'admin_state_up': port['admin_state_up']}
+            # Set the port status to UP
+            db.set_port_status(port['id'], q_const.PORT_STATUS_ACTIVE)
+        else:
+            entry = {'device': device}
+            LOG.debug("%s can not be found in database", device)
+        return entry
+
+    def update_device_down(self, rpc_context, **kwargs):
+        """Device no longer exists on agent."""
+        agent_id = kwargs.get('agent_id')
+        device = kwargs.get('device')
+        LOG.debug(_("Device %(device)s no longer exists on %(agent_id)s"),
+                  {'device': device, 'agent_id': agent_id})
+        port = db.get_port_from_device(device)
+        if port:
+            entry = {'device': device,
+                     'exists': True}
+            # Set port status to DOWN
+            db.set_port_status(port['id'], q_const.PORT_STATUS_DOWN)
+        else:
+            entry = {'device': device,
+                     'exists': False}
+            LOG.debug(_("%s can not be found in database"), device)
+        return entry
+
+    def update_device_up(self, rpc_context, **kwargs):
+        """Device is up on agent."""
+        agent_id = kwargs.get('agent_id')
+        device = kwargs.get('device')
+        LOG.debug(_("Device %(device)s up %(agent_id)s"),
+                  {'device': device, 'agent_id': agent_id})
+        port = self.get_port_from_device(device)
+        if port:
+            if port['status'] != q_const.PORT_STATUS_ACTIVE:
+                # Set port status to ACTIVE
+                db.set_port_status(port['id'], q_const.PORT_STATUS_ACTIVE)
+        else:
+            LOG.debug(_("%s can not be found in database"), device)
diff --git a/quantum/tests/unit/mlnx/__init__.py b/quantum/tests/unit/mlnx/__init__.py
new file mode 100644 (file)
index 0000000..c818bfe
--- /dev/null
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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/tests/unit/mlnx/test_defaults.py b/quantum/tests/unit/mlnx/test_defaults.py
new file mode 100644 (file)
index 0000000..d8c2109
--- /dev/null
@@ -0,0 +1,36 @@
+# Copyright (c) 2013 OpenStack, LLC.
+#
+# 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.
+
+from oslo.config import cfg
+
+#NOTE this import loads tests required options
+from quantum.plugins.mlnx.common import config  # noqa
+from quantum.tests import base
+
+
+class ConfigurationTest(base.BaseTestCase):
+
+    def test_defaults(self):
+        self.assertEqual(2,
+                         cfg.CONF.AGENT.polling_interval)
+        self.assertEqual('sudo',
+                         cfg.CONF.AGENT.root_helper)
+        self.assertEqual('vlan',
+                         cfg.CONF.MLNX.tenant_network_type)
+        self.assertEqual(1,
+                         len(cfg.CONF.MLNX.network_vlan_ranges))
+        self.assertEqual(0,
+                         len(cfg.CONF.ESWITCH.
+                             physical_interface_mappings))
diff --git a/quantum/tests/unit/mlnx/test_mlnx_db.py b/quantum/tests/unit/mlnx/test_mlnx_db.py
new file mode 100644 (file)
index 0000000..680dc22
--- /dev/null
@@ -0,0 +1,180 @@
+# Copyright (c) 2013 OpenStack, LLC.
+#
+# 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.
+
+from testtools import matchers
+
+from quantum.common import exceptions as q_exc
+from quantum.db import api as db
+from quantum.plugins.mlnx.db import mlnx_db_v2 as mlnx_db
+from quantum.tests import base
+from quantum.tests.unit import test_db_plugin as test_plugin
+
+PHYS_NET = 'physnet1'
+PHYS_NET_2 = 'physnet2'
+NET_TYPE = 'vlan'
+VLAN_MIN = 10
+VLAN_MAX = 19
+VLAN_RANGES = {PHYS_NET: [(VLAN_MIN, VLAN_MAX)]}
+UPDATED_VLAN_RANGES = {PHYS_NET: [(VLAN_MIN + 5, VLAN_MAX + 5)],
+                       PHYS_NET_2: [(VLAN_MIN + 20, VLAN_MAX + 20)]}
+TEST_NETWORK_ID = 'abcdefghijklmnopqrstuvwxyz'
+
+
+class SegmentationIdAllocationTest(base.BaseTestCase):
+    def setUp(self):
+        super(SegmentationIdAllocationTest, self).setUp()
+        mlnx_db.initialize()
+        mlnx_db.sync_network_states(VLAN_RANGES)
+        self.session = db.get_session()
+        self.addCleanup(db.clear_db)
+
+    def test_sync_segmentationIdAllocation(self):
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET,
+                                                    VLAN_MIN - 1))
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MIN).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MIN + 1).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MAX - 1).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MAX).allocated)
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET,
+                                                    VLAN_MAX + 1))
+
+        mlnx_db.sync_network_states(UPDATED_VLAN_RANGES)
+
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET,
+                                                    VLAN_MIN + 5 - 1))
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MIN + 5).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MIN + 5 + 1).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MAX + 5 - 1).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MAX + 5).allocated)
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET,
+                                                    VLAN_MAX + 5 + 1))
+
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET_2,
+                                                    VLAN_MIN + 20 - 1))
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET_2,
+                                                   VLAN_MIN + 20).allocated)
+        self.assertFalse(
+            mlnx_db.get_network_state(PHYS_NET_2,
+                                      VLAN_MIN + 20 + 1).allocated)
+        self.assertFalse(
+            mlnx_db.get_network_state(PHYS_NET_2,
+                                      VLAN_MAX + 20 - 1).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET_2,
+                                                   VLAN_MAX + 20).allocated)
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET_2,
+                                                    VLAN_MAX + 20 + 1))
+
+        mlnx_db.sync_network_states(VLAN_RANGES)
+
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET,
+                                                    VLAN_MIN - 1))
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MIN).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MIN + 1).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MAX - 1).allocated)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   VLAN_MAX).allocated)
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET,
+                                                    VLAN_MAX + 1))
+
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET_2,
+                                                    VLAN_MIN + 20))
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET_2,
+                                                    VLAN_MAX + 20))
+
+    def test_segmentationId_pool(self):
+        vlan_ids = set()
+        for x in xrange(VLAN_MIN, VLAN_MAX + 1):
+            physical_network, vlan_id = mlnx_db.reserve_network(self.session)
+            self.assertEqual(physical_network, PHYS_NET)
+            self.assertThat(vlan_id, matchers.GreaterThan(VLAN_MIN - 1))
+            self.assertThat(vlan_id, matchers.LessThan(VLAN_MAX + 1))
+            vlan_ids.add(vlan_id)
+
+        self.assertRaises(q_exc.NoNetworkAvailable,
+                          mlnx_db.reserve_network,
+                          self.session)
+        for vlan_id in vlan_ids:
+            mlnx_db.release_network(self.session, PHYS_NET,
+                                    vlan_id, VLAN_RANGES)
+
+    def test_specific_segmentationId_inside_pool(self):
+        vlan_id = VLAN_MIN + 5
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   vlan_id).allocated)
+        mlnx_db.reserve_specific_network(self.session, PHYS_NET, vlan_id)
+        self.assertTrue(mlnx_db.get_network_state(PHYS_NET,
+                                                  vlan_id).allocated)
+
+        self.assertRaises(q_exc.VlanIdInUse,
+                          mlnx_db.reserve_specific_network,
+                          self.session,
+                          PHYS_NET,
+                          vlan_id)
+
+        mlnx_db.release_network(self.session, PHYS_NET, vlan_id, VLAN_RANGES)
+        self.assertFalse(mlnx_db.get_network_state(PHYS_NET,
+                                                   vlan_id).allocated)
+
+    def test_specific_segmentationId_outside_pool(self):
+        vlan_id = VLAN_MAX + 5
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET, vlan_id))
+        mlnx_db.reserve_specific_network(self.session, PHYS_NET, vlan_id)
+        self.assertTrue(mlnx_db.get_network_state(PHYS_NET,
+                                                  vlan_id).allocated)
+
+        self.assertRaises(q_exc.VlanIdInUse,
+                          mlnx_db.reserve_specific_network,
+                          self.session,
+                          PHYS_NET,
+                          vlan_id)
+
+        mlnx_db.release_network(self.session, PHYS_NET, vlan_id, VLAN_RANGES)
+        self.assertIsNone(mlnx_db.get_network_state(PHYS_NET, vlan_id))
+
+
+class NetworkBindingsTest(test_plugin.QuantumDbPluginV2TestCase):
+    def setUp(self):
+        super(NetworkBindingsTest, self).setUp()
+        mlnx_db.initialize()
+        self.session = db.get_session()
+
+    def test_add_network_binding(self):
+        with self.network() as network:
+            TEST_NETWORK_ID = network['network']['id']
+            self.assertIsNone(mlnx_db.get_network_binding(self.session,
+                                                          TEST_NETWORK_ID))
+            mlnx_db.add_network_binding(self.session,
+                                        TEST_NETWORK_ID,
+                                        NET_TYPE,
+                                        PHYS_NET,
+                                        1234)
+            binding = mlnx_db.get_network_binding(self.session,
+                                                  TEST_NETWORK_ID)
+            self.assertIsNotNone(binding)
+            self.assertEqual(binding.network_id, TEST_NETWORK_ID)
+            self.assertEqual(binding.network_type, NET_TYPE)
+            self.assertEqual(binding.physical_network, PHYS_NET)
+            self.assertEqual(binding.segmentation_id, 1234)
diff --git a/quantum/tests/unit/mlnx/test_mlnx_plugin.py b/quantum/tests/unit/mlnx/test_mlnx_plugin.py
new file mode 100644 (file)
index 0000000..1fe6ce1
--- /dev/null
@@ -0,0 +1,62 @@
+# Copyright (c) 2013 OpenStack, LLC.
+#
+# 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.
+
+from quantum import context
+from quantum.manager import QuantumManager
+from quantum.plugins.mlnx.common import constants
+from quantum.tests.unit import test_db_plugin as test_plugin
+
+PLUGIN_NAME = ('quantum.plugins.mlnx.mlnx_plugin.MellanoxEswitchPlugin')
+
+
+class MlnxPluginV2TestCase(test_plugin.QuantumDbPluginV2TestCase):
+    _plugin_name = PLUGIN_NAME
+
+    def setUp(self):
+        super(MlnxPluginV2TestCase, self).setUp(self._plugin_name)
+
+
+class TestMlnxBasicGet(test_plugin.TestBasicGet, MlnxPluginV2TestCase):
+    pass
+
+
+class TestMlnxV2HTTPResponse(test_plugin.TestV2HTTPResponse,
+                             MlnxPluginV2TestCase):
+    pass
+
+
+class TestMlnxPortsV2(test_plugin.TestPortsV2,
+                      MlnxPluginV2TestCase):
+    VIF_TYPE = constants.VIF_TYPE_DIRECT
+    HAS_PORT_FILTER = False
+
+    def test_port_vif_details(self):
+        plugin = QuantumManager.get_plugin()
+        with self.port(name='name') as port:
+            port_id = port['port']['id']
+            self.assertEqual(port['port']['binding:vif_type'],
+                             self.VIF_TYPE)
+            # By default user is admin - now test non admin user
+            ctx = context.Context(user_id=None,
+                                  tenant_id=self._tenant_id,
+                                  is_admin=False,
+                                  read_deleted="no")
+            non_admin_port = plugin.get_port(ctx, port_id)
+            self.assertIn('status', non_admin_port)
+            self.assertNotIn('binding:vif_type', non_admin_port)
+
+
+class TestMlnxNetworksV2(test_plugin.TestNetworksV2, MlnxPluginV2TestCase):
+    pass
diff --git a/quantum/tests/unit/mlnx/test_rpcapi.py b/quantum/tests/unit/mlnx/test_rpcapi.py
new file mode 100644 (file)
index 0000000..419f8a2
--- /dev/null
@@ -0,0 +1,102 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2013 Mellanox Technologies, Ltd
+#
+# 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.
+
+"""
+Unit Tests for Mellanox RPC (major reuse of linuxbridge rpc unit tests)
+"""
+
+import stubout
+
+from quantum.agent import rpc as agent_rpc
+from quantum.common import topics
+from quantum.openstack.common import context
+from quantum.openstack.common import rpc
+from quantum.plugins.mlnx import agent_notify_api
+from quantum.tests import base
+
+
+class rpcApiTestCase(base.BaseTestCase):
+
+    def _test_mlnx_api(self, rpcapi, topic, method, rpc_method, **kwargs):
+        ctxt = context.RequestContext('fake_user', 'fake_project')
+        expected_retval = 'foo' if method == 'call' else None
+        expected_msg = rpcapi.make_msg(method, **kwargs)
+        expected_msg['version'] = rpcapi.BASE_RPC_API_VERSION
+        if rpc_method == 'cast' and method == 'run_instance':
+            kwargs['call'] = False
+
+        self.fake_args = None
+        self.fake_kwargs = None
+
+        def _fake_rpc_method(*args, **kwargs):
+            self.fake_args = args
+            self.fake_kwargs = kwargs
+            if expected_retval:
+                return expected_retval
+
+        self.stubs = stubout.StubOutForTesting()
+        self.stubs.Set(rpc, rpc_method, _fake_rpc_method)
+
+        retval = getattr(rpcapi, method)(ctxt, **kwargs)
+
+        self.assertEqual(retval, expected_retval)
+        expected_args = [ctxt, topic, expected_msg]
+
+        for arg, expected_arg in zip(self.fake_args, expected_args):
+            self.assertEqual(arg, expected_arg)
+
+    def test_delete_network(self):
+        rpcapi = agent_notify_api.AgentNotifierApi(topics.AGENT)
+        self._test_mlnx_api(rpcapi,
+                            topics.get_topic_name(topics.AGENT,
+                                                  topics.NETWORK,
+                                                  topics.DELETE),
+                            'network_delete', rpc_method='fanout_cast',
+                            network_id='fake_request_spec')
+
+    def test_port_update(self):
+        rpcapi = agent_notify_api.AgentNotifierApi(topics.AGENT)
+        self._test_mlnx_api(rpcapi,
+                            topics.get_topic_name(topics.AGENT,
+                                                  topics.PORT,
+                                                  topics.UPDATE),
+                            'port_update', rpc_method='fanout_cast',
+                            port='fake_port',
+                            network_type='vlan',
+                            physical_network='fake_net',
+                            vlan_id='fake_vlan_id')
+
+    def test_device_details(self):
+        rpcapi = agent_rpc.PluginApi(topics.PLUGIN)
+        self._test_mlnx_api(rpcapi, topics.PLUGIN,
+                            'get_device_details', rpc_method='call',
+                            device='fake_device',
+                            agent_id='fake_agent_id')
+
+    def test_update_device_down(self):
+        rpcapi = agent_rpc.PluginApi(topics.PLUGIN)
+        self._test_mlnx_api(rpcapi, topics.PLUGIN,
+                            'update_device_down', rpc_method='call',
+                            device='fake_device',
+                            agent_id='fake_agent_id')
+
+    def test_update_device_up(self):
+        rpcapi = agent_rpc.PluginApi(topics.PLUGIN)
+        self._test_mlnx_api(rpcapi, topics.PLUGIN,
+                            'update_device_up', rpc_method='call',
+                            device='fake_device',
+                            agent_id='fake_agent_id')
index 3198bdd49e947121c9de1a54a923944211e3b3fd..e3aafe6a7b4fa1728ccdd40d78cab6deae605b31 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -55,6 +55,7 @@ nec_plugin_config_path = 'etc/quantum/plugins/nec'
 hyperv_plugin_config_path = 'etc/quantum/plugins/hyperv'
 plumgrid_plugin_config_path = 'etc/quantum/plugins/plumgrid'
 midonet_plugin_config_path = 'etc/quantum/plugins/midonet'
+mlnx_plugin_config_path = 'etc/quantum/plugins/mlnx'
 
 if sys.platform == 'win32':
     # Windows doesn't have an "/etc" directory equivalent
@@ -111,6 +112,8 @@ else:
             ['etc/quantum/plugins/plumgrid/plumgrid.ini']),
         (midonet_plugin_config_path,
             ['etc/quantum/plugins/midonet/midonet.ini']),
+        (mlnx_plugin_config_path,
+            ['etc/quantum/plugins/mlnx/mlnx_conf.ini']),
     ]
 
     ConsoleScripts = [
@@ -139,6 +142,8 @@ else:
          'quantum.plugins.services.agent_loadbalancer.agent:main'),
         ('quantum-check-nvp-config = '
          'quantum.plugins.nicira.check_nvp_config:main'),
+        ('quantum-mlnx-agent ='
+        'quantum.plugins.mlnx.agent.eswitch_quantum_agent:main'),
     ]
 
     ProjectScripts = [