diff --git a/MANIFEST.in b/MANIFEST.in index 9ce2000971..3d36e9b0e0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,4 @@ include etc/* include etc/init.d/* include etc/quantum/plugins/openvswitch/* include etc/quantum/plugins/cisco/* +include etc/quantum/plugins/linuxbridge/* diff --git a/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini b/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini new file mode 100644 index 0000000000..8bead52e99 --- /dev/null +++ b/etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini @@ -0,0 +1,24 @@ +[VLANS] +vlan_start=1000 +vlan_end=3000 + +[DATABASE] +# Use the following when running the tests for the in-memory DB +connection = sqlite +# Uncomment the following for using the MySQL DB when actually running the plugin, +# also remove the earlier sqlite connection configuration +#connection = mysql +name = quantum_linux_bridge +user = +pass = +host = +# If you use a non-default port for the DB, change the following +port = 3306 + +[LINUX_BRIDGE] +#this is the interface connected to the switch on your Quantum network +physical_interface = eth1 + +[AGENT] +#agent's polling interval in seconds +polling_interval = 2 diff --git a/quantum/plugins/linuxbridge/LinuxBridgePlugin.py b/quantum/plugins/linuxbridge/LinuxBridgePlugin.py new file mode 100644 index 0000000000..1037ccbf82 --- /dev/null +++ b/quantum/plugins/linuxbridge/LinuxBridgePlugin.py @@ -0,0 +1,257 @@ +""" +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012, Cisco Systems, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# @author: Sumit Naiksatam, Cisco Systems, Inc. +""" + +import logging + +from quantum.api.api_common import OperationalStatus +from quantum.common import exceptions as exc +from quantum.db import api as db +from quantum.plugins.linuxbridge import plugin_configuration as conf +from quantum.plugins.linuxbridge.common import constants as const +from quantum.plugins.linuxbridge.common import utils as cutil +from quantum.plugins.linuxbridge.db import l2network_db as cdb +from quantum.quantum_plugin_base import QuantumPluginBase + + +LOG = logging.getLogger(__name__) + + +class LinuxBridgePlugin(QuantumPluginBase): + """ + LinuxBridgePlugin provides support for Quantum abstractions + using LinuxBridge. A new VLAN is created for each network. + It relies on an agent to perform the actual bridge configuration + on each host. + """ + + def __init__(self, configfile=None): + cdb.initialize() + LOG.debug("Linux Bridge Plugin initialization done successfully") + + def _get_vlan_for_tenant(self, tenant_id, **kwargs): + """Get an available VLAN ID""" + try: + return cdb.reserve_vlanid() + except: + raise Exception("Failed to reserve VLAN ID for network") + + def _release_vlan_for_tenant(self, tenant_id, net_id, **kwargs): + """Release the ID""" + vlan_binding = cdb.get_vlan_binding(net_id) + return cdb.release_vlanid(vlan_binding[const.VLANID]) + + def _validate_port_state(self, port_state): + if port_state.upper() not in ('ACTIVE', 'DOWN'): + raise exc.StateInvalid(port_state=port_state) + return True + + def get_all_networks(self, tenant_id, **kwargs): + """ + Returns a dictionary containing all + for + the specified tenant. + """ + LOG.debug("LinuxBridgePlugin.get_all_networks() called") + networks_list = db.network_list(tenant_id) + new_networks_list = [] + for network in networks_list: + new_network_dict = cutil.make_net_dict(network[const.UUID], + network[const.NETWORKNAME], + [], network[const.OPSTATUS]) + new_networks_list.append(new_network_dict) + + # This plugin does not perform filtering at the moment + return new_networks_list + + def get_network_details(self, tenant_id, net_id): + """ + retrieved a list of all the remote vifs that + are attached to the network + """ + LOG.debug("LinuxBridgePlugin.get_network_details() called") + network = db.network_get(net_id) + ports_list = db.port_list(net_id) + ports_on_net = [] + for port in ports_list: + new_port = cutil.make_port_dict(port) + ports_on_net.append(new_port) + + new_network = cutil.make_net_dict(network[const.UUID], + network[const.NETWORKNAME], + ports_on_net, + network[const.OPSTATUS]) + + return new_network + + def create_network(self, tenant_id, net_name, **kwargs): + """ + Creates a new Virtual Network, and assigns it + a symbolic name. + """ + LOG.debug("LinuxBridgePlugin.create_network() called") + new_network = db.network_create(tenant_id, net_name, + op_status=OperationalStatus.UP) + new_net_id = new_network[const.UUID] + vlan_id = self._get_vlan_for_tenant(tenant_id) + cdb.add_vlan_binding(vlan_id, new_net_id) + new_net_dict = {const.NET_ID: new_net_id, + const.NET_NAME: net_name, + const.NET_PORTS: [], + const.NET_OP_STATUS: new_network[const.OPSTATUS]} + return new_net_dict + + def delete_network(self, tenant_id, net_id): + """ + Deletes the network with the specified network identifier + belonging to the specified tenant. + """ + LOG.debug("LinuxBridgePlugin.delete_network() called") + net = db.network_get(net_id) + if net: + ports_on_net = db.port_list(net_id) + if len(ports_on_net) > 0: + for port in ports_on_net: + if port[const.INTERFACEID]: + raise exc.NetworkInUse(net_id=net_id) + for port in ports_on_net: + self.delete_port(tenant_id, net_id, port[const.UUID]) + + net_dict = cutil.make_net_dict(net[const.UUID], + net[const.NETWORKNAME], + [], net[const.OPSTATUS]) + try: + self._release_vlan_for_tenant(tenant_id, net_id) + cdb.remove_vlan_binding(net_id) + except Exception as excp: + LOG.warning("Exception: %s" % excp) + db.network_update(net_id, tenant_id, {const.OPSTATUS: + OperationalStatus.DOWN}) + db.network_destroy(net_id) + return net_dict + # Network not found + raise exc.NetworkNotFound(net_id=net_id) + + def update_network(self, tenant_id, net_id, **kwargs): + """ + Updates the attributes of a particular Virtual Network. + """ + LOG.debug("LinuxBridgePlugin.update_network() called") + network = db.network_update(net_id, tenant_id, **kwargs) + net_dict = cutil.make_net_dict(network[const.UUID], + network[const.NETWORKNAME], + [], network[const.OPSTATUS]) + return net_dict + + def get_all_ports(self, tenant_id, net_id, **kwargs): + """ + Retrieves all port identifiers belonging to the + specified Virtual Network. + """ + LOG.debug("LinuxBridgePlugin.get_all_ports() called") + network = db.network_get(net_id) + ports_list = db.port_list(net_id) + ports_on_net = [] + for port in ports_list: + new_port = cutil.make_port_dict(port) + ports_on_net.append(new_port) + + # This plugin does not perform filtering at the moment + return ports_on_net + + def get_port_details(self, tenant_id, net_id, port_id): + """ + This method allows the user to retrieve a remote interface + that is attached to this particular port. + """ + LOG.debug("LinuxBridgePlugin.get_port_details() called") + network = db.network_get(net_id) + port = db.port_get(port_id, net_id) + new_port_dict = cutil.make_port_dict(port) + return new_port_dict + + def create_port(self, tenant_id, net_id, port_state=None, **kwargs): + """ + Creates a port on the specified Virtual Network. + """ + LOG.debug("LinuxBridgePlugin.create_port() called") + port = db.port_create(net_id, port_state, + op_status=OperationalStatus.DOWN) + unique_port_id_string = port[const.UUID] + new_port_dict = cutil.make_port_dict(port) + return new_port_dict + + def update_port(self, tenant_id, net_id, port_id, **kwargs): + """ + Updates the attributes of a port on the specified Virtual Network. + """ + LOG.debug("LinuxBridgePlugin.update_port() called") + network = db.network_get(net_id) + self._validate_port_state(kwargs["state"]) + port = db.port_update(port_id, net_id, **kwargs) + + new_port_dict = cutil.make_port_dict(port) + return new_port_dict + + def delete_port(self, tenant_id, net_id, port_id): + """ + Deletes a port on a specified Virtual Network, + if the port contains a remote interface attachment, + the remote interface is first un-plugged and then the port + is deleted. + """ + LOG.debug("LinuxBridgePlugin.delete_port() called") + network = db.network_get(net_id) + port = db.port_get(port_id, net_id) + attachment_id = port[const.INTERFACEID] + if not attachment_id: + db.port_destroy(port_id, net_id) + new_port_dict = cutil.make_port_dict(port) + return new_port_dict + else: + raise exc.PortInUse(port_id=port_id, net_id=net_id, + att_id=attachment_id) + + def plug_interface(self, tenant_id, net_id, port_id, remote_interface_id): + """ + Attaches a remote interface to the specified port on the + specified Virtual Network. + """ + LOG.debug("LinuxBridgePlugin.plug_interface() called") + network = db.network_get(net_id) + port = db.port_get(port_id, net_id) + attachment_id = port[const.INTERFACEID] + if attachment_id: + raise exc.PortInUse(port_id=port_id, net_id=net_id, + att_id=attachment_id) + db.port_set_attachment(port_id, net_id, remote_interface_id) + + def unplug_interface(self, tenant_id, net_id, port_id): + """ + Detaches a remote interface from the specified port on the + specified Virtual Network. + """ + LOG.debug("LinuxBridgePlugin.unplug_interface() called") + network = db.network_get(net_id) + port = db.port_get(port_id, net_id) + attachment_id = port[const.INTERFACEID] + if attachment_id == None: + raise exc.InvalidDetach(port_id=port_id, net_id=net_id, + att_id=remote_interface_id) + db.port_unset_attachment(port_id, net_id) + db.port_update(port_id, net_id, op_status=OperationalStatus.DOWN) diff --git a/quantum/plugins/linuxbridge/README b/quantum/plugins/linuxbridge/README new file mode 100644 index 0000000000..c439099469 --- /dev/null +++ b/quantum/plugins/linuxbridge/README @@ -0,0 +1,155 @@ +# -- Background + +The Quantum Linux Bridge plugin is a plugin that allows you to manage +connectivity between VMs on hosts that are capable of running a Linux Bridge. + +The Quantum Linux Bridge plugin consists of three components: + +1) The plugin itself: The plugin uses a database backend (mysql for + now) to store configuration and mappings that are used by the + agent. The mysql server runs on a central server (often the same + host as nova itself). + +2) The quantum service host which will be running quantum. This can + be run on the server running nova. + +3) An agent which runs on the host and communicates with the host operating + system. The agent gathers the configuration and mappings from + the mysql database running on the quantum host. + +The sections below describe how to configure and run the quantum +service with the Linux Bridge plugin. + +# -- Python library dependencies + + Make sure you have the following package(s) installedi on quantum server + host as well as any hosts which run the agent: + python-configobj + bridge-utils + python-mysqldb + sqlite3 + +# -- Nova configuration (controller node) + +1) Make sure to set up nova using the quantum network manager in the + nova.conf on the node that will be running nova-network. + +--network_manager=nova.network.quantum.manager.QuantumManager + +# -- Nova configuration (compute node(s)) + +1) Configure the vif driver, and libvirt/vif type + +--connection_type=libvirt +--libvirt_type=qemu +--libvirt_vif_type=ethernet +--libvirt_vif_driver=nova.virt.libvirt.vif_linuxbridge_quantum.QuantumLibvirtLinuxBridgeDriver +--linuxnet_interface_driver=nova.network.quantum.linux_net_linux_bridge.QuantumLibvirtLinuxBridgeDriver + +The above two drivers are packaged with Quantum in the location: +quantum/plugins/linuxbridge/nova + +These need to copied to the compute node in the appropriate locations. + +a) Copy: + quantum/plugins/linuxbridge/nova/vif_linuxbridge_quantum.py + to: + nova/virt/libvirt/vif_linuxbridge_quantum.py + +b) Copy: + quantum/plugins/linuxbridge/nova/linux_net_linux_bridge.py + to: + nova/network/quantum/linux_net_linux_bridge.py + +2) If you want a DHCP server to be run for the VMs to acquire IPs, + add the following flag to your nova.conf file: + +--quantum_use_dhcp + +(Note: For more details on how to work with Quantum using Nova, i.e. how to create networks and such, + please refer to the top level Quantum README which points to the relevant documentation.) + +# -- Quantum configuration + +Make the Linux Bridge plugin the current quantum plugin + +- edit etc/plugins.ini and change the provider line to be: +provider = quantum.plugins.linuxbridge.LinuxBridgePlugin.LinuxBridgePlugin + +# -- Database config. + +(Note: The plugin ships with a default SQLite in-memory database configuration, + and can be used to run tests without performing the suggested DB config below.) + +The Linux Bridge quantum plugin requires access to a mysql database in order +to store configuration and mappings that will be used by the agent. Here is +how to set up the database on the host that you will be running the quantum +service on. + +MySQL should be installed on the host, and all plugins and clients +must be configured with access to the database. + +To prep mysql, run: + +$ mysql -u root -p -e "create database quantum_linux_bridge" + +# log in to mysql service +$ mysql -u root -p +# The Linux Bridge Quantum agent running on each compute node must be able to +# make a mysql connection back to the main database server. +mysql> GRANT USAGE ON *.* to root@'yourremotehost' IDENTIFIED BY 'newpassword'; +# force update of authorization changes +mysql> FLUSH PRIVILEGES; + +(Note: If the remote connection fails to MySQL, you might need to add the IP address, + and/or fully-qualified hostname, and/or unqualified hostname in the above GRANT sql + command. Also, you might need to specify "ALL" instead of "USAGE".) + +# -- Plugin configuration + +- Edit the configuration file: + etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini + Make sure it matches your mysql configuration. This file must be updated + with the addresses and credentials to access the database. + + Note: When running the tests, set the connection type to sqlite, and when + actually running the server set it to mysql. At any given time, only one + of these should be active in the conf file (you can comment out the other). + +- Remember to change the interface configuration to indicate the correct + ethernet interface on that particular host which is being used to participate + in the Quantum networks. This configuration has to be applied on each host + on which the agent runs. + +# -- Agent configuration + +- Edit the configuration file: + etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini + +- Copy quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py + and etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini + to the compute node. + +$ Run the following: + sudo python linuxbridge_quantum_agent.py linuxbridge_conf.ini + (Use --verbose option to see the logs) + +# -- Running Tests + +(Note: The plugin ships with a default SQLite in-memory database configuration, + and can be used to run tests out of the box. Alternatively you can perform the + DB configuration for a persistent database as mentioned in the Database + Configuration section.) + +- To run tests related to the Plugin and the VLAN management (run the + following from the top level Quantum directory): + PLUGIN_DIR=quantum/plugins/linuxbridge ./run_tests.sh -N + +- The above will not however run the tests for the agent (which deals + with creating the bridge and interfaces). To run the agent tests, run the + following from the top level Quantum directory: + sudo PLUGIN_DIR=quantum/plugins/linuxbridge ./run_tests.sh -N tests.unit._test_linuxbridgeAgent + + (Note: To run the agent tests you should have the environment setup as + indicated in the Agent Configuration, and also have the necessary dependencies + insalled.) diff --git a/quantum/plugins/linuxbridge/__init__.py b/quantum/plugins/linuxbridge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/quantum/plugins/linuxbridge/agent/__init__.py b/quantum/plugins/linuxbridge/agent/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py b/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py new file mode 100755 index 0000000000..3481754056 --- /dev/null +++ b/quantum/plugins/linuxbridge/agent/linuxbridge_quantum_agent.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# +# Performs per host Linux Bridge configuration for Quantum. +# Based on the structure of the OpenVSwitch agent in the +# Quantum OpenVSwitch Plugin. +# @author: Sumit Naiksatam, Cisco Systems, Inc. +# + +from optparse import OptionParser +from subprocess import * + +import ConfigParser +import logging as LOG +import MySQLdb +import os +import signal +import sqlite3 +import sys +import time + + +BRIDGE_NAME_PREFIX = "brq" +GATEWAY_INTERFACE_PREFIX = "gw-" +TAP_INTERFACE_PREFIX = "tap" +BRIDGE_FS = "/sys/devices/virtual/net/" +BRIDGE_NAME_PLACEHOLDER = "bridge_name" +BRIDGE_INTERFACES_FS = BRIDGE_FS + BRIDGE_NAME_PLACEHOLDER + "/brif/" +PORT_OPSTATUS_UPDATESQL = "UPDATE ports SET op_status = '%s' WHERE uuid = '%s'" +DEVICE_NAME_PLACEHOLDER = "device_name" +BRIDGE_PORT_FS_FOR_DEVICE = BRIDGE_FS + DEVICE_NAME_PLACEHOLDER + "/brport" +VLAN_BINDINGS = "vlan_bindings" +PORT_BINDINGS = "port_bindings" +OP_STATUS_UP = "UP" +OP_STATUS_DOWN = "DOWN" +DB_CONNECTION = None + + +class LinuxBridge: + def __init__(self, br_name_prefix, physical_interface): + self.br_name_prefix = br_name_prefix + self.physical_interface = physical_interface + + def run_cmd(self, args): + LOG.debug("Running command: " + " ".join(args)) + p = Popen(args, stdout=PIPE) + retval = p.communicate()[0] + if p.returncode == -(signal.SIGALRM): + LOG.debug("Timeout running command: " + " ".join(args)) + if retval: + LOG.debug("Command returned: %s" % retval) + return retval + + def device_exists(self, device): + """Check if ethernet device exists.""" + retval = self.run_cmd(['ip', 'link', 'show', 'dev', device]) + if retval: + return True + else: + return False + + def get_bridge_name(self, network_id): + if not network_id: + LOG.warning("Invalid Network ID, will lead to incorrect bridge" \ + "name") + bridge_name = self.br_name_prefix + network_id[0:11] + return bridge_name + + def get_subinterface_name(self, vlan_id): + if not vlan_id: + LOG.warning("Invalid VLAN ID, will lead to incorrect " \ + "subinterface name") + subinterface_name = '%s.%s' % (self.physical_interface, vlan_id) + return subinterface_name + + def get_tap_device_name(self, interface_id): + if not interface_id: + LOG.warning("Invalid Interface ID, will lead to incorrect " \ + "tap device name") + tap_device_name = TAP_INTERFACE_PREFIX + interface_id[0:11] + return tap_device_name + + def get_all_quantum_bridges(self): + quantum_bridge_list = [] + bridge_list = os.listdir(BRIDGE_FS) + for bridge in bridge_list: + if bridge.startswith(BRIDGE_NAME_PREFIX): + quantum_bridge_list.append(bridge) + return quantum_bridge_list + + def get_interfaces_on_bridge(self, bridge_name): + if self.device_exists(bridge_name): + bridge_interface_path = \ + BRIDGE_INTERFACES_FS.replace(BRIDGE_NAME_PLACEHOLDER, + bridge_name) + return os.listdir(bridge_interface_path) + + def get_all_tap_devices(self): + tap_devices = [] + retval = self.run_cmd(['ip', 'tuntap']) + rows = retval.split('\n') + for row in rows: + split_row = row.split(':') + if split_row[0].startswith(TAP_INTERFACE_PREFIX): + tap_devices.append(split_row[0]) + + return tap_devices + + def get_all_gateway_devices(self): + gw_devices = [] + retval = self.run_cmd(['ip', 'tuntap']) + rows = retval.split('\n') + for row in rows: + split_row = row.split(':') + if split_row[0].startswith(GATEWAY_INTERFACE_PREFIX): + gw_devices.append(split_row[0]) + + return gw_devices + + def get_bridge_for_tap_device(self, tap_device_name): + bridges = self.get_all_quantum_bridges() + for bridge in bridges: + interfaces = self.get_interfaces_on_bridge(bridge) + if tap_device_name in interfaces: + return bridge + + return None + + def is_device_on_bridge(self, device_name): + if not device_name: + return False + else: + bridge_port_path = \ + BRIDGE_PORT_FS_FOR_DEVICE.replace(DEVICE_NAME_PLACEHOLDER, + device_name) + return os.path.exists(bridge_port_path) + + def ensure_vlan_bridge(self, network_id, vlan_id): + """Create a vlan and bridge unless they already exist.""" + interface = self.ensure_vlan(vlan_id) + bridge_name = self.get_bridge_name(network_id) + self.ensure_bridge(bridge_name, interface) + return interface + + def ensure_vlan(self, vlan_id): + """Create a vlan unless it already exists.""" + interface = self.get_subinterface_name(vlan_id) + if not self.device_exists(interface): + LOG.debug("Creating subinterface %s for VLAN %s on interface %s" % + (interface, vlan_id, self.physical_interface)) + if self.run_cmd(['ip', 'link', 'add', 'link', + self.physical_interface, + 'name', interface, 'type', 'vlan', 'id', + vlan_id]): + return + if self.run_cmd(['ip', 'link', 'set', interface, 'up']): + return + LOG.debug("Done creating subinterface %s" % interface) + return interface + + def ensure_bridge(self, bridge_name, interface): + """ + Create a bridge unless it already exists. + """ + if not self.device_exists(bridge_name): + LOG.debug("Starting bridge %s for subinterface %s" % (bridge_name, + interface)) + if self.run_cmd(['brctl', 'addbr', bridge_name]): + return + if self.run_cmd(['brctl', 'setfd', bridge_name, str(0)]): + return + if self.run_cmd(['brctl', 'stp', bridge_name, 'off']): + return + if self.run_cmd(['ip', 'link', 'set', bridge_name, 'up']): + return + LOG.debug("Done starting bridge %s for subinterface %s" % + (bridge_name, interface)) + + self.run_cmd(['brctl', 'addif', bridge_name, interface]) + + def add_tap_interface(self, network_id, vlan_id, tap_device_name): + """ + If a VIF has been plugged into a network, this function will + add the corresponding tap device to the relevant bridge + """ + if not tap_device_name: + return False + + if not self.device_exists(tap_device_name): + LOG.debug("Tap device: %s does not exist on this host, skipped" % + tap_device_name) + return False + + current_bridge_name = \ + self.get_bridge_for_tap_device(tap_device_name) + bridge_name = self.get_bridge_name(network_id) + if bridge_name == current_bridge_name: + return False + LOG.debug("Adding device %s to bridge %s" % (tap_device_name, + bridge_name)) + if current_bridge_name: + if self.run_cmd(['brctl', 'delif', current_bridge_name, + tap_device_name]): + return False + + self.ensure_vlan_bridge(network_id, vlan_id) + if self.run_cmd(['brctl', 'addif', bridge_name, tap_device_name]): + return False + LOG.debug("Done adding device %s to bridge %s" % (tap_device_name, + bridge_name)) + return True + + def add_interface(self, network_id, vlan_id, interface_id): + if not interface_id: + """ + Since the VIF id is null, no VIF is plugged into this port + no more processing is required + """ + return False + if interface_id.startswith(GATEWAY_INTERFACE_PREFIX): + return self.add_tap_interface(network_id, vlan_id, + interface_id) + else: + tap_device_name = self.get_tap_device_name(interface_id) + return self.add_tap_interface(network_id, vlan_id, + tap_device_name) + + def delete_vlan_bridge(self, bridge_name): + if self.device_exists(bridge_name): + interfaces_on_bridge = self.get_interfaces_on_bridge(bridge_name) + for interface in interfaces_on_bridge: + self.remove_interface(bridge_name, interface) + if interface.startswith(self.physical_interface): + self.delete_vlan(interface) + + LOG.debug("Deleting bridge %s" % bridge_name) + if self.run_cmd(['ip', 'link', 'set', bridge_name, 'down']): + return + if self.run_cmd(['brctl', 'delbr', bridge_name]): + return + LOG.debug("Done deleting bridge %s" % bridge_name) + + else: + LOG.error("Cannot delete bridge %s, does not exist" % bridge_name) + + def remove_interface(self, bridge_name, interface_name): + if self.device_exists(bridge_name): + if not self.is_device_on_bridge(interface_name): + return True + LOG.debug("Removing device %s from bridge %s" % \ + (interface_name, bridge_name)) + if self.run_cmd(['brctl', 'delif', bridge_name, interface_name]): + return False + LOG.debug("Done removing device %s from bridge %s" % \ + (interface_name, bridge_name)) + return True + else: + LOG.debug("Cannot remove device %s, bridge %s does not exist" % \ + (interface_name, bridge_name)) + return False + + def delete_vlan(self, interface): + if self.device_exists(interface): + LOG.debug("Deleting subinterface %s for vlan" % interface) + if self.run_cmd(['ip', 'link', 'set', interface, 'down']): + return + if self.run_cmd(['ip', 'link', 'delete', interface]): + return + LOG.debug("Done deleting subinterface %s" % interface) + + +class LinuxBridgeQuantumAgent: + + def __init__(self, br_name_prefix, physical_interface, polling_interval): + self.polling_interval = int(polling_interval) + self.setup_linux_bridge(br_name_prefix, physical_interface) + + def setup_linux_bridge(self, br_name_prefix, physical_interface): + self.linux_br = LinuxBridge(br_name_prefix, physical_interface) + + def process_port_binding(self, port_id, network_id, interface_id, + vlan_id): + return self.linux_br.add_interface(network_id, vlan_id, interface_id) + + def process_unplugged_interfaces(self, plugged_interfaces): + """ + If there are any tap devices that are not corresponding to the + list of attached VIFs, then those are corresponding to recently + unplugged VIFs, so we need to remove those tap devices from their + current bridge association + """ + plugged_tap_device_names = [] + plugged_gateway_device_names = [] + for interface in plugged_interfaces: + if interface.startswith(GATEWAY_INTERFACE_PREFIX): + """ + The name for the gateway devices is set by the linux net + driver, hence we use the name as is + """ + plugged_gateway_device_names.append(interface) + else: + tap_device_name = self.linux_br.get_tap_device_name(interface) + plugged_tap_device_names.append(tap_device_name) + + LOG.debug("plugged tap device names %s" % plugged_tap_device_names) + for tap_device in self.linux_br.get_all_tap_devices(): + if tap_device not in plugged_tap_device_names: + current_bridge_name = \ + self.linux_br.get_bridge_for_tap_device(tap_device) + if current_bridge_name: + self.linux_br.remove_interface(current_bridge_name, + tap_device) + + for gw_device in self.linux_br.get_all_gateway_devices(): + if gw_device not in plugged_gateway_device_names: + current_bridge_name = \ + self.linux_br.get_bridge_for_tap_device(gw_device) + if current_bridge_name: + self.linux_br.remove_interface(current_bridge_name, + gw_device) + + def process_deleted_networks(self, vlan_bindings): + current_quantum_networks = vlan_bindings.keys() + current_quantum_bridge_names = [] + for network in current_quantum_networks: + bridge_name = self.linux_br.get_bridge_name(network) + current_quantum_bridge_names.append(bridge_name) + + quantum_bridges_on_this_host = self.linux_br.get_all_quantum_bridges() + for bridge in quantum_bridges_on_this_host: + if bridge not in current_quantum_bridge_names: + self.linux_br.delete_vlan_bridge(bridge) + + def manage_networks_on_host(self, conn, old_vlan_bindings, + old_port_bindings): + if DB_CONNECTION != 'sqlite': + cursor = MySQLdb.cursors.DictCursor(conn) + else: + cursor = conn.cursor() + cursor.execute("SELECT * FROM vlan_bindings") + rows = cursor.fetchall() + cursor.close() + vlan_bindings = {} + vlans_string = "" + for row in rows: + vlan_bindings[row['network_id']] = row + vlans_string = "%s %s" % (vlans_string, row) + + plugged_interfaces = [] + cursor = MySQLdb.cursors.DictCursor(conn) + cursor.execute("SELECT * FROM ports where state = 'ACTIVE'") + port_bindings = cursor.fetchall() + cursor.close() + + ports_string = "" + for pb in port_bindings: + ports_string = "%s %s" % (ports_string, pb) + if pb['interface_id']: + vlan_id = \ + str(vlan_bindings[pb['network_id']]['vlan_id']) + if self.process_port_binding(pb['uuid'], + pb['network_id'], + pb['interface_id'], + vlan_id): + cursor = MySQLdb.cursors.DictCursor(conn) + sql = PORT_OPSTATUS_UPDATESQL % (pb['uuid'], + OP_STATUS_UP) + cursor.execute(sql) + cursor.close() + plugged_interfaces.append(pb['interface_id']) + + if old_port_bindings != port_bindings: + LOG.debug("Port-bindings: %s" % ports_string) + + self.process_unplugged_interfaces(plugged_interfaces) + + if old_vlan_bindings != vlan_bindings: + LOG.debug("VLAN-bindings: %s" % vlans_string) + + self.process_deleted_networks(vlan_bindings) + + conn.commit() + return {VLAN_BINDINGS: vlan_bindings, + PORT_BINDINGS: port_bindings} + + def daemon_loop(self, conn): + old_vlan_bindings = {} + old_port_bindings = {} + + while True: + bindings = self.manage_networks_on_host(conn, + old_vlan_bindings, + old_port_bindings) + old_vlan_bindings = bindings[VLAN_BINDINGS] + old_port_bindings = bindings[PORT_BINDINGS] + time.sleep(self.polling_interval) + +if __name__ == "__main__": + usagestr = "%prog [OPTIONS] " + parser = OptionParser(usage=usagestr) + parser.add_option("-v", "--verbose", dest="verbose", + action="store_true", default=False, help="turn on verbose logging") + + options, args = parser.parse_args() + + if options.verbose: + LOG.basicConfig(level=LOG.DEBUG) + else: + LOG.basicConfig(level=LOG.WARN) + + if len(args) != 1: + parser.print_help() + sys.exit(1) + + config_file = args[0] + config = ConfigParser.ConfigParser() + conn = None + try: + fh = open(config_file) + fh.close() + config.read(config_file) + br_name_prefix = BRIDGE_NAME_PREFIX + physical_interface = config.get("LINUX_BRIDGE", "physical_interface") + polling_interval = config.get("AGENT", "polling_interval") + 'Establish database connection and load models' + DB_CONNECTION = config.get("DATABASE", "connection") + if DB_CONNECTION == 'sqlite': + LOG.info("Connecting to sqlite DB") + conn = sqlite3.connect(":memory:") + conn.row_factory = sqlite3.Row + else: + db_name = config.get("DATABASE", "name") + db_user = config.get("DATABASE", "user") + db_pass = config.get("DATABASE", "pass") + db_host = config.get("DATABASE", "host") + db_port = int(config.get("DATABASE", "port")) + LOG.info("Connecting to database %s on %s" % (db_name, db_host)) + conn = MySQLdb.connect(host=db_host, user=db_user, port=db_port, + passwd=db_pass, db=db_name) + except Exception, e: + LOG.error("Unable to parse config file \"%s\": \nException%s" + % (config_file, str(e))) + sys.exit(1) + + try: + plugin = LinuxBridgeQuantumAgent(br_name_prefix, physical_interface, + polling_interval) + LOG.info("Agent initialized successfully, now running...") + plugin.daemon_loop(conn) + finally: + if conn: + conn.close() + + sys.exit(0) diff --git a/quantum/plugins/linuxbridge/common/__init__.py b/quantum/plugins/linuxbridge/common/__init__.py new file mode 100644 index 0000000000..f77db6ef68 --- /dev/null +++ b/quantum/plugins/linuxbridge/common/__init__.py @@ -0,0 +1,20 @@ +""" +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Sumit Naiksatam, Cisco Systems, Inc. +# +""" diff --git a/quantum/plugins/linuxbridge/common/configparser.py b/quantum/plugins/linuxbridge/common/configparser.py new file mode 100644 index 0000000000..2b170a2fbf --- /dev/null +++ b/quantum/plugins/linuxbridge/common/configparser.py @@ -0,0 +1,34 @@ +""" +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Sumit Naiksatam, Cisco Systems, Inc. +# +""" + +from configobj import ConfigObj + + +class ConfigParser(ConfigObj): + """Config Parser based on the ConfigObj module""" + + def __init__(self, filename): + super(ConfigParser, self).__init__(filename, raise_errors=True, + file_error=True) + + def dummy(self, section, key): + """Dummy function to return the same key, used in walk""" + return section[key] diff --git a/quantum/plugins/linuxbridge/common/constants.py b/quantum/plugins/linuxbridge/common/constants.py new file mode 100644 index 0000000000..0aff3ff45c --- /dev/null +++ b/quantum/plugins/linuxbridge/common/constants.py @@ -0,0 +1,65 @@ +""" +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Sumit Naiksatam, Cisco Systems, Inc. +# +""" + +PORT_STATE = 'port-state' +PORT_UP = "ACTIVE" +PORT_DOWN = "DOWN" + +UUID = 'uuid' +TENANTID = 'tenant_id' +NETWORKID = 'network_id' +NETWORKNAME = 'name' +NETWORKPORTS = 'ports' +OPSTATUS = 'op_status' +INTERFACEID = 'interface_id' +PORTSTATE = 'state' +PORTID = 'port_id' +PPNAME = 'name' +PPVLANID = 'vlan_id' +VLANID = 'vlan_id' +VLANNAME = 'vlan_name' + +ATTACHMENT = 'attachment' +PORT_ID = 'port-id' +PORT_OP_STATUS = 'port-op-status' + +NET_ID = 'net-id' +NET_NAME = 'net-name' +NET_PORTS = 'net-ports' +NET_OP_STATUS = 'net-op-status' +NET_VLAN_NAME = 'net-vlan-name' +NET_VLAN_ID = 'net-vlan-id' +NET_TENANTS = 'net-tenants' + +USERNAME = 'username' +PASSWORD = 'password' + +DELIMITERS = "[,;:\b\s]" + +UUID_LENGTH = 36 + +UNPLUGGED = '(detached)' + +ASSOCIATION_STATUS = 'association_status' + +ATTACHED = 'attached' + +DETACHED = 'detached' diff --git a/quantum/plugins/linuxbridge/common/exceptions.py b/quantum/plugins/linuxbridge/common/exceptions.py new file mode 100644 index 0000000000..40dafd3538 --- /dev/null +++ b/quantum/plugins/linuxbridge/common/exceptions.py @@ -0,0 +1,72 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Sumit Naiksatam, Cisco Systems, Inc. +# @author: Rohit Agarwalla, Cisco Systems, Inc. + +""" +Exceptions used by the LinuxBridge plugin +""" +from quantum.common import exceptions + + +class NetworksLimit(exceptions.QuantumException): + """Total number of network objects limit has been hit""" + message = _("Unable to create new network. Number of networks" \ + "for the system has exceeded the limit") + + +class NetworkVlanBindingAlreadyExists(exceptions.QuantumException): + """Binding cannot be created, since it already exists""" + message = _("NetworkVlanBinding for %(vlan_id)s and network " \ + "%(network_id)s already exists") + + +class NetworkVlanBindingNotFound(exceptions.QuantumException): + """Binding could not be found""" + message = _("NetworkVlanBinding for network " \ + "%(network_id)s does not exist") + + +class VlanIDNotFound(exceptions.QuantumException): + """VLAN ID cannot be found""" + message = _("Vlan ID %(vlan_id)s not found") + + +class VlanIDNotAvailable(exceptions.QuantumException): + """No VLAN ID available""" + message = _("No Vlan ID available") + + +class UnableToChangeVlanRange(exceptions.QuantumException): + """No VLAN ID available""" + message = _("Current VLAN ID range %(range_start)s to %(range_end)s " \ + "cannot be changed. Please check plugin conf file.") + + +try: + _("test") +except NameError: + + def _(a_string): + """ + Default implementation of the gettext string + translation function: no translation + """ + return a_string +except TypeError: + # during doctesting, _ might mean something else + pass diff --git a/quantum/plugins/linuxbridge/common/utils.py b/quantum/plugins/linuxbridge/common/utils.py new file mode 100644 index 0000000000..569eaa3315 --- /dev/null +++ b/quantum/plugins/linuxbridge/common/utils.py @@ -0,0 +1,50 @@ +""" +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Sumit Naiksatam, Cisco Systems, Inc. +# +""" + +import logging + +from quantum.api.api_common import OperationalStatus +from quantum.plugins.linuxbridge.common import constants as const + +LOG = logging.getLogger(__name__) + + +def make_net_dict(net_id, net_name, ports, op_status): + """Helper funciton""" + res = {const.NET_ID: net_id, const.NET_NAME: net_name, const.NET_OP_STATUS: + op_status} + if ports: + res[const.NET_PORTS] = ports + return res + + +def make_port_dict(port): + """Helper funciton""" + if port[const.PORTSTATE] == const.PORT_UP: + op_status = port[const.OPSTATUS] + else: + op_status = OperationalStatus.DOWN + + return {const.PORT_ID: str(port[const.UUID]), + const.PORT_STATE: port[const.PORTSTATE], + const.PORT_OP_STATUS: op_status, + const.NET_ID: port[const.NETWORKID], + const.ATTACHMENT: port[const.INTERFACEID]} diff --git a/quantum/plugins/linuxbridge/db/__init__.py b/quantum/plugins/linuxbridge/db/__init__.py new file mode 100644 index 0000000000..33daf1f33a --- /dev/null +++ b/quantum/plugins/linuxbridge/db/__init__.py @@ -0,0 +1,18 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Sumit Naiksatam, Cisco Systems, Inc. +# diff --git a/quantum/plugins/linuxbridge/db/l2network_db.py b/quantum/plugins/linuxbridge/db/l2network_db.py new file mode 100644 index 0000000000..c6d64a4ba6 --- /dev/null +++ b/quantum/plugins/linuxbridge/db/l2network_db.py @@ -0,0 +1,255 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Cisco Systems, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# @author: Rohit Agarwalla, Cisco Systems, Inc. + +from sqlalchemy import func +from sqlalchemy.orm import exc + +from quantum.common import exceptions as q_exc +from quantum.plugins.linuxbridge import plugin_configuration as conf +from quantum.plugins.linuxbridge.common import exceptions as c_exc +from quantum.plugins.linuxbridge.db import l2network_models + +import logging + +import quantum.db.api as db + + +LOG = logging.getLogger(__name__) + + +def initialize(): + 'Establish database connection and load models' + if conf.DB_CONNECTION == 'sqlite': + options = {"sql_connection": "sqlite://"} + else: + options = {"sql_connection": "mysql://%s:%s@%s:%s/%s" % (conf.DB_USER, + conf.DB_PASS, + conf.DB_HOST, + conf.DB_PORT, + conf.DB_NAME)} + + db.configure_db(options) + create_vlanids() + + +def create_vlanids(): + """Prepopulates the vlan_bindings table""" + LOG.debug("create_vlanids() called") + session = db.get_session() + start = int(conf.VLAN_START) + end = int(conf.VLAN_END) + try: + vlanid = session.query(l2network_models.VlanID).\ + one() + except exc.MultipleResultsFound: + """ + TODO (Sumit): Salvatore rightly points out that this will not handle + change in VLAN ID range across server reboots. This is currently not + a supported feature. This logic will need to change if this feature + has to be supported. + Per Dan's suggestion we just throw a server exception for now. + """ + current_start = \ + int(session.query(func.min(l2network_models.VlanID.vlan_id)). + one()[0]) + current_end = \ + int(session.query(func.max(l2network_models.VlanID.vlan_id)). + one()[0]) + if current_start != start or current_end != end: + LOG.debug("Old VLAN range %s-%s" % (current_start, current_end)) + LOG.debug("New VLAN range %s-%s" % (start, end)) + raise c_exc.UnableToChangeVlanRange(range_start=current_start, + range_end=current_end) + except exc.NoResultFound: + LOG.debug("Setting VLAN range to %s-%s" % (start, end)) + while start <= end: + vlanid = l2network_models.VlanID(start) + session.add(vlanid) + start += 1 + session.flush() + return + + +def get_all_vlanids(): + """Gets all the vlanids""" + LOG.debug("get_all_vlanids() called") + session = db.get_session() + try: + vlanids = session.query(l2network_models.VlanID).\ + all() + return vlanids + except exc.NoResultFound: + return [] + + +def is_vlanid_used(vlan_id): + """Checks if a vlanid is in use""" + LOG.debug("is_vlanid_used() called") + session = db.get_session() + try: + vlanid = session.query(l2network_models.VlanID).\ + filter_by(vlan_id=vlan_id).\ + one() + return vlanid["vlan_used"] + except exc.NoResultFound: + raise c_exc.VlanIDNotFound(vlan_id=vlan_id) + + +def release_vlanid(vlan_id): + """Sets the vlanid state to be unused""" + LOG.debug("release_vlanid() called") + session = db.get_session() + try: + vlanid = session.query(l2network_models.VlanID).\ + filter_by(vlan_id=vlan_id).\ + one() + vlanid["vlan_used"] = False + session.merge(vlanid) + session.flush() + return vlanid["vlan_used"] + except exc.NoResultFound: + raise c_exc.VlanIDNotFound(vlan_id=vlan_id) + return + + +def delete_vlanid(vlan_id): + """Deletes a vlanid entry from db""" + LOG.debug("delete_vlanid() called") + session = db.get_session() + try: + vlanid = session.query(l2network_models.VlanID).\ + filter_by(vlan_id=vlan_id).\ + one() + session.delete(vlanid) + session.flush() + return vlanid + except exc.NoResultFound: + raise c_exc.VlanIDNotFound(vlan_id=vlan_id) + + +def reserve_vlanid(): + """Reserves the first unused vlanid""" + LOG.debug("reserve_vlanid() called") + session = db.get_session() + try: + rvlan = session.query(l2network_models.VlanID).\ + first() + if not rvlan: + create_vlanids() + + rvlan = session.query(l2network_models.VlanID).\ + filter_by(vlan_used=False).\ + first() + if not rvlan: + raise c_exc.VlanIDNotAvailable() + + rvlanid = session.query(l2network_models.VlanID).\ + filter_by(vlan_id=rvlan["vlan_id"]).\ + one() + rvlanid["vlan_used"] = True + session.merge(rvlanid) + session.flush() + return rvlan["vlan_id"] + except exc.NoResultFound: + raise c_exc.VlanIDNotAvailable() + + +def get_all_vlanids_used(): + """Gets all the vlanids used""" + LOG.debug("get_all_vlanids() called") + session = db.get_session() + try: + vlanids = session.query(l2network_models.VlanID).\ + filter_by(vlan_used=True).\ + all() + return vlanids + except exc.NoResultFound: + return [] + + +def get_all_vlan_bindings(): + """Lists all the vlan to network associations""" + LOG.debug("get_all_vlan_bindings() called") + session = db.get_session() + try: + bindings = session.query(l2network_models.VlanBinding).\ + all() + return bindings + except exc.NoResultFound: + return [] + + +def get_vlan_binding(netid): + """Lists the vlan given a network_id""" + LOG.debug("get_vlan_binding() called") + session = db.get_session() + try: + binding = session.query(l2network_models.VlanBinding).\ + filter_by(network_id=netid).\ + one() + return binding + except exc.NoResultFound: + raise c_exc.NetworkVlanBindingNotFound(network_id=netid) + + +def add_vlan_binding(vlanid, netid): + """Adds a vlan to network association""" + LOG.debug("add_vlan_binding() called") + session = db.get_session() + try: + binding = session.query(l2network_models.VlanBinding).\ + filter_by(vlan_id=vlanid).\ + one() + raise c_exc.NetworkVlanBindingAlreadyExists(vlan_id=vlanid, + network_id=netid) + except exc.NoResultFound: + binding = l2network_models.VlanBinding(vlanid, netid) + session.add(binding) + session.flush() + return binding + + +def remove_vlan_binding(netid): + """Removes a vlan to network association""" + LOG.debug("remove_vlan_binding() called") + session = db.get_session() + try: + binding = session.query(l2network_models.VlanBinding).\ + filter_by(network_id=netid).\ + one() + session.delete(binding) + session.flush() + return binding + except exc.NoResultFound: + pass + + +def update_vlan_binding(netid, newvlanid=None): + """Updates a vlan to network association""" + LOG.debug("update_vlan_binding() called") + session = db.get_session() + try: + binding = session.query(l2network_models.VlanBinding).\ + filter_by(network_id=netid).\ + one() + if newvlanid: + binding["vlan_id"] = newvlanid + session.merge(binding) + session.flush() + return binding + except exc.NoResultFound: + raise q_exc.NetworkNotFound(net_id=netid) diff --git a/quantum/plugins/linuxbridge/db/l2network_models.py b/quantum/plugins/linuxbridge/db/l2network_models.py new file mode 100644 index 0000000000..b5613b3223 --- /dev/null +++ b/quantum/plugins/linuxbridge/db/l2network_models.py @@ -0,0 +1,57 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Cisco Systems, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# @author: Rohit Agarwalla, Cisco Systems, Inc. + +import uuid + +from sqlalchemy import Column, Integer, String, Boolean +from sqlalchemy.orm import relation, object_mapper + +from quantum.db.models import BASE +from quantum.db.models import QuantumBase +from quantum.db import models + + +class VlanID(BASE, QuantumBase): + """Represents a vlan_id usage""" + __tablename__ = 'vlan_ids' + + vlan_id = Column(Integer, primary_key=True) + vlan_used = Column(Boolean) + + def __init__(self, vlan_id): + self.vlan_id = vlan_id + self.vlan_used = False + + def __repr__(self): + return "" % \ + (self.vlan_id, self.vlan_used) + + +class VlanBinding(BASE, QuantumBase): + """Represents a binding of vlan_id to network_id""" + __tablename__ = 'vlan_bindings' + + vlan_id = Column(Integer, primary_key=True) + network_id = Column(String(255), nullable=False) + + def __init__(self, vlan_id, network_id): + self.vlan_id = vlan_id + self.network_id = network_id + + def __repr__(self): + return "" % \ + (self.vlan_id, self.network_id) diff --git a/quantum/plugins/linuxbridge/nova/linux_net_linux_bridge.py b/quantum/plugins/linuxbridge/nova/linux_net_linux_bridge.py new file mode 100755 index 0000000000..1c8e6ae980 --- /dev/null +++ b/quantum/plugins/linuxbridge/nova/linux_net_linux_bridge.py @@ -0,0 +1,117 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. +# All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# Extends the linux_net.py kvm/linux network driver in Nova, +# borrows structure and code +# @author: Sumit Naiksatam, Cisco Systems, Inc. +# + + +"""Extends the linux_net driver when using the Linux Bridge plugin with +QuantumManager""" + + +from nova import exception +from nova import log as logging +from nova import utils + +from nova.network.linux_net import * + + +LOG = logging.getLogger(__name__) + + +BRDIGE_NAME_PREFIX = "brq" +GATEWAY_INTERFACE_PREFIX = "gw-" + + +def _device_exists(device): + """Check if ethernet device exists.""" + (_out, err) = utils.execute('ip', 'link', 'show', 'dev', device, + check_exit_code=False) + return not err + + +# plugs interfaces using Linux Bridge when using QuantumManager +class QuantumLibvirtLinuxBridgeDriver(LinuxNetInterfaceDriver): + + def plug(self, network, mac_address, gateway=True): + LOG.debug(_("inside plug()")) + dev = self.get_dev(network) + bridge = self.get_bridge(network) + if not gateway: + # If we weren't instructed to act as a gateway then add the + # appropriate flows to block all non-dhcp traffic. + # .. and make sure iptbles won't forward it as well. + iptables_manager.ipv4['filter'].add_rule('FORWARD', + '--in-interface %s -j DROP' % bridge) + iptables_manager.ipv4['filter'].add_rule('FORWARD', + '--out-interface %s -j DROP' % bridge) + return bridge + else: + iptables_manager.ipv4['filter'].add_rule('FORWARD', + '--in-interface %s -j ACCEPT' % bridge) + iptables_manager.ipv4['filter'].add_rule('FORWARD', + '--out-interface %s -j ACCEPT' % bridge) + + if not _device_exists(dev): + try: + # First, try with 'ip' + utils.execute('ip', 'tuntap', 'add', dev, 'mode', 'tap', + run_as_root=True) + except exception.ProcessExecutionError: + # Second option: tunctl + utils.execute('tunctl', '-b', '-t', dev, run_as_root=True) + utils.execute('ip', 'link', 'set', dev, "address", mac_address, + run_as_root=True) + utils.execute('ip', 'link', 'set', dev, 'up', run_as_root=True) + + if not _device_exists(bridge): + LOG.debug(_("Starting bridge %s "), bridge) + utils.execute('brctl', 'addbr', bridge, run_as_root=True) + utils.execute('brctl', 'setfd', bridge, str(0), run_as_root=True) + utils.execute('brctl', 'stp', bridge, 'off', run_as_root=True) + utils.execute('ip', 'link', 'set', bridge, "address", mac_address, + run_as_root=True) + utils.execute('ip', 'link', 'set', bridge, 'up', run_as_root=True) + LOG.debug(_("Done starting bridge %s"), bridge) + + full_ip = '%s/%s' % (network['dhcp_server'], + network['cidr'].rpartition('/')[2]) + utils.execute('ip', 'address', 'add', full_ip, 'dev', bridge, + run_as_root=True) + + return dev + + def unplug(self, network): + LOG.debug(_("inside unplug()")) + dev = self.get_dev(network) + try: + utils.execute('ip', 'link', 'delete', dev, run_as_root=True) + except exception.ProcessExecutionError: + LOG.warning(_("Failed while unplugging gateway interface '%s'"), + dev) + raise + LOG.debug(_("Unplugged gateway interface '%s'"), dev) + return dev + + def get_dev(self, network): + dev = GATEWAY_INTERFACE_PREFIX + str(network['uuid'][0:11]) + return dev + + def get_bridge(self, network): + bridge = BRDIGE_NAME_PREFIX + str(network['uuid'][0:11]) + return bridge diff --git a/quantum/plugins/linuxbridge/nova/vif_linuxbridge_quantum.py b/quantum/plugins/linuxbridge/nova/vif_linuxbridge_quantum.py new file mode 100644 index 0000000000..1e2a40fb54 --- /dev/null +++ b/quantum/plugins/linuxbridge/nova/vif_linuxbridge_quantum.py @@ -0,0 +1,73 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (C) 2012 Midokura KK +# Copyright (C) 2012 Nicira, Inc +# Copyright (C) 2012 Cisco Systems, Inc +# Copyright 2012 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +""" +VIF driver for libvirt when QuantumManager is configured with Linux Bridge +plugin +""" + +from nova import flags +from nova import log as logging +from nova.network import linux_net +from nova.virt import netutils +from nova import utils +from nova.virt.vif import VIFDriver +from nova import exception + +LOG = logging.getLogger('nova.virt.libvirt.vif_linuxbridge_quantum') + +FLAGS = flags.FLAGS + + +class QuantumLibvirtLinuxBridgeDriver(VIFDriver): + """VIF driver for Linux Bridge.""" + + def get_dev_name(_self, iface_id): + return "tap" + iface_id[0:11] + + def plug(self, instance, network, mapping): + iface_id = mapping['vif_uuid'] + dev = self.get_dev_name(iface_id) + if not linux_net._device_exists(dev): + try: + # First, try with 'ip' + utils.execute('ip', 'tuntap', 'add', dev, 'mode', 'tap', + run_as_root=True) + except exception.ProcessExecutionError: + # Second option: tunctl + utils.execute('tunctl', '-b', '-t', dev, run_as_root=True) + utils.execute('ip', 'link', 'set', dev, 'up', run_as_root=True) + + result = { + 'script': '', + 'name': dev, + 'mac_address': mapping['mac']} + return result + + def unplug(self, instance, network, mapping): + """Unplug the VIF from the network by deleting the port from + the bridge.""" + dev = self.get_dev_name(mapping['vif_uuid']) + try: + utils.execute('ip', 'link', 'delete', dev, run_as_root=True) + except exception.ProcessExecutionError: + LOG.warning(_("Failed while unplugging vif of instance '%s'"), + instance['name']) + raise diff --git a/quantum/plugins/linuxbridge/plugin_configuration.py b/quantum/plugins/linuxbridge/plugin_configuration.py new file mode 100644 index 0000000000..cc6cdd66d6 --- /dev/null +++ b/quantum/plugins/linuxbridge/plugin_configuration.py @@ -0,0 +1,48 @@ +""" +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Sumit Naiksatam, Cisco Systems, Inc. +# @author: Rohit Agarwalla, Cisco Systems, Inc. +""" + +import os + +from quantum.common.config import find_config_file +from quantum.plugins.linuxbridge.common import configparser as confp + + +CONF_FILE = find_config_file({'plugin': 'linuxbridge'}, None, + "linuxbridge_conf.ini") +CONF_PARSER_OBJ = confp.ConfigParser(CONF_FILE) + + +""" +Reading the conf for the linuxbridge_plugin +""" +SECTION_CONF = CONF_PARSER_OBJ['VLANS'] +VLAN_START = SECTION_CONF['vlan_start'] +VLAN_END = SECTION_CONF['vlan_end'] + + +SECTION_CONF = CONF_PARSER_OBJ['DATABASE'] +DB_CONNECTION = SECTION_CONF['connection'] +if DB_CONNECTION != 'sqlite': + DB_NAME = SECTION_CONF['name'] + DB_USER = SECTION_CONF['user'] + DB_PASS = SECTION_CONF['pass'] + DB_HOST = SECTION_CONF['host'] + DB_PORT = SECTION_CONF['port'] diff --git a/quantum/plugins/linuxbridge/run_tests.py b/quantum/plugins/linuxbridge/run_tests.py new file mode 100644 index 0000000000..805922670a --- /dev/null +++ b/quantum/plugins/linuxbridge/run_tests.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 OpenStack, LLC +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Unittest runner for quantum Linux Bridge plugin + +This file should be run from the top dir in the quantum directory + +To run all tests:: + PLUGIN_DIR=quantum/plugins/linuxbridge ./run_tests.sh +""" + +import gettext +import logging +import os +import unittest +import sys + +from nose import config +from nose import core + +sys.path.append(os.getcwd()) +sys.path.append(os.path.dirname(__file__)) + + +from quantum.api.api_common import OperationalStatus +from quantum.common.test_lib import run_tests, test_config +from quantum.plugins.linuxbridge.LinuxBridgePlugin import LinuxBridgePlugin +import quantum.tests.unit +from tests.unit.test_database import L2networkDBTest + + +if __name__ == '__main__': + exit_status = False + + # if a single test case was specified, + # we should only invoked the tests once + invoke_once = len(sys.argv) > 1 + + test_config['plugin_name'] = "LinuxBridgePlugin.LinuxBridgePlugin" + test_config['default_net_op_status'] = OperationalStatus.UP + test_config['default_port_op_status'] = OperationalStatus.DOWN + + cwd = os.getcwd() + c = config.Config(stream=sys.stdout, + env=os.environ, + verbosity=3, + includeExe=True, + traverseNamespace=True, + plugins=core.DefaultPluginManager()) + c.configureWhere(quantum.tests.unit.__path__) + exit_status = run_tests(c) + + if invoke_once: + sys.exit(0) + + os.chdir(cwd) + + working_dir = os.path.abspath("quantum/plugins/linuxbridge") + c = config.Config(stream=sys.stdout, + env=os.environ, + verbosity=3, + workingDir=working_dir) + exit_status = exit_status or run_tests(c) + + sys.exit(exit_status) diff --git a/quantum/plugins/linuxbridge/tests/__init__.py b/quantum/plugins/linuxbridge/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/quantum/plugins/linuxbridge/tests/unit/__init__.py b/quantum/plugins/linuxbridge/tests/unit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/quantum/plugins/linuxbridge/tests/unit/_test_linuxbridgeAgent.py b/quantum/plugins/linuxbridge/tests/unit/_test_linuxbridgeAgent.py new file mode 100644 index 0000000000..9452189407 --- /dev/null +++ b/quantum/plugins/linuxbridge/tests/unit/_test_linuxbridgeAgent.py @@ -0,0 +1,451 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012 Cisco Systems, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +# @author: Shweta Padubidri, Cisco Systems, Inc. +# + +import ConfigParser +import logging as LOG +import unittest +import sys +import os +import signal +from subprocess import * + +import quantum.plugins.linuxbridge.agent.linuxbridge_quantum_agent\ + as linux_agent +from quantum.plugins.linuxbridge.common import constants as lconst +from quantum.plugins.linuxbridge import LinuxBridgePlugin +from quantum.plugins.linuxbridge.db import l2network_db as cdb +import quantum.db.api as db + + +LOG.getLogger(__name__) + + +class LinuxBridgeAgentTest(unittest.TestCase): + + def test_add_gateway_interface( + self, tenant_id="test_tenant", network_name="test_network", + interface_id='fe701ddf-26a2-42ea-b9e6-7313d1c522cc', + mac_address='fe:16:3e:51:60:dd'): + + LOG.debug("test_tap_gateway_interface - START") + new_network =\ + self._linuxbridge_plugin.create_network(tenant_id, network_name) + new_port = self._linuxbridge_plugin.create_port( + tenant_id, new_network[lconst.NET_ID], lconst.PORT_UP) + self._linuxbridge_plugin.plug_interface( + tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID], interface_id) + bridge_name = self.br_name_prefix + new_network[lconst.NET_ID][0:11] + self.create_bridge(bridge_name) + device_name = self.gw_name_prefix + new_network[lconst.NET_ID][0:11] + self.create_device(device_name, mac_address) + + vlan_bind = cdb.get_vlan_binding(new_network[lconst.NET_ID]) + vlan_id = vlan_bind[lconst.VLANID] + + self._linuxbridge_quantum_agent.process_port_binding( + new_port[lconst.PORT_ID], new_network[lconst.NET_ID], + device_name, str(vlan_id)) + list_interface = self._linuxbridge_quantum_agent.linux_br.\ + get_interfaces_on_bridge(bridge_name) + + self.assertTrue(device_name in list_interface) + for interface in list_interface: + self._linuxbridge_quantum_agent.linux_br.remove_interface( + bridge_name, interface) + self.delete_device(interface) + self.delete_bridge(bridge_name) + self.tearDownUnplugInterface(tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + + LOG.debug("test_add_gateway_interface - END") + + def test_add_tap_interface( + self, tenant_id="test_tenant", network_name="test_network", + interface_id='fe701ddf-26a2-42ea-b9e6-7313d1c522cc', + mac_address='fe:16:3e:51:60:dd'): + + LOG.debug("test_add_tap_interface - START") + new_network =\ + self._linuxbridge_plugin.create_network(tenant_id, network_name) + new_port = self._linuxbridge_plugin.create_port( + tenant_id, new_network[lconst.NET_ID], lconst.PORT_UP) + self._linuxbridge_plugin.plug_interface( + tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID], interface_id) + bridge_name = self.br_name_prefix + new_network[lconst.NET_ID][0:11] + self.create_bridge(bridge_name) + device_name = self.tap_name_prefix + interface_id[0:11] + self.create_device(device_name, mac_address) + + vlan_bind = cdb.get_vlan_binding(new_network[lconst.NET_ID]) + vlan_id = vlan_bind[lconst.VLANID] + + self._linuxbridge_quantum_agent.process_port_binding( + new_port[lconst.PORT_ID], new_network[lconst.NET_ID], + interface_id, str(vlan_id)) + list_interface = self._linuxbridge_quantum_agent.linux_br.\ + get_interfaces_on_bridge(bridge_name) + + self.assertTrue(device_name in list_interface) + for interface in list_interface: + self._linuxbridge_quantum_agent.linux_br.remove_interface( + bridge_name, interface) + self.delete_device(interface) + self.delete_bridge(bridge_name) + self.tearDownUnplugInterface(tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + + LOG.debug("test_add_tap_interface -END") + + def test_remove_interface( + self, tenant_id="test_tenant", network_name="test_network", + interface_id='fe701ddf-26a2-42ea-b9e6-7313d1c522cc', + mac_address='fe:16:3e:51:60:dd'): + + LOG.debug("test_remove_interface - START") + new_network =\ + self._linuxbridge_plugin.create_network(tenant_id, network_name) + new_port = self._linuxbridge_plugin.create_port( + tenant_id, new_network[lconst.NET_ID], lconst.PORT_UP) + self._linuxbridge_plugin.plug_interface( + tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID], interface_id) + bridge_name = self.br_name_prefix + new_network[lconst.NET_ID][0:11] + self.create_bridge(bridge_name) + device_name = self.tap_name_prefix + interface_id[0:11] + self.create_device(device_name, mac_address) + + vlan_bind = cdb.get_vlan_binding(new_network[lconst.NET_ID]) + vlan_id = vlan_bind[lconst.VLANID] + + self._linuxbridge_quantum_agent.process_port_binding( + new_port[lconst.PORT_ID], new_network[lconst.NET_ID], + interface_id, str(vlan_id)) + list_interface = self._linuxbridge_quantum_agent.linux_br.\ + get_interfaces_on_bridge(bridge_name) + + self._linuxbridge_quantum_agent.linux_br.remove_interface(bridge_name, + device_name) + list_interface = self._linuxbridge_quantum_agent.linux_br.\ + get_interfaces_on_bridge(bridge_name) + self.assertFalse(device_name in list_interface) + for interface in list_interface: + self._linuxbridge_quantum_agent.linux_br.remove_interface( + bridge_name, interface) + self.delete_device(interface) + self.delete_device(device_name) + self.delete_bridge(bridge_name) + self.tearDownUnplugInterface(tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + + LOG.debug("test_remove_interface -END") + + def test_ensure_vlan_bridge( + self, tenant_id="test_tenant", network_name="test_network", + interface_id='fe701ddf-26a2-42ea-b9e6-7313d1c522cc'): + + LOG.debug("test_ensure_vlan_bridge - START") + new_network =\ + self._linuxbridge_plugin.create_network(tenant_id, network_name) + new_port = self._linuxbridge_plugin.create_port( + tenant_id, new_network[lconst.NET_ID], lconst.PORT_UP) + self._linuxbridge_plugin.plug_interface( + tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID], interface_id) + bridge_name = self.br_name_prefix + new_network[lconst.NET_ID][0:11] + vlan_bind = cdb.get_vlan_binding(new_network[lconst.NET_ID]) + vlan_id = vlan_bind[lconst.VLANID] + vlan_subinterface = self.physical_interface + '.' + str(vlan_id) + + self._linuxbridge_quantum_agent.linux_br.ensure_vlan_bridge( + new_network[lconst.NET_ID], str(vlan_id)) + list_quantum_bridges = self._linuxbridge_quantum_agent.linux_br.\ + get_all_quantum_bridges() + self.assertTrue(bridge_name in list_quantum_bridges) + list_interface = self._linuxbridge_quantum_agent.linux_br.\ + get_interfaces_on_bridge(bridge_name) + self.assertTrue(vlan_subinterface in list_interface) + + for interface in list_interface: + self._linuxbridge_quantum_agent.linux_br.remove_interface( + bridge_name, interface) + self.delete_device(interface) + self.delete_bridge(bridge_name) + self.tearDownUnplugInterface(tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + + LOG.debug("test_ensure_vlan_bridge -END") + + def test_delete_vlan_bridge( + self, tenant_id="test_tenant", network_name="test_network", + interface_id='fe701ddf-26a2-42ea-b9e6-7313d1c522cc'): + + LOG.debug("test_delete_vlan_bridge - START") + new_network =\ + self._linuxbridge_plugin.create_network(tenant_id, network_name) + new_port = self._linuxbridge_plugin.create_port( + tenant_id, new_network[lconst.NET_ID], lconst.PORT_UP) + self._linuxbridge_plugin.plug_interface( + tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID], interface_id) + bridge_name = self.br_name_prefix + new_network[lconst.NET_ID][0:11] + vlan_bind = cdb.get_vlan_binding(new_network[lconst.NET_ID]) + vlan_id = vlan_bind[lconst.VLANID] + vlan_subinterface = self.physical_interface + '.' + str(vlan_id) + + self._linuxbridge_quantum_agent.linux_br.ensure_vlan_bridge( + new_network[lconst.NET_ID], str(vlan_id)) + self._linuxbridge_quantum_agent.linux_br.delete_vlan_bridge( + bridge_name) + + self.assertEquals(self.device_exists(vlan_subinterface), False) + self.assertEquals(self.device_exists(bridge_name), False) + self.tearDownUnplugInterface(tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + + LOG.debug("test_delete_vlan_bridge - END") + + def test_process_deleted_networks( + self, tenant_id="test_tenant", network_name="test_network", + interface_id='fe701ddf-26a2-42ea-b9e6-7313d1c522cc'): + + LOG.debug("test_delete_vlan_bridge - START") + new_network =\ + self._linuxbridge_plugin.create_network(tenant_id, network_name) + new_port = self._linuxbridge_plugin.create_port( + tenant_id, new_network[lconst.NET_ID], lconst.PORT_UP) + self._linuxbridge_plugin.plug_interface( + tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID], interface_id) + bridge_name = self.br_name_prefix + new_network[lconst.NET_ID][0:11] + vlan_bindings = {} + vlan_bindings[new_network[lconst.NET_ID]] =\ + cdb.get_vlan_binding(new_network[lconst.NET_ID]) + vlan_id = vlan_bindings[new_network[lconst.NET_ID]][lconst.VLANID] + vlan_subinterface = self.physical_interface + '.' + str(vlan_id) + + self._linuxbridge_quantum_agent.linux_br.ensure_vlan_bridge( + new_network[lconst.NET_ID], str(vlan_id)) + self.tearDownUnplugInterface(tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + vlan_bindings = {} + self._linuxbridge_quantum_agent.process_deleted_networks(vlan_bindings) + + self.assertEquals(self.device_exists(vlan_subinterface), False) + self.assertEquals(self.device_exists(bridge_name), False) + LOG.debug("test_delete_vlan_bridge - END") + + def test_process_unplugged_tap_interface( + self, tenant_id="test_tenant", network_name="test_network", + interface_id='fe701ddf-26a2-42ea-b9e6-7313d1c522cc', + mac_address='fe:16:3e:51:60:dd'): + + LOG.debug("test_process_unplugged_tap_interface - START") + new_network =\ + self._linuxbridge_plugin.create_network(tenant_id, network_name) + new_port = self._linuxbridge_plugin.create_port( + tenant_id, new_network[lconst.NET_ID], lconst.PORT_UP) + self._linuxbridge_plugin.plug_interface( + tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID], interface_id) + bridge_name = self.br_name_prefix + new_network[lconst.NET_ID][0:11] + self.create_bridge(bridge_name) + device_name = self.tap_name_prefix + interface_id[0:11] + self.create_device(device_name, mac_address) + + vlan_bind = cdb.get_vlan_binding(new_network[lconst.NET_ID]) + vlan_id = vlan_bind[lconst.VLANID] + + self._linuxbridge_quantum_agent.process_port_binding( + new_port[lconst.PORT_ID], new_network[lconst.NET_ID], + interface_id, str(vlan_id)) + list_interface = self._linuxbridge_quantum_agent.linux_br.\ + get_interfaces_on_bridge(bridge_name) + self._linuxbridge_plugin.unplug_interface(tenant_id, + new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + plugged_interface = [] + self._linuxbridge_quantum_agent.process_unplugged_interfaces( + plugged_interface) + list_interface = self._linuxbridge_quantum_agent.linux_br.\ + get_interfaces_on_bridge(bridge_name) + self.assertFalse(device_name in list_interface) + for interface in list_interface: + self._linuxbridge_quantum_agent.linux_br.remove_interface( + bridge_name, interface) + self.delete_device(interface) + self.delete_device(device_name) + self.delete_bridge(bridge_name) + self.tearDownNetworkPort(tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + + LOG.debug("test_test_process_unplugged_tap_interface -END") + + def test_process_unplugged_gw_interface( + self, tenant_id="test_tenant", network_name="test_network", + interface_id='fe701ddf-26a2-42ea-b9e6-7313d1c522cc', + mac_address='fe:16:3e:51:60:dd'): + + LOG.debug("test_process_unplugged_gw_interface - START") + new_network =\ + self._linuxbridge_plugin.create_network(tenant_id, network_name) + new_port = self._linuxbridge_plugin.create_port( + tenant_id, new_network[lconst.NET_ID], lconst.PORT_UP) + self._linuxbridge_plugin.plug_interface( + tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID], interface_id) + bridge_name = self.br_name_prefix + new_network[lconst.NET_ID][0:11] + self.create_bridge(bridge_name) + device_name = self.gw_name_prefix + new_network[lconst.NET_ID][0:11] + self.create_device(device_name, mac_address) + + vlan_bind = cdb.get_vlan_binding(new_network[lconst.NET_ID]) + vlan_id = vlan_bind[lconst.VLANID] + + self._linuxbridge_quantum_agent.process_port_binding( + new_port[lconst.PORT_ID], new_network[lconst.NET_ID], + interface_id, str(vlan_id)) + list_interface = self._linuxbridge_quantum_agent.linux_br.\ + get_interfaces_on_bridge(bridge_name) + self._linuxbridge_plugin.unplug_interface(tenant_id, + new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + plugged_interface = [] + self._linuxbridge_quantum_agent.process_unplugged_interfaces( + plugged_interface) + list_interface = self._linuxbridge_quantum_agent.linux_br.\ + get_interfaces_on_bridge(bridge_name) + self.assertFalse(device_name in list_interface) + for interface in list_interface: + self._linuxbridge_quantum_agent.linux_br.remove_interface( + bridge_name, interface) + self.delete_device(interface) + self.delete_device(device_name) + self.delete_bridge(bridge_name) + self.tearDownNetworkPort(tenant_id, new_network[lconst.NET_ID], + new_port[lconst.PORT_ID]) + + LOG.debug("test_test_process_unplugged_gw_interface -END") + + def create_bridge(self, bridge_name): + """ + Create a bridge + """ + self.run_cmd(['brctl', 'addbr', bridge_name]) + self.run_cmd(['brctl', 'setfd', bridge_name, str(0)]) + self.run_cmd(['brctl', 'stp', bridge_name, 'off']) + self.run_cmd(['ip', 'link', 'set', bridge_name, 'up']) + + def delete_bridge(self, bridge_name): + """ + Delete a bridge + """ + self.run_cmd(['ip', 'link', 'set', bridge_name, 'down']) + self.run_cmd(['brctl', 'delbr', bridge_name]) + + def create_device(self, dev, mac_address): + self.run_cmd(['ip', 'tuntap', 'add', dev, 'mode', 'tap']) + self.run_cmd(['ip', 'link', 'set', dev, "address", mac_address]) + self.run_cmd(['ip', 'link', 'set', dev, 'up']) + + def delete_device(self, dev): + self.run_cmd(['ip', 'link', 'delete', dev]) + + def setUp(self): + """ + Set up function + """ + self.tenant_id = "test_tenant" + self.network_name = "test_network" + self.config_file = os.path.join(os.path.dirname(__file__), os.pardir, + os.pardir, os.pardir, os.pardir, + os.pardir, "etc", "quantum", + "plugins", "linuxbridge", + "linuxbridge_conf.ini") + + config = ConfigParser.ConfigParser() + self.br_name_prefix = "brq" + self.gw_name_prefix = "gw-" + self.tap_name_prefix = "tap" + self._linuxbridge_plugin = LinuxBridgePlugin.LinuxBridgePlugin() + try: + fh = open(self.config_file) + fh.close() + config.read(self.config_file) + self.physical_interface = config.get("LINUX_BRIDGE", + "physical_interface") + self.polling_interval = config.get("AGENT", "polling_interval") + except Exception, e: + LOG.error("Unable to parse config file \"%s\": \nException%s" + % (self.config_file, str(e))) + sys.exit(1) + self._linuxbridge = linux_agent.LinuxBridge(self.br_name_prefix, + self.physical_interface) + self._linuxbridge_quantum_agent = linux_agent.LinuxBridgeQuantumAgent( + self.br_name_prefix, + self.physical_interface, + self.polling_interval) + + def run_cmd(self, args): + LOG.debug("Running command: " + " ".join(args)) + p = Popen(args, stdout=PIPE) + retval = p.communicate()[0] + if p.returncode == -(signal.SIGALRM): + LOG.debug("Timeout running command: " + " ".join(args)) + if retval: + LOG.debug("Command returned: %s" % retval) + return retval + + def device_exists(self, device): + """Check if ethernet device exists.""" + retval = self.run_cmd(['ip', 'link', 'show', 'dev', device]) + if retval: + return True + else: + return False + + """ + Clean up functions after the tests + """ + def tearDown(self): + """Clear the test environment(Clean the Database)""" + db.clear_db() + + def tearDownNetwork(self, tenant_id, network_dict_id): + """ + Tear down the Network + """ + self._linuxbridge_plugin.delete_network(tenant_id, network_dict_id) + + def tearDownUnplugInterface(self, tenant_id, network_dict_id, port_id): + """ + Tear down the port + """ + self._linuxbridge_plugin.unplug_interface(tenant_id, network_dict_id, + port_id) + self.tearDownNetworkPort(tenant_id, network_dict_id, port_id) + + def tearDownNetworkPort(self, tenant_id, network_dict_id, port_id): + """ + Tear down Network Port + """ + self._linuxbridge_plugin.delete_port(tenant_id, network_dict_id, + port_id) + self.tearDownNetwork(tenant_id, network_dict_id) diff --git a/quantum/plugins/linuxbridge/tests/unit/test_database.py b/quantum/plugins/linuxbridge/tests/unit/test_database.py new file mode 100644 index 0000000000..3ce70af94e --- /dev/null +++ b/quantum/plugins/linuxbridge/tests/unit/test_database.py @@ -0,0 +1,362 @@ +""" +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2012, Cisco Systems, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# @author: Rohit Agarwalla, Cisco Systems, Inc. +""" + +""" +test_database.py is an independent test suite +that tests the database api method calls +""" +import logging as LOG +import unittest + +from common import constants as const + +import quantum.db.api as db +import db.l2network_db as l2network_db + + +LOG.getLogger(__name__) + + +class L2networkDB(object): + + """Class conisting of methods to call L2network db methods""" + def get_all_vlan_bindings(self): + """Get all vlan binding into a list of dict""" + vlans = [] + try: + for vlan_bind in l2network_db.get_all_vlan_bindings(): + LOG.debug("Getting vlan bindings for vlan: %s" % + vlan_bind.vlan_id) + vlan_dict = {} + vlan_dict["vlan-id"] = str(vlan_bind.vlan_id) + vlan_dict["net-id"] = str(vlan_bind.network_id) + vlans.append(vlan_dict) + except Exception, exc: + LOG.error("Failed to get all vlan bindings: %s" % str(exc)) + return vlans + + def get_vlan_binding(self, network_id): + """Get a vlan binding""" + vlan = [] + try: + for vlan_bind in l2network_db.get_vlan_binding(network_id): + LOG.debug("Getting vlan binding for vlan: %s" + % vlan_bind.vlan_id) + vlan_dict = {} + vlan_dict["vlan-id"] = str(vlan_bind.vlan_id) + vlan_dict["net-id"] = str(vlan_bind.network_id) + vlan.append(vlan_dict) + except Exception, exc: + LOG.error("Failed to get vlan binding: %s" % str(exc)) + return vlan + + def create_vlan_binding(self, vlan_id, network_id): + """Create a vlan binding""" + vlan_dict = {} + try: + res = l2network_db.add_vlan_binding(vlan_id, network_id) + LOG.debug("Created vlan binding for vlan: %s" % res.vlan_id) + vlan_dict["vlan-id"] = str(res.vlan_id) + vlan_dict["net-id"] = str(res.network_id) + return vlan_dict + except Exception, exc: + LOG.error("Failed to create vlan binding: %s" % str(exc)) + + def delete_vlan_binding(self, network_id): + """Delete a vlan binding""" + try: + res = l2network_db.remove_vlan_binding(network_id) + LOG.debug("Deleted vlan binding for vlan: %s" % res.vlan_id) + vlan_dict = {} + vlan_dict["vlan-id"] = str(res.vlan_id) + return vlan_dict + except Exception, exc: + raise Exception("Failed to delete vlan binding: %s" % str(exc)) + + def update_vlan_binding(self, network_id, vlan_id): + """Update a vlan binding""" + try: + res = l2network_db.update_vlan_binding(network_id, vlan_id) + LOG.debug("Updating vlan binding for vlan: %s" % res.vlan_id) + vlan_dict = {} + vlan_dict["vlan-id"] = str(res.vlan_id) + vlan_dict["net-id"] = str(res.network_id) + return vlan_dict + except Exception, exc: + raise Exception("Failed to update vlan binding: %s" % str(exc)) + + +class QuantumDB(object): + """Class conisting of methods to call Quantum db methods""" + def get_all_networks(self, tenant_id): + """Get all networks""" + nets = [] + try: + for net in db.network_list(tenant_id): + LOG.debug("Getting network: %s" % net.uuid) + net_dict = {} + net_dict["tenant-id"] = net.tenant_id + net_dict["net-id"] = str(net.uuid) + net_dict["net-name"] = net.name + nets.append(net_dict) + except Exception, exc: + LOG.error("Failed to get all networks: %s" % str(exc)) + return nets + + def get_network(self, network_id): + """Get a network""" + net = [] + try: + for net in db.network_get(network_id): + LOG.debug("Getting network: %s" % net.uuid) + net_dict = {} + net_dict["tenant-id"] = net.tenant_id + net_dict["net-id"] = str(net.uuid) + net_dict["net-name"] = net.name + net.append(net_dict) + except Exception, exc: + LOG.error("Failed to get network: %s" % str(exc)) + return net + + def create_network(self, tenant_id, net_name): + """Create a network""" + net_dict = {} + try: + res = db.network_create(tenant_id, + net_name, + op_status="UP") + LOG.debug("Created network: %s" % res.uuid) + net_dict["tenant-id"] = res.tenant_id + net_dict["net-id"] = str(res.uuid) + net_dict["net-name"] = res.name + return net_dict + except Exception, exc: + LOG.error("Failed to create network: %s" % str(exc)) + + def delete_network(self, net_id): + """Delete a network""" + try: + net = db.network_destroy(net_id) + LOG.debug("Deleted network: %s" % net.uuid) + net_dict = {} + net_dict["net-id"] = str(net.uuid) + return net_dict + except Exception, exc: + raise Exception("Failed to delete port: %s" % str(exc)) + + def update_network(self, tenant_id, net_id, **kwargs): + """Update a network""" + try: + net = db.network_update(net_id, tenant_id, **kwargs) + LOG.debug("Updated network: %s" % net.uuid) + net_dict = {} + net_dict["net-id"] = str(net.uuid) + net_dict["net-name"] = net.name + return net_dict + except Exception, exc: + raise Exception("Failed to update network: %s" % str(exc)) + + def get_all_ports(self, net_id): + """Get all ports""" + ports = [] + try: + for port in db.port_list(net_id): + LOG.debug("Getting port: %s" % port.uuid) + port_dict = {} + port_dict["port-id"] = str(port.uuid) + port_dict["net-id"] = str(port.network_id) + port_dict["int-id"] = port.interface_id + port_dict["state"] = port.state + port_dict["net"] = port.network + ports.append(port_dict) + return ports + except Exception, exc: + LOG.error("Failed to get all ports: %s" % str(exc)) + + def get_port(self, net_id, port_id): + """Get a port""" + port_list = [] + port = db.port_get(net_id, port_id) + try: + LOG.debug("Getting port: %s" % port.uuid) + port_dict = {} + port_dict["port-id"] = str(port.uuid) + port_dict["net-id"] = str(port.network_id) + port_dict["int-id"] = port.interface_id + port_dict["state"] = port.state + port_list.append(port_dict) + return port_list + except Exception, exc: + LOG.error("Failed to get port: %s" % str(exc)) + + def create_port(self, net_id): + """Add a port""" + port_dict = {} + try: + port = db.port_create(net_id) + LOG.debug("Creating port %s" % port.uuid) + port_dict["port-id"] = str(port.uuid) + port_dict["net-id"] = str(port.network_id) + port_dict["int-id"] = port.interface_id + port_dict["state"] = port.state + return port_dict + except Exception, exc: + LOG.error("Failed to create port: %s" % str(exc)) + + def delete_port(self, net_id, port_id): + """Delete a port""" + try: + port = db.port_destroy(net_id, port_id) + LOG.debug("Deleted port %s" % port.uuid) + port_dict = {} + port_dict["port-id"] = str(port.uuid) + return port_dict + except Exception, exc: + raise Exception("Failed to delete port: %s" % str(exc)) + + def update_port(self, net_id, port_id, port_state): + """Update a port""" + try: + port = db.port_set_state(net_id, port_id, port_state) + LOG.debug("Updated port %s" % port.uuid) + port_dict = {} + port_dict["port-id"] = str(port.uuid) + port_dict["net-id"] = str(port.network_id) + port_dict["int-id"] = port.interface_id + port_dict["state"] = port.state + return port_dict + except Exception, exc: + raise Exception("Failed to update port state: %s" % str(exc)) + + def plug_interface(self, net_id, port_id, int_id): + """Plug interface to a port""" + try: + port = db.port_set_attachment(net_id, port_id, int_id) + LOG.debug("Attached interface to port %s" % port.uuid) + port_dict = {} + port_dict["port-id"] = str(port.uuid) + port_dict["net-id"] = str(port.network_id) + port_dict["int-id"] = port.interface_id + port_dict["state"] = port.state + return port_dict + except Exception, exc: + raise Exception("Failed to plug interface: %s" % str(exc)) + + def unplug_interface(self, net_id, port_id): + """Unplug interface to a port""" + try: + port = db.port_unset_attachment(net_id, port_id) + LOG.debug("Detached interface from port %s" % port.uuid) + port_dict = {} + port_dict["port-id"] = str(port.uuid) + port_dict["net-id"] = str(port.network_id) + port_dict["int-id"] = port.interface_id + port_dict["state"] = port.state + return port_dict + except Exception, exc: + raise Exception("Failed to unplug interface: %s" % str(exc)) + + +class L2networkDBTest(unittest.TestCase): + """Class conisting of L2network DB unit tests""" + def setUp(self): + """Setup for tests""" + l2network_db.initialize() + l2network_db.create_vlanids() + self.dbtest = L2networkDB() + self.quantum = QuantumDB() + LOG.debug("Setup") + + def tearDown(self): + """Tear Down""" + db.clear_db() + + def testa_create_vlanbinding(self): + """test add vlan binding""" + net1 = self.quantum.create_network("t1", "netid1") + vlan1 = self.dbtest.create_vlan_binding(10, net1["net-id"]) + self.assertTrue(vlan1["vlan-id"] == "10") + self.teardown_vlanbinding() + self.teardown_network() + + def testb_getall_vlanbindings(self): + """test get all vlan binding""" + net1 = self.quantum.create_network("t1", "netid1") + net2 = self.quantum.create_network("t1", "netid2") + vlan1 = self.dbtest.create_vlan_binding(10, net1["net-id"]) + self.assertTrue(vlan1["vlan-id"] == "10") + vlan2 = self.dbtest.create_vlan_binding(20, net2["net-id"]) + self.assertTrue(vlan2["vlan-id"] == "20") + vlans = self.dbtest.get_all_vlan_bindings() + self.assertTrue(len(vlans) == 2) + self.teardown_vlanbinding() + self.teardown_network() + + def testc_delete_vlanbinding(self): + """test delete vlan binding""" + net1 = self.quantum.create_network("t1", "netid1") + vlan1 = self.dbtest.create_vlan_binding(10, net1["net-id"]) + self.assertTrue(vlan1["vlan-id"] == "10") + self.dbtest.delete_vlan_binding(net1["net-id"]) + vlans = self.dbtest.get_all_vlan_bindings() + count = 0 + for vlan in vlans: + if vlan["vlan-id"] is "10": + count += 1 + self.assertTrue(count == 0) + self.teardown_vlanbinding() + self.teardown_network() + + def testd_update_vlanbinding(self): + """test update vlan binding""" + net1 = self.quantum.create_network("t1", "netid1") + vlan1 = self.dbtest.create_vlan_binding(10, net1["net-id"]) + self.assertTrue(vlan1["vlan-id"] == "10") + vlan1 = self.dbtest.update_vlan_binding(net1["net-id"], 11) + self.assertTrue(vlan1["vlan-id"] == "11") + self.teardown_vlanbinding() + self.teardown_network() + + def teste_test_vlanids(self): + """test vlanid methods""" + l2network_db.create_vlanids() + vlanids = l2network_db.get_all_vlanids() + self.assertTrue(len(vlanids) > 0) + vlanid = l2network_db.reserve_vlanid() + used = l2network_db.is_vlanid_used(vlanid) + self.assertTrue(used) + used = l2network_db.release_vlanid(vlanid) + self.assertFalse(used) + #counting on default teardown here to clear db + + def teardown_network(self): + """tearDown Network table""" + LOG.debug("Tearing Down Network") + nets = self.quantum.get_all_networks("t1") + for net in nets: + netid = net["net-id"] + self.quantum.delete_network(netid) + + def teardown_vlanbinding(self): + """tearDown VlanBinding table""" + LOG.debug("Tearing Down Vlan Binding") + vlans = self.dbtest.get_all_vlan_bindings() + for vlan in vlans: + netid = vlan["net-id"] + self.dbtest.delete_vlan_binding(netid) diff --git a/setup.py b/setup.py index b39821f1df..788be9f26b 100644 --- a/setup.py +++ b/setup.py @@ -70,6 +70,7 @@ config_path = 'etc/quantum/' init_path = 'etc/init.d' ovs_plugin_config_path = 'etc/quantum/plugins/openvswitch' cisco_plugin_config_path = 'etc/quantum/plugins/cisco' +linuxbridge_plugin_config_path = 'etc/quantum/plugins/linuxbridge' DataFiles = [ (config_path, @@ -84,6 +85,8 @@ DataFiles = [ 'etc/quantum/plugins/cisco/ucs.ini', 'etc/quantum/plugins/cisco/cisco_plugins.ini', 'etc/quantum/plugins/cisco/db_conn.ini']), + (linuxbridge_plugin_config_path, + ['etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini']), ] setup(