From: Irena Berezovsky Date: Mon, 11 Mar 2013 10:10:15 +0000 (+0200) Subject: blueprint mellanox-quantum-plugin X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=94492767e009fee24b8fcb5b588943ad159c7254;p=openstack-build%2Fneutron-build.git blueprint mellanox-quantum-plugin 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 --- diff --git a/bin/quantum-mlnx-agent b/bin/quantum-mlnx-agent new file mode 100644 index 000000000..263e10373 --- /dev/null +++ b/bin/quantum-mlnx-agent @@ -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 index 000000000..f842fe487 --- /dev/null +++ b/etc/quantum/plugins/mlnx/mlnx_conf.ini @@ -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 +# [::] 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 +# : 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 index 000000000..b61ad4788 --- /dev/null +++ b/quantum/plugins/mlnx/README @@ -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 index 000000000..c818bfe31 --- /dev/null +++ b/quantum/plugins/mlnx/__init__.py @@ -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 index 000000000..c818bfe31 --- /dev/null +++ b/quantum/plugins/mlnx/agent/__init__.py @@ -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 index 000000000..b58072f82 --- /dev/null +++ b/quantum/plugins/mlnx/agent/eswitch_quantum_agent.py @@ -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 index 000000000..8e3adbb8b --- /dev/null +++ b/quantum/plugins/mlnx/agent/utils.py @@ -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 index 000000000..29d7c2311 --- /dev/null +++ b/quantum/plugins/mlnx/agent_notify_api.py @@ -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 index 000000000..c818bfe31 --- /dev/null +++ b/quantum/plugins/mlnx/common/__init__.py @@ -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 index 000000000..672e256e3 --- /dev/null +++ b/quantum/plugins/mlnx/common/config.py @@ -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 :: " + "or ")), +] + + +eswitch_opts = [ + cfg.ListOpt('physical_interface_mappings', + default=DEFAULT_INTERFACE_MAPPINGS, + help=_("List of :")), + 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 index 000000000..eb6c1b8ef --- /dev/null +++ b/quantum/plugins/mlnx/common/constants.py @@ -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 index 000000000..e4c45b2b1 --- /dev/null +++ b/quantum/plugins/mlnx/common/exceptions.py @@ -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 index 000000000..c818bfe31 --- /dev/null +++ b/quantum/plugins/mlnx/db/__init__.py @@ -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 index 000000000..e2c4a7490 --- /dev/null +++ b/quantum/plugins/mlnx/db/mlnx_db_v2.py @@ -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 index 000000000..9c026a4a3 --- /dev/null +++ b/quantum/plugins/mlnx/db/mlnx_models_v2.py @@ -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 "" % (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 "" % (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 "" % (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 index 000000000..b258ad8d1 --- /dev/null +++ b/quantum/plugins/mlnx/mlnx_plugin.py @@ -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 index 000000000..1d8623b33 --- /dev/null +++ b/quantum/plugins/mlnx/rpc_callbacks.py @@ -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 index 000000000..c818bfe31 --- /dev/null +++ b/quantum/tests/unit/mlnx/__init__.py @@ -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 index 000000000..d8c210920 --- /dev/null +++ b/quantum/tests/unit/mlnx/test_defaults.py @@ -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 index 000000000..680dc22bb --- /dev/null +++ b/quantum/tests/unit/mlnx/test_mlnx_db.py @@ -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 index 000000000..1fe6ce1b8 --- /dev/null +++ b/quantum/tests/unit/mlnx/test_mlnx_plugin.py @@ -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 index 000000000..419f8a278 --- /dev/null +++ b/quantum/tests/unit/mlnx/test_rpcapi.py @@ -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') diff --git a/setup.py b/setup.py index 3198bdd49..e3aafe6a7 100644 --- 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 = [