From 546151399d5b8502fab10828d40e3c9a103c7a31 Mon Sep 17 00:00:00 2001 From: Bob Kukura Date: Fri, 5 Oct 2012 11:50:39 -0400 Subject: [PATCH] Initial Modular L2 plugin implementation. The Modular L2 Plugin uses drivers to support separately extensible sets of network types and of mechanisms for accessing networks of those types. This is an initial implementation that has been tested with the openvswitch and linuxbridge agents, and should also work with the hyperv agent. See quantum/plugins/ml2/README for details. Implements: blueprint modular-l2 Change-Id: Ia8cae480180f0990af7d5e5e56f29eaeac205e0e --- etc/quantum/plugins/ml2/ml2_conf.ini | 79 ++++ .../versions/5ac71e65402c_ml2_initial.py | 84 +++++ .../versions/folsom_initial.py | 3 + quantum/extensions/portbindings.py | 2 + quantum/extensions/providernet.py | 1 + quantum/plugins/ml2/README | 82 +++++ quantum/plugins/ml2/__init__.py | 14 + quantum/plugins/ml2/config.py | 39 ++ quantum/plugins/ml2/db.py | 100 +++++ quantum/plugins/ml2/driver_api.py | 158 ++++++++ quantum/plugins/ml2/drivers/__init__.py | 14 + quantum/plugins/ml2/drivers/type_flat.py | 134 +++++++ quantum/plugins/ml2/drivers/type_local.py | 62 ++++ quantum/plugins/ml2/drivers/type_vlan.py | 269 ++++++++++++++ quantum/plugins/ml2/managers.py | 133 +++++++ quantum/plugins/ml2/models.py | 37 ++ quantum/plugins/ml2/plugin.py | 342 ++++++++++++++++++ quantum/plugins/ml2/rpc.py | 203 +++++++++++ quantum/tests/unit/ml2/__init__.py | 14 + .../tests/unit/ml2/test_agent_scheduler.py | 32 ++ quantum/tests/unit/ml2/test_ml2_plugin.py | 63 ++++ quantum/tests/unit/ml2/test_rpcapi.py | 108 ++++++ quantum/tests/unit/ml2/test_security_group.py | 89 +++++ setup.cfg | 5 + tools/pip-requires | 1 + 25 files changed, 2068 insertions(+) create mode 100644 etc/quantum/plugins/ml2/ml2_conf.ini create mode 100644 quantum/db/migration/alembic_migrations/versions/5ac71e65402c_ml2_initial.py create mode 100644 quantum/plugins/ml2/README create mode 100644 quantum/plugins/ml2/__init__.py create mode 100644 quantum/plugins/ml2/config.py create mode 100644 quantum/plugins/ml2/db.py create mode 100644 quantum/plugins/ml2/driver_api.py create mode 100644 quantum/plugins/ml2/drivers/__init__.py create mode 100644 quantum/plugins/ml2/drivers/type_flat.py create mode 100644 quantum/plugins/ml2/drivers/type_local.py create mode 100644 quantum/plugins/ml2/drivers/type_vlan.py create mode 100644 quantum/plugins/ml2/managers.py create mode 100644 quantum/plugins/ml2/models.py create mode 100644 quantum/plugins/ml2/plugin.py create mode 100644 quantum/plugins/ml2/rpc.py create mode 100644 quantum/tests/unit/ml2/__init__.py create mode 100644 quantum/tests/unit/ml2/test_agent_scheduler.py create mode 100644 quantum/tests/unit/ml2/test_ml2_plugin.py create mode 100644 quantum/tests/unit/ml2/test_rpcapi.py create mode 100644 quantum/tests/unit/ml2/test_security_group.py diff --git a/etc/quantum/plugins/ml2/ml2_conf.ini b/etc/quantum/plugins/ml2/ml2_conf.ini new file mode 100644 index 0000000000..4c0aeba77f --- /dev/null +++ b/etc/quantum/plugins/ml2/ml2_conf.ini @@ -0,0 +1,79 @@ +[DATABASE] +# (StrOpt) SQLAlchemy database connection string. This MUST be changed +# to actually run the plugin with persistent storage. +# +# Default: sql_connection = sqlite:// +# Example: sql_connection = mysql://root:password@localhost/quantum_ml2?charset=utf8 + +# (IntOpt) Database reconnection retry limit after database +# connectivity is lost. Value of -1 specifies infinite retry limit. +# +# Default: sql_max_retries = -1 +# Example: sql_max_retries = 10 + +# (IntOpt) Database reconnection interval in seconds after the initial +# connection to the database fails. +# +# Default: reconnect_interval = 2 +# Example: reconnect_interval = 10 + +# (BoolOpt) Enable the use of eventlet's db_pool for MySQL. The flags +# sql_min_pool_size, sql_max_pool_size and sql_idle_timeout are +# relevant only if this is enabled. +# +# Default: sql_dbpool_enable = False +# Example: sql_dbpool_enable = True + +# (IntOpt) Minimum number of MySQL connections to keep open in a pool. +# +# Default: sql_min_pool_size = 1 +# Example: sql_min_pool_size = 5 + +# (IntOpt) Maximum number of MySQL connections to keep open in a pool. +# +# Default: sql_max_pool_size = 5 +# Example: sql_max_pool_size = 20 + +# (IntOpt) Timeout in seconds before idle MySQL connections are +# reaped. +# +# Default: sql_idle_timeout = 3600 +# Example: sql_idle_timeout = 6000 + +# (IntOpt) Maximum number of SQL connections to keep open in a +# QueuePool in SQLAlchemy. +# +# Default: sqlalchemy_pool_size = 5 +# Example: sqlalchemy_pool_size = 10 + +[ml2] +# (ListOpt) List of network type driver entrypoints to be loaded from +# the quantum.ml2.type_drivers namespace. +# +# Default: type_drivers = local,flat,vlan +# Example: type_drivers = flat,vlan,gre + +# (ListOpt) Ordered list of network_types to allocate as tenant +# networks. The default value 'local' is useful for single-box testing +# but provides no connectivity between hosts. +# +# Default: tenant_network_types = local +# Example: tenant_network_types = vlan,gre + +[ml2_type_flat] +# (ListOpt) List of physical_network names with which flat networks +# can be created. Use * to allow flat networks with arbitrary +# physical_network names. +# +# Default:flat_networks = +# Example:flat_networks = physnet1,physnet2 +# Example:flat_networks = * + +[ml2_type_vlan] +# (ListOpt) List of [::] tuples +# specifying physical_network names usable for VLAN provider and +# tenant networks, as well as ranges of VLAN tags on each +# physical_network available for allocation as tenant networks. +# +# Default: network_vlan_ranges = +# Example: network_vlan_ranges = physnet1:1000:2999,physnet2 diff --git a/quantum/db/migration/alembic_migrations/versions/5ac71e65402c_ml2_initial.py b/quantum/db/migration/alembic_migrations/versions/5ac71e65402c_ml2_initial.py new file mode 100644 index 0000000000..6fb8ada41b --- /dev/null +++ b/quantum/db/migration/alembic_migrations/versions/5ac71e65402c_ml2_initial.py @@ -0,0 +1,84 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 OpenStack Foundation +# +# 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. +# + +"""ml2_initial + +Revision ID: 5ac71e65402c +Revises: 32b517556ec9 +Create Date: 2013-05-27 16:08:40.853821 + +""" + +# revision identifiers, used by Alembic. +revision = '5ac71e65402c' +down_revision = '32b517556ec9' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'quantum.plugins.ml2.plugin.Ml2Plugin' +] + +from alembic import op +import sqlalchemy as sa + + +from quantum.db import migration + + +def upgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + ### commands auto generated by Alembic - please adjust! ### + op.create_table( + 'ml2_network_segments', + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('network_id', sa.String(length=36), nullable=False), + sa.Column('network_type', sa.String(length=32), nullable=False), + sa.Column('physical_network', sa.String(length=64), nullable=True), + sa.Column('segmentation_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['network_id'], ['networks.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table( + 'ml2_vlan_allocations', + sa.Column('physical_network', sa.String(length=64), nullable=False), + sa.Column('vlan_id', sa.Integer(), autoincrement=False, + nullable=False), + sa.Column('allocated', sa.Boolean(), autoincrement=False, + nullable=False), + sa.PrimaryKeyConstraint('physical_network', 'vlan_id') + ) + op.create_table( + 'ml2_flat_allocations', + sa.Column('physical_network', sa.String(length=64), nullable=False), + sa.PrimaryKeyConstraint('physical_network') + ) + ### end Alembic commands ### + + +def downgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('ml2_network_segments') + op.drop_table('ml2_flat_allocations') + op.drop_table('ml2_vlan_allocations') + ### end Alembic commands ### diff --git a/quantum/db/migration/alembic_migrations/versions/folsom_initial.py b/quantum/db/migration/alembic_migrations/versions/folsom_initial.py index ffd232964b..c95d7b4434 100644 --- a/quantum/db/migration/alembic_migrations/versions/folsom_initial.py +++ b/quantum/db/migration/alembic_migrations/versions/folsom_initial.py @@ -29,6 +29,7 @@ PLUGINS = { 'cisco': 'quantum.plugins.cisco.network_plugin.PluginV2', 'lbr': 'quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2', 'meta': 'quantum.plugins.metaplugin.meta_quantum_plugin.MetaPluginV2', + 'ml2': 'quantum.plugins.ml2.ml2_plugin.Ml2Plugin', 'nec': 'quantum.plugins.nec.nec_plugin.NECPluginV2', 'nvp': 'quantum.plugins.nicira.QuantumPlugin.NvpPluginV2', 'ovs': 'quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2', @@ -38,6 +39,7 @@ PLUGINS = { L3_CAPABLE = [ PLUGINS['lbr'], PLUGINS['meta'], + PLUGINS['ml2'], PLUGINS['nec'], PLUGINS['ovs'], PLUGINS['ryu'], @@ -45,6 +47,7 @@ L3_CAPABLE = [ FOLSOM_QUOTA = [ PLUGINS['lbr'], + PLUGINS['ml2'], PLUGINS['nvp'], PLUGINS['ovs'], ] diff --git a/quantum/extensions/portbindings.py b/quantum/extensions/portbindings.py index 67a22a84e8..2bf4f24d2c 100644 --- a/quantum/extensions/portbindings.py +++ b/quantum/extensions/portbindings.py @@ -35,6 +35,8 @@ PROFILE = 'binding:profile' CAPABILITIES = 'binding:capabilities' CAP_PORT_FILTER = 'port_filter' +VIF_TYPE_UNBOUND = 'unbound' +VIF_TYPE_BINDING_FAILED = 'binding_failed' VIF_TYPE_OVS = 'ovs' VIF_TYPE_BRIDGE = 'bridge' VIF_TYPE_802_QBG = '802.1qbg' diff --git a/quantum/extensions/providernet.py b/quantum/extensions/providernet.py index 80a189d6f1..a626652281 100644 --- a/quantum/extensions/providernet.py +++ b/quantum/extensions/providernet.py @@ -30,6 +30,7 @@ EXTENDED_ATTRIBUTES_2_0 = { 'enforce_policy': True, 'is_visible': True}, PHYSICAL_NETWORK: {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, 'default': attributes.ATTR_NOT_SPECIFIED, 'enforce_policy': True, 'is_visible': True}, diff --git a/quantum/plugins/ml2/README b/quantum/plugins/ml2/README new file mode 100644 index 0000000000..38d704dd42 --- /dev/null +++ b/quantum/plugins/ml2/README @@ -0,0 +1,82 @@ +The Modular Layer 2 (ml2) plugin is a framework allowing OpenStack +Networking to simultaneously utilize the variety of layer 2 networking +technologies found in complex real-world data centers. It currently +works with the existing openvswitch, linuxbridge, and hyperv L2 +agents, and is intended to replace and deprecate the monolithic +plugins associated with those L2 agents. The ml2 framework is also +intended to greatly simplify adding support for new L2 networking +technologies, requiring much less initial and ongoing effort than +would be required to add a new monolithic core plugin. + +Drivers within ml2 implement separately extensible sets of network +types and of mechanisms for accessing networks of those types. Unlike +with the metaplugin, multiple mechanisms can be used simultaneously to +access different ports of the same virtual network. Mechanisms can +utilize L2 agents via RPC and/or use mechanism drivers to interact +with external devices or controllers. Virtual networks can be composed +of multiple segments of the same or different types. Type and +mechanism drivers are loaded as python entrypoints using the stevedore +library. + +Each available network type is managed by an ml2 +TypeDriver. TypeDrivers maintain any needed type-specific network +state, and perform provider network validation and tenant network +allocation. The initial ml2 version includes drivers for the local, +flat, and vlan network types. Additional TypeDrivers for gre and vxlan +network types are expected before the havana release. + +RPC callback and notification interfaces support interaction with L2, +DHCP, and L3 agents. This version has been tested with the existing +openvswitch and linuxbridge plugins' L2 agents, and should also work +with the hyperv L2 agent. A modular agent may be developed as a +follow-on effort. + +Support for mechanism drivers is currently skeletal. The +MechanismDriver interface is currently a stub, with details to be +defined in future versions. MechanismDrivers will be called both +inside and following DB transactions for network and port +create/update/delete operations. They will also be called to establish +a port binding, determining the VIF type and network segment to be +used. + +The database schema and driver APIs support multi-segment networks, +but the client API for multi-segment networks is not yet implemented. + +A devstack patch supporting use of the ml2 plugin with either the +openvswitch or linuxbridge L2 agent for the local, flat and vlan +network types is under review at +https://review.openstack.org/#/c/27576/. Note that the gre network +type and the tunnel-related RPCs are not yet implemented, so use the +vlan network type for multi-node testing. Also note that ml2 does not +yet work with nova's GenericVIFDriver, so it is necessary to configure +nova to use a specific driver compatible with the L2 agent deployed on +each compute node. + +Note that the ml2 plugin is new and should be conidered experimental +at this point. It is undergoing rapid development, so driver APIs and +other details are likely to change during the havana development +cycle. + +Follow-on tasks required for full ml2 support in havana, including +parity with the existing monolithic openvswitch, linuxbridge, and +hyperv plugins: + +- Additional unit tests + +- Implement MechanismDriver port binding so that a useful + binding:vif_type value is returned for nova's GenericVIFDriver based + on the binding:host_id value and information from the agents_db + +- Implement TypeDriver for GRE networks + +- Implement GRE tunnel endpoint management RPCs + + +Additional follow-on tasks expected for the havana release: + +- Extend MechanismDriver API to support integration with external + devices such as SDN controllers and top-of-rack switches + +- Implement TypeDriver for VXLAN networks + +- Extend providernet extension API to support multi-segment networks diff --git a/quantum/plugins/ml2/__init__.py b/quantum/plugins/ml2/__init__.py new file mode 100644 index 0000000000..788cea1f70 --- /dev/null +++ b/quantum/plugins/ml2/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/quantum/plugins/ml2/config.py b/quantum/plugins/ml2/config.py new file mode 100644 index 0000000000..139cfeda19 --- /dev/null +++ b/quantum/plugins/ml2/config.py @@ -0,0 +1,39 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from oslo.config import cfg + +from quantum import scheduler + + +ml2_opts = [ + cfg.ListOpt('type_drivers', + default=['local', 'flat', 'vlan'], + help=_("List of network type driver entrypoints to be loaded " + "from the quantum.ml2.type_drivers namespace.")), + cfg.ListOpt('tenant_network_types', + default=['local'], + help=_("Ordered list of network_types to allocate as tenant " + "networks.")), + cfg.ListOpt('mechanism_drivers', + default=[], + help=_("List of networking mechanism driver entrypoints to " + "be loaded from the quantum.ml2.mechanism_drivers " + "namespace.")), +] + + +cfg.CONF.register_opts(ml2_opts, "ml2") +cfg.CONF.register_opts(scheduler.AGENTS_SCHEDULER_OPTS) diff --git a/quantum/plugins/ml2/db.py b/quantum/plugins/ml2/db.py new file mode 100644 index 0000000000..05f55c30a5 --- /dev/null +++ b/quantum/plugins/ml2/db.py @@ -0,0 +1,100 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from sqlalchemy.orm import exc + +from quantum.db import api as db_api +from quantum.db import models_v2 +from quantum.db import securitygroups_db as sg_db +from quantum import manager +from quantum.openstack.common import log +from quantum.openstack.common import uuidutils +from quantum.plugins.ml2 import driver_api as api +from quantum.plugins.ml2 import models + +LOG = log.getLogger(__name__) + + +def initialize(): + db_api.configure_db() + + +def add_network_segment(session, network_id, segment): + with session.begin(subtransactions=True): + record = models.NetworkSegment( + id=uuidutils.generate_uuid(), + network_id=network_id, + network_type=segment.get(api.NETWORK_TYPE), + physical_network=segment.get(api.PHYSICAL_NETWORK), + segmentation_id=segment.get(api.SEGMENTATION_ID) + ) + session.add(record) + LOG.info(_("Added segment %(id)s of type %(network_type)s for network" + " %(network_id)s"), record) + + +def get_network_segments(session, network_id): + with session.begin(subtransactions=True): + records = (session.query(models.NetworkSegment). + filter_by(network_id=network_id)) + return [{api.NETWORK_TYPE: record.network_type, + api.PHYSICAL_NETWORK: record.physical_network, + api.SEGMENTATION_ID: record.segmentation_id} + for record in records] + + +def get_port(session, port_id): + """Get port record for update within transcation.""" + + with session.begin(subtransactions=True): + try: + record = (session.query(models_v2.Port). + filter(models_v2.Port.id.startswith(port_id)). + one()) + return record + except exc.NoResultFound: + return + except exc.MultipleResultsFound: + LOG.error(_("Multiple ports have port_id starting with %s"), + port_id) + return + + +def get_port_and_sgs(port_id): + """Get port from database with security group info.""" + + LOG.debug(_("get_port_and_sgs() called for port_id %s"), port_id) + session = db_api.get_session() + sg_binding_port = sg_db.SecurityGroupPortBinding.port_id + + with session.begin(subtransactions=True): + query = session.query(models_v2.Port, + sg_db.SecurityGroupPortBinding.security_group_id) + query = query.outerjoin(sg_db.SecurityGroupPortBinding, + models_v2.Port.id == sg_binding_port) + query = query.filter(models_v2.Port.id.startswith(port_id)) + port_and_sgs = query.all() + if not port_and_sgs: + return + port = port_and_sgs[0][0] + plugin = manager.QuantumManager.get_plugin() + port_dict = plugin._make_port_dict(port) + port_dict['security_groups'] = [ + sg_id for port_, sg_id in port_and_sgs if sg_id] + port_dict['security_group_rules'] = [] + port_dict['security_group_source_groups'] = [] + port_dict['fixed_ips'] = [ip['ip_address'] + for ip in port['fixed_ips']] + return port_dict diff --git a/quantum/plugins/ml2/driver_api.py b/quantum/plugins/ml2/driver_api.py new file mode 100644 index 0000000000..305fd3e600 --- /dev/null +++ b/quantum/plugins/ml2/driver_api.py @@ -0,0 +1,158 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from abc import ABCMeta, abstractmethod + +# The following keys are used in the segment dictionaries passed via +# the driver API. These are defined separately from similar keys in +# quantum.extensions.providernet so that drivers don't need to change +# if/when providernet moves to the core API. +# +NETWORK_TYPE = 'network_type' +PHYSICAL_NETWORK = 'physical_network' +SEGMENTATION_ID = 'segmentation_id' + + +class TypeDriver(object): + """Define stable abstract interface for ML2 type drivers. + + ML2 type drivers each support a specific network_type for provider + and/or tenant network segments. Type drivers must implement this + abstract interface, which defines the API by which the plugin uses + the driver to manage the persistent type-specific resource + allocation state associated with network segments of that type. + + Network segments are represented by segment dictionaries using the + NETWORK_TYPE, PHYSICAL_NETWORK, and SEGMENTATION_ID keys defined + above, corresponding to the provider attributes. Future revisions + of the TypeDriver API may add additional segment dictionary + keys. Attributes not applicable for a particular network_type may + either be excluded or stored as None. + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def get_type(self): + """Get driver's network type. + + :returns network_type value handled by this driver + """ + pass + + @abstractmethod + def initialize(self): + """Perform driver initialization. + + Called after all drivers have been loaded and the database has + been initialized. No abstract methods defined below will be + called prior to this method being called. + """ + pass + + @abstractmethod + def validate_provider_segment(self, segment): + """Validate attributes of a provider network segment. + + :param segment: segment dictionary using keys defined above + :returns: segment dictionary with any defaulted attributes added + :raises: quantum.common.exceptions.InvalidInput if invalid + + Called outside transaction context to validate the provider + attributes for a provider network segment. Raise InvalidInput + if: + + - any required attribute is missing + - any prohibited or unrecognized attribute is present + - any attribute value is not valid + + The network_type attribute is present in segment, but + need not be validated. + """ + pass + + @abstractmethod + def reserve_provider_segment(self, session, segment): + """Reserve resource associated with a provider network segment. + + :param session: database session + :param segment: segment dictionary using keys defined above + + Called inside transaction context on session to reserve the + type-specific resource for a provider network segment. The + segment dictionary passed in was returned by a previous + validate_provider_segment() call. + """ + pass + + @abstractmethod + def allocate_tenant_segment(self, session): + """Allocate resource for a new tenant network segment. + + :param session: database session + :returns: segment dictionary using keys defined above + + Called inside transaction context on session to allocate a new + tenant network, typically from a type-specific resource + pool. If successful, return a segment dictionary describing + the segment. If tenant network segment cannot be allocated + (i.e. tenant networks not supported or resource pool is + exhausted), return None. + """ + pass + + @abstractmethod + def release_segment(self, session, segment): + """Release network segment. + + :param session: database session + :param segment: segment dictionary using keys defined above + + Called inside transaction context on session to release a + tenant or provider network's type-specific resource. Runtime + errors are not expected, but raising an exception will result + in rollback of the transaction. + """ + pass + + +class MechanismDriver(object): + """Define stable abstract interface for ML2 mechanism drivers. + + Note that this is currently a stub class, but it is expected to be + functional for the H-2 milestone. It currently serves mainly to + help solidify the architectural distinction between TypeDrivers + and MechanismDrivers. + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def initialize(self): + """Perform driver initialization. + + Called after all drivers have been loaded and the database has + been initialized. No abstract methods defined below will be + called prior to this method being called. + """ + pass + + # TODO(rkukura): Add methods called inside and after transaction + # for create_network, update_network, delete_network, create_port, + # update_port, delete_port, and maybe for port binding + # changes. Exceptions raised by methods called inside transactions + # can rollback, but shouldn't block. Methods called after + # transaction commits can block, and exceptions may cause deletion + # of resource. diff --git a/quantum/plugins/ml2/drivers/__init__.py b/quantum/plugins/ml2/drivers/__init__.py new file mode 100644 index 0000000000..788cea1f70 --- /dev/null +++ b/quantum/plugins/ml2/drivers/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/quantum/plugins/ml2/drivers/type_flat.py b/quantum/plugins/ml2/drivers/type_flat.py new file mode 100644 index 0000000000..4619a99795 --- /dev/null +++ b/quantum/plugins/ml2/drivers/type_flat.py @@ -0,0 +1,134 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from oslo.config import cfg +import sqlalchemy as sa + +from quantum.common import exceptions as exc +from quantum.db import model_base +from quantum.openstack.common import log +from quantum.plugins.ml2 import driver_api as api + +LOG = log.getLogger(__name__) + +TYPE_FLAT = 'flat' + +flat_opts = [ + cfg.ListOpt('flat_networks', + default=[], + help=_("List of physical_network names with which flat " + "networks can be created. Use * to allow flat " + "networks with arbitrary physical_network names.")) +] + +cfg.CONF.register_opts(flat_opts, "ml2_type_flat") + + +class FlatAllocation(model_base.BASEV2): + """Represent persistent allocation state of a physical network. + + If a record exists for a physical network, then that physical + network has been allocated as a flat network. + """ + + __tablename__ = 'ml2_flat_allocations' + + physical_network = sa.Column(sa.String(64), nullable=False, + primary_key=True) + + +class FlatTypeDriver(api.TypeDriver): + """Manage state for flat networks with ML2. + + The FlatTypeDriver implements the 'flat' network_type. Flat + network segments provide connectivity between VMs and other + devices using any connected IEEE 802.1D conformant + physical_network, without the use of VLAN tags, tunneling, or + other segmentation mechanisms. Therefore at most one flat network + segment can exist on each available physical_network. + """ + + def __init__(self): + self._parse_networks(cfg.CONF.ml2_type_flat.flat_networks) + + def _parse_networks(self, entries): + self.flat_networks = entries + if '*' in self.flat_networks: + LOG.info(_("Arbitrary flat physical_network names allowed")) + self.flat_networks = None + else: + # TODO(rkukura): Validate that each physical_network name + # is neither empty nor too long. + LOG.info(_("Allowable flat physical_network names: %s"), + self.flat_networks) + + def get_type(self): + return TYPE_FLAT + + def initialize(self): + LOG.info(_("ML2 FlatTypeDriver initialization complete")) + + def validate_provider_segment(self, segment): + physical_network = segment.get(api.PHYSICAL_NETWORK) + if not physical_network: + msg = _("physical_network required for flat provider network") + raise exc.InvalidInput(error_message=msg) + if self.flat_networks and physical_network not in self.flat_networks: + msg = (_("physical_network '%s' unknown for flat provider network") + % physical_network) + raise exc.InvalidInput(error_message=msg) + + for key, value in segment.iteritems(): + if value and key not in [api.NETWORK_TYPE, + api.PHYSICAL_NETWORK]: + msg = _("%s prohibited for flat provider network") % key + raise exc.InvalidInput(error_message=msg) + + return segment + + def reserve_provider_segment(self, session, segment): + physical_network = segment[api.PHYSICAL_NETWORK] + with session.begin(subtransactions=True): + try: + alloc = (session.query(FlatAllocation). + filter_by(physical_network=physical_network). + with_lockmode('update'). + one()) + raise exc.FlatNetworkInUse( + physical_network=physical_network) + except sa.orm.exc.NoResultFound: + LOG.debug(_("Reserving flat network on physical " + "network %s"), physical_network) + alloc = FlatAllocation(physical_network=physical_network) + session.add(alloc) + + def allocate_tenant_segment(self, session): + # Tenant flat networks are not supported. + return + + def release_segment(self, session, segment): + physical_network = segment[api.PHYSICAL_NETWORK] + with session.begin(subtransactions=True): + try: + alloc = (session.query(FlatAllocation). + filter_by(physical_network=physical_network). + with_lockmode('update'). + one()) + session.delete(alloc) + LOG.debug(_("Releasing flat network on physical " + "network %s"), physical_network) + except sa.orm.exc.NoResultFound: + LOG.warning(_("No flat network found on physical network %s"), + physical_network) diff --git a/quantum/plugins/ml2/drivers/type_local.py b/quantum/plugins/ml2/drivers/type_local.py new file mode 100644 index 0000000000..f79485babd --- /dev/null +++ b/quantum/plugins/ml2/drivers/type_local.py @@ -0,0 +1,62 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from quantum.common import exceptions as exc +from quantum.openstack.common import log +from quantum.plugins.ml2 import driver_api as api + +LOG = log.getLogger(__name__) + +TYPE_LOCAL = 'local' + + +class LocalTypeDriver(api.TypeDriver): + """Manage state for local networks with ML2. + + The LocalTypeDriver implements the 'local' network_type. Local + network segments provide connectivity between VMs and other + devices running on the same node, provided that a common local + network bridging technology is available to those devices. Local + network segments do not provide any connectivity between nodes. + """ + + def __init__(self): + LOG.info(_("ML2 LocalTypeDriver initialization complete")) + + def get_type(self): + return TYPE_LOCAL + + def initialize(self): + pass + + def validate_provider_segment(self, segment): + for key, value in segment.iteritems(): + if value and key not in [api.NETWORK_TYPE]: + msg = _("%s prohibited for local provider network") % key + raise exc.InvalidInput(error_message=msg) + + return segment + + def reserve_provider_segment(self, session, segment): + # No resources to reserve + pass + + def allocate_tenant_segment(self, session): + # No resources to allocate + return {api.NETWORK_TYPE: TYPE_LOCAL} + + def release_segment(self, session, segment): + # No resources to release + pass diff --git a/quantum/plugins/ml2/drivers/type_vlan.py b/quantum/plugins/ml2/drivers/type_vlan.py new file mode 100644 index 0000000000..357744e0fc --- /dev/null +++ b/quantum/plugins/ml2/drivers/type_vlan.py @@ -0,0 +1,269 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +import sys + +from oslo.config import cfg +import sqlalchemy as sa + +from quantum.common import constants as q_const +from quantum.common import exceptions as exc +from quantum.common import utils +from quantum.db import api as db_api +from quantum.db import model_base +from quantum.openstack.common import log +from quantum.plugins.common import utils as plugin_utils +from quantum.plugins.ml2 import driver_api as api + +LOG = log.getLogger(__name__) + +TYPE_VLAN = 'vlan' + +vlan_opts = [ + cfg.ListOpt('network_vlan_ranges', + default=[], + help=_("List of :: or " + " specifying physical_network names " + "usable for VLAN provider and tenant networks, as " + "well as ranges of VLAN tags on each available for " + "allocation to tenant networks.")) +] + +cfg.CONF.register_opts(vlan_opts, "ml2_type_vlan") + + +class VlanAllocation(model_base.BASEV2): + """Represent allocation state of a vlan_id on a physical network. + + If allocated is False, the vlan_id on the physical_network is + available for allocation to a tenant network. If allocated is + True, the vlan_id on the physical_network is in use, either as a + tenant or provider network. + + When an allocation is released, if the vlan_id for the + physical_network is inside the pool described by + VlanTypeDriver.network_vlan_ranges, then allocated is set to + False. If it is outside the pool, the record is deleted. + """ + + __tablename__ = 'ml2_vlan_allocations' + + physical_network = sa.Column(sa.String(64), nullable=False, + primary_key=True) + vlan_id = sa.Column(sa.Integer, nullable=False, primary_key=True, + autoincrement=False) + allocated = sa.Column(sa.Boolean, nullable=False) + + +class VlanTypeDriver(api.TypeDriver): + """Manage state for VLAN networks with ML2. + + The VlanTypeDriver implements the 'vlan' network_type. VLAN + network segments provide connectivity between VMs and other + devices using any connected IEEE 802.1Q conformant + physical_network segmented into virtual networks via IEEE 802.1Q + headers. Up to 4094 VLAN network segments can exist on each + available physical_network. + """ + + def __init__(self): + self._parse_network_vlan_ranges() + + def _parse_network_vlan_ranges(self): + try: + self.network_vlan_ranges = plugin_utils.parse_network_vlan_ranges( + cfg.CONF.ml2_type_vlan.network_vlan_ranges) + # TODO(rkukura): Validate that each physical_network name + # is neither empty nor too long. + except Exception: + LOG.exception(_("Failed to parse network_vlan_ranges. " + "Service terminated!")) + sys.exit(1) + LOG.info(_("Network VLAN ranges: %s"), self.network_vlan_ranges) + + def _sync_vlan_allocations(self): + session = db_api.get_session() + with session.begin(subtransactions=True): + # get existing allocations for all physical networks + allocations = dict() + allocs = (session.query(VlanAllocation). + with_lockmode('update')) + for alloc in allocs: + if alloc.physical_network not in allocations: + allocations[alloc.physical_network] = set() + allocations[alloc.physical_network].add(alloc) + + # process vlan ranges for each configured physical network + for (physical_network, + vlan_ranges) in self.network_vlan_ranges.iteritems(): + # determine current configured allocatable vlans for + # this physical network + vlan_ids = set() + for vlan_min, vlan_max in vlan_ranges: + vlan_ids |= set(xrange(vlan_min, vlan_max + 1)) + + # remove from table unallocated vlans not currently + # allocatable + if physical_network in allocations: + for alloc in allocations[physical_network]: + try: + # see if vlan is allocatable + vlan_ids.remove(alloc.vlan_id) + except KeyError: + # it's not allocatable, so check if its allocated + if not alloc.allocated: + # it's not, so remove it from table + LOG.debug(_("Removing vlan %(vlan_id)s on " + "physical network " + "%(physical_network)s from pool"), + {'vlan_id': alloc.vlan_id, + 'physical_network': + physical_network}) + session.delete(alloc) + del allocations[physical_network] + + # add missing allocatable vlans to table + for vlan_id in sorted(vlan_ids): + alloc = VlanAllocation(physical_network=physical_network, + vlan_id=vlan_id, + allocated=False) + session.add(alloc) + + # remove from table unallocated vlans for any unconfigured + # physical networks + for allocs in allocations.itervalues(): + for alloc in allocs: + if not alloc.allocated: + LOG.debug(_("Removing vlan %(vlan_id)s on physical " + "network %(physical_network)s from pool"), + {'vlan_id': alloc.vlan_id, + 'physical_network': + alloc.physical_network}) + session.delete(alloc) + + def get_type(self): + return TYPE_VLAN + + def initialize(self): + self._sync_vlan_allocations() + LOG.info(_("VlanTypeDriver initialization complete")) + + def validate_provider_segment(self, segment): + physical_network = segment.get(api.PHYSICAL_NETWORK) + if not physical_network: + msg = _("physical_network required for VLAN provider network") + raise exc.InvalidInput(error_message=msg) + if physical_network not in self.network_vlan_ranges: + msg = (_("physical_network '%s' unknown for VLAN provider network") + % physical_network) + raise exc.InvalidInput(error_message=msg) + + segmentation_id = segment.get(api.SEGMENTATION_ID) + if segmentation_id is None: + msg = _("segmentation_id required for VLAN provider network") + raise exc.InvalidInput(error_message=msg) + if not utils.is_valid_vlan_tag(segmentation_id): + msg = (_("segmentation_id out of range (%(min)s through " + "%(max)s)") % + {'min': q_const.MIN_VLAN_TAG, + 'max': q_const.MAX_VLAN_TAG}) + raise exc.InvalidInput(error_message=msg) + + for key, value in segment.iteritems(): + if value and key not in [api.NETWORK_TYPE, + api.PHYSICAL_NETWORK, + api.SEGMENTATION_ID]: + msg = _("%s prohibited for VLAN provider network") % key + raise exc.InvalidInput(error_message=msg) + + return segment + + def reserve_provider_segment(self, session, segment): + physical_network = segment[api.PHYSICAL_NETWORK] + vlan_id = segment[api.SEGMENTATION_ID] + with session.begin(subtransactions=True): + try: + alloc = (session.query(VlanAllocation). + filter_by(physical_network=physical_network, + vlan_id=vlan_id). + with_lockmode('update'). + one()) + if alloc.allocated: + raise exc.VlanIdInUse(vlan_id=vlan_id, + physical_network=physical_network) + LOG.debug(_("Reserving specific vlan %(vlan_id)s on physical " + "network %(physical_network)s from pool"), + {'vlan_id': vlan_id, + 'physical_network': physical_network}) + alloc.allocated = True + except sa.orm.exc.NoResultFound: + LOG.debug(_("Reserving specific vlan %(vlan_id)s on physical " + "network %(physical_network)s outside pool"), + {'vlan_id': vlan_id, + 'physical_network': physical_network}) + alloc = VlanAllocation(physical_network=physical_network, + vlan_id=vlan_id, + allocated=True) + session.add(alloc) + + def allocate_tenant_segment(self, session): + with session.begin(subtransactions=True): + alloc = (session.query(VlanAllocation). + filter_by(allocated=False). + with_lockmode('update'). + first()) + if alloc: + LOG.debug(_("Allocating vlan %(vlan_id)s on physical network " + "%(physical_network)s from pool"), + {'vlan_id': alloc.vlan_id, + 'physical_network': alloc.physical_network}) + alloc.allocated = True + return {api.NETWORK_TYPE: TYPE_VLAN, + api.PHYSICAL_NETWORK: alloc.physical_network, + api.SEGMENTATION_ID: alloc.vlan_id} + + def release_segment(self, session, segment): + physical_network = segment[api.PHYSICAL_NETWORK] + vlan_id = segment[api.SEGMENTATION_ID] + with session.begin(subtransactions=True): + try: + alloc = (session.query(VlanAllocation). + filter_by(physical_network=physical_network, + vlan_id=vlan_id). + with_lockmode('update'). + one()) + alloc.allocated = False + inside = False + for vlan_min, vlan_max in self.network_vlan_ranges.get( + physical_network, []): + if vlan_min <= vlan_id <= vlan_max: + inside = True + break + if not inside: + session.delete(alloc) + LOG.debug(_("Releasing vlan %(vlan_id)s on physical " + "network %(physical_network)s outside pool"), + {'vlan_id': vlan_id, + 'physical_network': physical_network}) + else: + LOG.debug(_("Releasing vlan %(vlan_id)s on physical " + "network %(physical_network)s to pool"), + {'vlan_id': vlan_id, + 'physical_network': physical_network}) + except sa.orm.exc.NoResultFound: + LOG.warning(_("No vlan_id %(vlan_id)s found on physical " + "network %(physical_network)s"), + {'vlan_id': vlan_id, + 'physical_network': physical_network}) diff --git a/quantum/plugins/ml2/managers.py b/quantum/plugins/ml2/managers.py new file mode 100644 index 0000000000..239de48596 --- /dev/null +++ b/quantum/plugins/ml2/managers.py @@ -0,0 +1,133 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +import sys + +from oslo.config import cfg +import stevedore + +from quantum.common import exceptions as exc +from quantum.openstack.common import log +from quantum.plugins.ml2 import driver_api as api + + +LOG = log.getLogger(__name__) + + +class TypeManager(stevedore.named.NamedExtensionManager): + """Manage network segment types using drivers.""" + + # Mapping from type name to DriverManager + drivers = {} + + def __init__(self): + # REVISIT(rkukura): Need way to make stevedore use our logging + # configuration. Currently, nothing is logged if loading a + # driver fails. + + LOG.info(_("Configured type driver names: %s"), + cfg.CONF.ml2.type_drivers) + super(TypeManager, self).__init__('quantum.ml2.type_drivers', + cfg.CONF.ml2.type_drivers, + invoke_on_load=True) + LOG.info(_("Loaded type driver names: %s"), self.names()) + self._register_types() + self._check_tenant_network_types(cfg.CONF.ml2.tenant_network_types) + + def _register_types(self): + for ext in self: + type = ext.obj.get_type() + if type in self.drivers: + LOG.error(_("Type driver '%(new_driver)s' ignored because type" + " driver '%(old_driver)s' is already registered" + " for type '%(type)s'"), + {'new_driver': ext.name, + 'old_driver': self.drivers[type].name, + 'type': type}) + else: + self.drivers[type] = ext + LOG.info(_("Registered types: %s"), self.drivers.keys()) + + def _check_tenant_network_types(self, types): + self.tenant_network_types = [] + for network_type in types: + if network_type in self.drivers: + self.tenant_network_types.append(network_type) + else: + LOG.error(_("No type driver for tenant network_type: %s. " + "Service terminated!"), + network_type) + sys.exit(1) + LOG.info(_("Tenant network_types: %s"), self.tenant_network_types) + + def initialize(self): + for type, driver in self.drivers.iteritems(): + LOG.info(_("Initializing driver for type '%s'"), type) + driver.obj.initialize() + + def validate_provider_segment(self, segment): + network_type = segment[api.NETWORK_TYPE] + driver = self.drivers.get(network_type) + if driver: + return driver.obj.validate_provider_segment(segment) + else: + msg = _("network_type value '%s' not supported") % network_type + raise exc.InvalidInput(error_message=msg) + + def reserve_provider_segment(self, session, segment): + network_type = segment.get(api.NETWORK_TYPE) + driver = self.drivers.get(network_type) + driver.obj.reserve_provider_segment(session, segment) + + def allocate_tenant_segment(self, session): + for network_type in self.tenant_network_types: + driver = self.drivers.get(network_type) + segment = driver.obj.allocate_tenant_segment(session) + if segment: + return segment + raise exc.NoNetworkAvailable() + + def release_segment(self, session, segment): + network_type = segment.get(api.NETWORK_TYPE) + driver = self.drivers.get(network_type) + driver.obj.release_segment(session, segment) + + +class MechanismManager(stevedore.named.NamedExtensionManager): + """Manage networking mechanisms using drivers. + + Note that this is currently a stub class, but it is expected to be + functional for the H-2 milestone. It currently serves mainly to + help solidify the architectural distinction between TypeDrivers + and MechanismDrivers. + """ + + def __init__(self): + # REVISIT(rkukura): Need way to make stevedore use our logging + # configuration. Currently, nothing is logged if loading a + # driver fails. + + LOG.info(_("Configured mechanism driver names: %s"), + cfg.CONF.ml2.mechanism_drivers) + super(MechanismManager, self).__init__('quantum.ml2.mechanism_drivers', + cfg.CONF.ml2.mechanism_drivers, + invoke_on_load=True) + LOG.info(_("Loaded mechanism driver names: %s"), self.names()) + # TODO(rkukura): Register mechanisms. + + def initialize(self): + pass + + # TODO(rkukura): Define mechanism dispatch methods diff --git a/quantum/plugins/ml2/models.py b/quantum/plugins/ml2/models.py new file mode 100644 index 0000000000..9252e0ed56 --- /dev/null +++ b/quantum/plugins/ml2/models.py @@ -0,0 +1,37 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +import sqlalchemy as sa + +from quantum.db import model_base +from quantum.db import models_v2 + + +class NetworkSegment(model_base.BASEV2, models_v2.HasId): + """Represent persistent state of a network segment. + + A network segment is a portion of a quantum network with a + specific physical realization. A quantum network can consist of + one or more segments. + """ + + __tablename__ = 'ml2_network_segments' + + network_id = sa.Column(sa.String(36), + sa.ForeignKey('networks.id', ondelete="CASCADE"), + nullable=False) + network_type = sa.Column(sa.String(32), nullable=False) + physical_network = sa.Column(sa.String(64)) + segmentation_id = sa.Column(sa.Integer) diff --git a/quantum/plugins/ml2/plugin.py b/quantum/plugins/ml2/plugin.py new file mode 100644 index 0000000000..ed378e79ff --- /dev/null +++ b/quantum/plugins/ml2/plugin.py @@ -0,0 +1,342 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from oslo.config import cfg + +from quantum.agent import securitygroups_rpc as sg_rpc +from quantum.api.rpc.agentnotifiers import dhcp_rpc_agent_api +from quantum.api.rpc.agentnotifiers import l3_rpc_agent_api +from quantum.api.v2 import attributes +from quantum.common import constants as const +from quantum.common import exceptions as exc +from quantum.common import topics +from quantum.db import agentschedulers_db +from quantum.db import db_base_plugin_v2 +from quantum.db import extraroute_db +from quantum.db import portbindings_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 importutils +from quantum.openstack.common import log +from quantum.openstack.common import rpc as c_rpc +from quantum.plugins.ml2 import config # noqa +from quantum.plugins.ml2 import db +from quantum.plugins.ml2 import driver_api as api +from quantum.plugins.ml2 import managers +from quantum.plugins.ml2 import rpc + +LOG = log.getLogger(__name__) + +# REVISIT(rkukura): Move this and other network_type constants to +# providernet.py? +TYPE_MULTI_SEGMENT = 'multi-segment' + + +class Ml2Plugin(db_base_plugin_v2.QuantumDbPluginV2, + extraroute_db.ExtraRoute_db_mixin, + sg_db_rpc.SecurityGroupServerRpcMixin, + agentschedulers_db.AgentSchedulerDbMixin, + portbindings_db.PortBindingMixin): + """Implement the Quantum L2 abstractions using modules. + + Ml2Plugin is a Quantum plugin based on separately extensible sets + of network types and mechanisms for connecting to networks of + those types. The network types and mechanisms are implemented as + drivers loaded via Python entry points. Networks can be made up of + multiple segments (not yet fully implemented). + """ + + # This attribute specifies whether the plugin supports or not + # bulk/pagination/sorting operations. Name mangling is used in + # order to ensure it is qualified by class + __native_bulk_support = True + __native_pagination_support = True + __native_sorting_support = True + + # List of supported extensions + _supported_extension_aliases = ["provider", "router", "extraroute", + "binding", "quotas", "security-group", + "agent", "agent_scheduler"] + + @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 + + def __init__(self): + # First load drivers, then initialize DB, then initialize drivers + self.type_manager = managers.TypeManager() + self.mechanism_manager = managers.MechanismManager() + db.initialize() + self.type_manager.initialize() + self.mechanism_manager.initialize() + + self._setup_rpc() + + # REVISIT(rkukura): Use stevedore for these? + self.network_scheduler = importutils.import_object( + cfg.CONF.network_scheduler_driver) + self.router_scheduler = importutils.import_object( + cfg.CONF.router_scheduler_driver) + + LOG.info(_("Modular L2 Plugin initialization complete")) + + def _setup_rpc(self): + self.notifier = rpc.AgentNotifierApi(topics.AGENT) + self.dhcp_agent_notifier = dhcp_rpc_agent_api.DhcpAgentNotifyAPI() + self.l3_agent_notifier = l3_rpc_agent_api.L3AgentNotify + self.callbacks = rpc.RpcCallbacks(self.notifier) + self.topic = topics.PLUGIN + self.conn = c_rpc.create_connection(new=True) + self.dispatcher = self.callbacks.create_rpc_dispatcher() + self.conn.create_consumer(self.topic, self.dispatcher, + fanout=False) + self.conn.consume_in_thread() + + def _process_provider_create(self, context, attrs): + network_type = self._get_attribute(attrs, provider.NETWORK_TYPE) + physical_network = self._get_attribute(attrs, + provider.PHYSICAL_NETWORK) + segmentation_id = self._get_attribute(attrs, provider.SEGMENTATION_ID) + + if attributes.is_attr_set(network_type): + segment = {api.NETWORK_TYPE: network_type, + api.PHYSICAL_NETWORK: physical_network, + api.SEGMENTATION_ID: segmentation_id} + return self.type_manager.validate_provider_segment(segment) + + if (attributes.is_attr_set(attrs.get(provider.PHYSICAL_NETWORK)) or + attributes.is_attr_set(attrs.get(provider.SEGMENTATION_ID))): + msg = _("network_type required if other provider attributes " + "specified") + raise exc.InvalidInput(error_message=msg) + + def _get_attribute(self, attrs, key): + value = attrs.get(key) + if value is attributes.ATTR_NOT_SPECIFIED: + value = None + return value + + def _check_provider_update(self, context, attrs): + if (attributes.is_attr_set(attrs.get(provider.NETWORK_TYPE)) or + attributes.is_attr_set(attrs.get(provider.PHYSICAL_NETWORK)) or + attributes.is_attr_set(attrs.get(provider.SEGMENTATION_ID))): + msg = _("Plugin does not support updating provider attributes") + raise exc.InvalidInput(error_message=msg) + + def _extend_network_dict_provider(self, context, network): + id = network['id'] + segments = db.get_network_segments(context.session, id) + if not segments: + LOG.error(_("Network %s has no segments"), id) + network[provider.NETWORK_TYPE] = None + network[provider.PHYSICAL_NETWORK] = None + network[provider.SEGMENTATION_ID] = None + elif len(segments) > 1: + network[provider.NETWORK_TYPE] = TYPE_MULTI_SEGMENT + network[provider.PHYSICAL_NETWORK] = None + network[provider.SEGMENTATION_ID] = None + else: + segment = segments[0] + network[provider.NETWORK_TYPE] = segment[api.NETWORK_TYPE] + network[provider.PHYSICAL_NETWORK] = segment[api.PHYSICAL_NETWORK] + network[provider.SEGMENTATION_ID] = segment[api.SEGMENTATION_ID] + + def _filter_nets_provider(self, context, nets, filters): + # TODO(rkukura): Implement filtering. + return nets + + def _extend_port_dict_binding(self, context, port): + # TODO(rkukura): Implement based on host_id, agents, and + # MechanismDrivers. Also set CAPABILITIES. Use + # extra_binding_dict if applicable, or maybe a new hook so + # base handles field processing and get_port and get_ports + # don't need to be overridden. + port[portbindings.VIF_TYPE] = portbindings.VIF_TYPE_UNBOUND + + def _notify_port_updated(self, context, port): + session = context.session + with session.begin(subtransactions=True): + network_id = port['network_id'] + segments = db.get_network_segments(session, network_id) + if not segments: + LOG.warning(_("In _notify_port_updated() for port %(port_id), " + "network %(network_id) has no segments"), + {'port_id': port['id'], + 'network_id': network_id}) + return + # TODO(rkukura): Use port binding to select segment. + segment = segments[0] + self.notifier.port_update(context, port, + segment[api.NETWORK_TYPE], + segment[api.SEGMENTATION_ID], + segment[api.PHYSICAL_NETWORK]) + + def create_network(self, context, network): + attrs = network['network'] + segment = self._process_provider_create(context, attrs) + tenant_id = self._get_tenant_id_for_create(context, attrs) + + session = context.session + with session.begin(subtransactions=True): + self._ensure_default_security_group(context, tenant_id) + if segment: + self.type_manager.reserve_provider_segment(session, segment) + else: + segment = self.type_manager.allocate_tenant_segment(session) + result = super(Ml2Plugin, self).create_network(context, network) + id = result['id'] + self._process_l3_create(context, attrs, id) + # REVISIT(rkukura): Consider moving all segment management + # to TypeManager. + db.add_network_segment(session, id, segment) + self._extend_network_dict_provider(context, result) + self._extend_network_dict_l3(context, result) + + return result + + def update_network(self, context, id, network): + attrs = network['network'] + self._check_provider_update(context, attrs) + + session = context.session + with session.begin(subtransactions=True): + result = super(Ml2Plugin, self).update_network(context, id, + network) + self._process_l3_update(context, attrs, id) + self._extend_network_dict_provider(context, result) + self._extend_network_dict_l3(context, result) + + return result + + def get_network(self, context, id, fields=None): + session = context.session + with session.begin(subtransactions=True): + result = super(Ml2Plugin, self).get_network(context, id, None) + self._extend_network_dict_provider(context, result) + self._extend_network_dict_l3(context, result) + + return self._fields(result, fields) + + def get_networks(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, page_reverse=False): + session = context.session + with session.begin(subtransactions=True): + nets = super(Ml2Plugin, + self).get_networks(context, filters, None, sorts, + limit, marker, page_reverse) + for net in nets: + self._extend_network_dict_provider(context, net) + self._extend_network_dict_l3(context, net) + + nets = self._filter_nets_provider(context, nets, filters) + nets = self._filter_nets_l3(context, nets, filters) + + return [self._fields(net, fields) for net in nets] + + def delete_network(self, context, id): + session = context.session + with session.begin(subtransactions=True): + segments = db.get_network_segments(session, id) + super(Ml2Plugin, self).delete_network(context, id) + for segment in segments: + self.type_manager.release_segment(session, segment) + # The segment records are deleted via cascade from the + # network record, so explicit removal is not necessary. + + self.notifier.network_delete(context, id) + + def create_port(self, context, port): + attrs = port['port'] + attrs['status'] = const.PORT_STATUS_DOWN + + session = context.session + with session.begin(subtransactions=True): + self._ensure_default_security_group_on_port(context, port) + sgids = self._get_security_groups_on_port(context, port) + result = super(Ml2Plugin, self).create_port(context, port) + self._process_portbindings_create_and_update(context, attrs, + result) + self._process_port_create_security_group(context, result, sgids) + self._extend_port_dict_binding(context, result) + + self.notify_security_groups_member_updated(context, result) + return result + + def update_port(self, context, id, port): + attrs = port['port'] + need_port_update_notify = False + + session = context.session + with session.begin(subtransactions=True): + original_port = super(Ml2Plugin, self).get_port(context, id) + updated_port = super(Ml2Plugin, self).update_port(context, id, + port) + need_port_update_notify = self.update_security_group_on_port( + context, id, port, original_port, updated_port) + self._process_portbindings_create_and_update(context, + attrs, + updated_port) + self._extend_port_dict_binding(context, updated_port) + + need_port_update_notify |= self.is_security_group_member_updated( + context, original_port, updated_port) + + if original_port['admin_state_up'] != updated_port['admin_state_up']: + need_port_update_notify = True + + if need_port_update_notify: + self._notify_port_updated(context, updated_port) + + return updated_port + + def get_port(self, context, id, fields=None): + session = context.session + with session.begin(subtransactions=True): + port = super(Ml2Plugin, self).get_port(context, id, fields) + self._extend_port_dict_binding(context, port) + + return self._fields(port, fields) + + def get_ports(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, page_reverse=False): + session = context.session + with session.begin(subtransactions=True): + ports = super(Ml2Plugin, + self).get_ports(context, filters, fields, sorts, + limit, marker, page_reverse) + # TODO(nati): filter by security group + for port in ports: + self._extend_port_dict_binding(context, port) + + return [self._fields(port, fields) for port in ports] + + def delete_port(self, context, id, l3_port_check=True): + if l3_port_check: + self.prevent_l3_port_deletion(context, id) + + session = context.session + with session.begin(subtransactions=True): + self.disassociate_floatingips(context, id) + port = self.get_port(context, id) + self._delete_port_security_group_bindings(context, id) + super(Ml2Plugin, self).delete_port(context, id) + + self.notify_security_groups_member_updated(context, port) diff --git a/quantum/plugins/ml2/rpc.py b/quantum/plugins/ml2/rpc.py new file mode 100644 index 0000000000..c4394c55b3 --- /dev/null +++ b/quantum/plugins/ml2/rpc.py @@ -0,0 +1,203 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from quantum.agent import securitygroups_rpc as sg_rpc +from quantum.common import constants as q_const +from quantum.common import rpc as q_rpc +from quantum.common import topics +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 +from quantum.openstack.common.rpc import proxy +from quantum.plugins.ml2 import db +from quantum.plugins.ml2 import driver_api as api + +LOG = log.getLogger(__name__) + +TAP_DEVICE_PREFIX = 'tap' +TAP_DEVICE_PREFIX_LENGTH = 3 + + +class RpcCallbacks(dhcp_rpc_base.DhcpRpcCallbackMixin, + l3_rpc_base.L3RpcCallbackMixin, + sg_db_rpc.SecurityGroupServerRpcCallbackMixin): + + RPC_API_VERSION = '1.1' + # history + # 1.0 Initial version (from openvswitch/linuxbridge) + # 1.1 Support Security Group RPC + + def __init__(self, notifier): + self.notifier = notifier + + 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 _device_to_port_id(cls, device): + # REVISIT(rkukura): Consider calling into MechanismDrivers to + # process device names, or having MechanismDrivers supply list + # of device prefixes to strip. + if device.startswith(TAP_DEVICE_PREFIX): + return device[TAP_DEVICE_PREFIX_LENGTH:] + else: + return device + + @classmethod + def get_port_from_device(cls, device): + port_id = cls._device_to_port_id(device) + port = db.get_port_and_sgs(port_id) + if port: + port['device'] = 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 %(device)s details requested by agent " + "%(agent_id)s"), + {'device': device, 'agent_id': agent_id}) + port_id = self._device_to_port_id(device) + + session = db_api.get_session() + with session.begin(subtransactions=True): + port = db.get_port(session, port_id) + if not port: + LOG.warning(_("Device %(device)s requested by agent " + "%(agent_id)s not found in database"), + {'device': device, 'agent_id': agent_id}) + return {'device': device} + segments = db.get_network_segments(session, port.network_id) + if not segments: + LOG.warning(_("Device %(device)s requested by agent " + "%(agent_id)s has network %(network_id) with " + "no segments"), + {'device': device, + 'agent_id': agent_id, + 'network_id': port.network_id}) + return {'device': device} + #TODO(rkukura): Use/create port binding + segment = segments[0] + new_status = (q_const.PORT_STATUS_ACTIVE if port.admin_state_up + else q_const.PORT_STATUS_DOWN) + if port.status != new_status: + port.status = new_status + entry = {'device': device, + 'network_id': port.network_id, + 'port_id': port.id, + 'admin_state_up': port.admin_state_up, + 'network_type': segment[api.NETWORK_TYPE], + 'segmentation_id': segment[api.SEGMENTATION_ID], + 'physical_network': segment[api.PHYSICAL_NETWORK]} + LOG.debug(_("Returning: %s"), entry) + return entry + + def update_device_down(self, rpc_context, **kwargs): + """Device no longer exists on agent.""" + # TODO(garyk) - live migration and port status + agent_id = kwargs.get('agent_id') + device = kwargs.get('device') + LOG.debug(_("Device %(device)s no longer exists at agent " + "%(agent_id)s"), + {'device': device, 'agent_id': agent_id}) + port_id = self._device_to_port_id(device) + + session = db_api.get_session() + with session.begin(subtransactions=True): + port = db.get_port(session, port_id) + if not port: + LOG.warning(_("Device %(device)s updated down by agent " + "%(agent_id)s not found in database"), + {'device': device, 'agent_id': agent_id}) + return {'device': device, + 'exists': False} + if port.status != q_const.PORT_STATUS_DOWN: + port.status = q_const.PORT_STATUS_DOWN + return {'device': device, + 'exists': True} + + 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 at agent %(agent_id)s"), + {'device': device, 'agent_id': agent_id}) + port_id = self._device_to_port_id(device) + + session = db_api.get_session() + with session.begin(subtransactions=True): + port = db.get_port(session, port_id) + if not port: + LOG.warning(_("Device %(device)s updated up by agent " + "%(agent_id)s not found in database"), + {'device': device, 'agent_id': agent_id}) + if port.status != q_const.PORT_STATUS_ACTIVE: + port.status = q_const.PORT_STATUS_ACTIVE + + # TODO(rkukura) Add tunnel_sync() here if not implemented via a + # driver. + + +class AgentNotifierApi(proxy.RpcProxy, + sg_rpc.SecurityGroupAgentRpcApiMixin): + """Agent side of the openvswitch 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) + + # TODO(rkukura): Add topic_tunnel_update here if not + # implemented via a driver. + + def network_delete(self, context, network_id): + self.fanout_cast(context, + self.make_msg('network_delete', + network_id=network_id), + topic=self.topic_network_delete) + + def port_update(self, context, port, network_type, segmentation_id, + physical_network): + self.fanout_cast(context, + self.make_msg('port_update', + port=port, + network_type=network_type, + segmentation_id=segmentation_id, + physical_network=physical_network), + topic=self.topic_port_update) + + # TODO(rkukura): Add tunnel_update() here if not + # implemented via a driver. diff --git a/quantum/tests/unit/ml2/__init__.py b/quantum/tests/unit/ml2/__init__.py new file mode 100644 index 0000000000..788cea1f70 --- /dev/null +++ b/quantum/tests/unit/ml2/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2013 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/quantum/tests/unit/ml2/test_agent_scheduler.py b/quantum/tests/unit/ml2/test_agent_scheduler.py new file mode 100644 index 0000000000..60f7f47648 --- /dev/null +++ b/quantum/tests/unit/ml2/test_agent_scheduler.py @@ -0,0 +1,32 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from quantum.tests.unit.ml2 import test_ml2_plugin +from quantum.tests.unit.openvswitch import test_agent_scheduler + + +class Ml2AgentSchedulerTestCase( + test_agent_scheduler.OvsAgentSchedulerTestCase): + plugin_str = test_ml2_plugin.PLUGIN_NAME + + +class Ml2L3AgentNotifierTestCase( + test_agent_scheduler.OvsL3AgentNotifierTestCase): + plugin_str = test_ml2_plugin.PLUGIN_NAME + + +class Ml2DhcpAgentNotifierTestCase( + test_agent_scheduler.OvsDhcpAgentNotifierTestCase): + plugin_str = test_ml2_plugin.PLUGIN_NAME diff --git a/quantum/tests/unit/ml2/test_ml2_plugin.py b/quantum/tests/unit/ml2/test_ml2_plugin.py new file mode 100644 index 0000000000..0c832e84ea --- /dev/null +++ b/quantum/tests/unit/ml2/test_ml2_plugin.py @@ -0,0 +1,63 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +from quantum.tests.unit import _test_extension_portbindings as test_bindings +from quantum.tests.unit import test_db_plugin as test_plugin + + +PLUGIN_NAME = 'quantum.plugins.ml2.plugin.Ml2Plugin' + + +class Ml2PluginV2TestCase(test_plugin.QuantumDbPluginV2TestCase): + + _plugin_name = PLUGIN_NAME + + def setUp(self): + super(Ml2PluginV2TestCase, self).setUp(PLUGIN_NAME) + self.port_create_status = 'DOWN' + + +class TestMl2BasicGet(test_plugin.TestBasicGet, + Ml2PluginV2TestCase): + pass + + +class TestMl2V2HTTPResponse(test_plugin.TestV2HTTPResponse, + Ml2PluginV2TestCase): + pass + + +class TestMl2NetworksV2(test_plugin.TestNetworksV2, + Ml2PluginV2TestCase): + pass + + +class TestMl2PortsV2(test_plugin.TestPortsV2, Ml2PluginV2TestCase): + + def test_update_port_status_build(self): + with self.port() as port: + self.assertEqual(port['port']['status'], 'DOWN') + self.assertEqual(self.port_create_status, 'DOWN') + + +# TODO(rkukura) add TestMl2PortBinding + + +# TODO(rkukura) add TestMl2PortBindingNoSG + + +class TestMl2PortBindingHost(Ml2PluginV2TestCase, + test_bindings.PortBindingsHostTestCaseMixin): + pass diff --git a/quantum/tests/unit/ml2/test_rpcapi.py b/quantum/tests/unit/ml2/test_rpcapi.py new file mode 100644 index 0000000000..2deff04d17 --- /dev/null +++ b/quantum/tests/unit/ml2/test_rpcapi.py @@ -0,0 +1,108 @@ +# Copyright (c) 2013 OpenStack Foundation +# 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. + +""" +Unit Tests for ml2 rpc +""" + +import mock + +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.ml2 import rpc as plugin_rpc +from quantum.tests import base + + +class RpcApiTestCase(base.BaseTestCase): + + def _test_rpc_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 + + rpc_method_mock = mock.Mock() + rpc_method_mock.return_value = expected_retval + setattr(rpc, rpc_method, rpc_method_mock) + + retval = getattr(rpcapi, method)(ctxt, **kwargs) + + self.assertEqual(retval, expected_retval) + + expected_args = [ctxt, topic, expected_msg] + for arg, expected_arg in zip(rpc_method_mock.call_args[0], + expected_args): + self.assertEqual(arg, expected_arg) + + def test_delete_network(self): + rpcapi = plugin_rpc.AgentNotifierApi(topics.AGENT) + self._test_rpc_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 = plugin_rpc.AgentNotifierApi(topics.AGENT) + self._test_rpc_api(rpcapi, + topics.get_topic_name(topics.AGENT, + topics.PORT, + topics.UPDATE), + 'port_update', rpc_method='fanout_cast', + port='fake_port', + network_type='fake_network_type', + segmentation_id='fake_segmentation_id', + physical_network='fake_physical_network') + + # def test_tunnel_update(self): + # rpcapi = plugin_rpc.AgentNotifierApi(topics.AGENT) + # self._test_rpc_api(rpcapi, + # topics.get_topic_name(topics.AGENT, + # constants.TUNNEL, + # topics.UPDATE), + # 'tunnel_update', rpc_method='fanout_cast', + # tunnel_ip='fake_ip', tunnel_id='fake_id') + + def test_device_details(self): + rpcapi = agent_rpc.PluginApi(topics.PLUGIN) + self._test_rpc_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_rpc_api(rpcapi, topics.PLUGIN, + 'update_device_down', rpc_method='call', + device='fake_device', + agent_id='fake_agent_id') + + # def test_tunnel_sync(self): + # rpcapi = agent_rpc.PluginApi(topics.PLUGIN) + # self._test_rpc_api(rpcapi, topics.PLUGIN, + # 'tunnel_sync', rpc_method='call', + # tunnel_ip='fake_tunnel_ip') + + def test_update_device_up(self): + rpcapi = agent_rpc.PluginApi(topics.PLUGIN) + self._test_rpc_api(rpcapi, topics.PLUGIN, + 'update_device_up', rpc_method='call', + device='fake_device', + agent_id='fake_agent_id') diff --git a/quantum/tests/unit/ml2/test_security_group.py b/quantum/tests/unit/ml2/test_security_group.py new file mode 100644 index 0000000000..219eeaebb9 --- /dev/null +++ b/quantum/tests/unit/ml2/test_security_group.py @@ -0,0 +1,89 @@ +# Copyright (c) 2013 OpenStack Foundation +# Copyright 2013, Nachi Ueno, NTT MCL, 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. + +import mock + +from quantum.api.v2 import attributes +from quantum.extensions import securitygroup as ext_sg +from quantum import manager +from quantum.tests.unit import test_extension_security_group as test_sg +from quantum.tests.unit import test_security_groups_rpc as test_sg_rpc + +PLUGIN_NAME = 'quantum.plugins.ml2.plugin.Ml2Plugin' +NOTIFIER = 'quantum.plugins.ml2.rpc.AgentNotifierApi' + + +class Ml2SecurityGroupsTestCase(test_sg.SecurityGroupDBTestCase): + _plugin_name = PLUGIN_NAME + + def setUp(self, plugin=None): + test_sg_rpc.set_firewall_driver(test_sg_rpc.FIREWALL_HYBRID_DRIVER) + self.addCleanup(mock.patch.stopall) + notifier_p = mock.patch(NOTIFIER) + notifier_cls = notifier_p.start() + self.notifier = mock.Mock() + notifier_cls.return_value = self.notifier + self._attribute_map_bk_ = {} + for item in attributes.RESOURCE_ATTRIBUTE_MAP: + self._attribute_map_bk_[item] = (attributes. + RESOURCE_ATTRIBUTE_MAP[item]. + copy()) + super(Ml2SecurityGroupsTestCase, self).setUp(PLUGIN_NAME) + + def tearDown(self): + super(Ml2SecurityGroupsTestCase, self).tearDown() + attributes.RESOURCE_ATTRIBUTE_MAP = self._attribute_map_bk_ + + +class TestMl2SecurityGroups(Ml2SecurityGroupsTestCase, + test_sg.TestSecurityGroups, + test_sg_rpc.SGNotificationTestMixin): + def test_security_group_get_port_from_device(self): + with self.network() as n: + with self.subnet(n): + with self.security_group() as sg: + security_group_id = sg['security_group']['id'] + res = self._create_port(self.fmt, n['network']['id']) + port = self.deserialize(self.fmt, res) + fixed_ips = port['port']['fixed_ips'] + data = {'port': {'fixed_ips': fixed_ips, + 'name': port['port']['name'], + ext_sg.SECURITYGROUPS: + [security_group_id]}} + + req = self.new_update_request('ports', data, + port['port']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.api)) + port_id = res['port']['id'] + plugin = manager.QuantumManager.get_plugin() + port_dict = plugin.callbacks.get_port_from_device(port_id) + self.assertEqual(port_id, port_dict['id']) + self.assertEqual([security_group_id], + port_dict[ext_sg.SECURITYGROUPS]) + self.assertEqual([], port_dict['security_group_rules']) + self.assertEqual([fixed_ips[0]['ip_address']], + port_dict['fixed_ips']) + self._delete('ports', port_id) + + def test_security_group_get_port_from_device_with_no_port(self): + plugin = manager.QuantumManager.get_plugin() + port_dict = plugin.callbacks.get_port_from_device('bad_device_id') + self.assertEqual(None, port_dict) + + +class TestMl2SecurityGroupsXML(TestMl2SecurityGroups): + fmt = 'xml' diff --git a/setup.cfg b/setup.cfg index 187b36800d..9a96382e20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,7 @@ data_files = etc/quantum/plugins/linuxbridge = etc/quantum/plugins/linuxbridge/linuxbridge_conf.ini etc/quantum/plugins/metaplugin = etc/quantum/plugins/metaplugin/metaplugin.ini etc/quantum/plugins/midonet = etc/quantum/plugins/midonet/midonet.ini + etc/quantum/plugins/ml2 = etc/quantum/plugins/ml2/ml2_conf.ini etc/quantum/plugins/mlnx = etc/quantum/plugins/mlnx/mlnx_conf.ini etc/quantum/plugins/nec = etc/quantum/plugins/nec/nec.ini etc/quantum/plugins/nicira = etc/quantum/plugins/nicira/nvp.ini @@ -85,6 +86,10 @@ console_scripts = quantum-ovs-cleanup = quantum.agent.ovs_cleanup_util:main quantum-ryu-agent = quantum.plugins.ryu.agent.ryu_quantum_agent:main quantum-server = quantum.server:main +quantum.ml2.type_drivers = + flat = quantum.plugins.ml2.drivers.type_flat:FlatTypeDriver + local = quantum.plugins.ml2.drivers.type_local:LocalTypeDriver + vlan = quantum.plugins.ml2.drivers.type_vlan:VlanTypeDriver [build_sphinx] all_files = 1 diff --git a/tools/pip-requires b/tools/pip-requires index 6bd03e946e..56079fbc7e 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -20,6 +20,7 @@ python-keystoneclient>=0.2.0 alembic>=0.4.1 oslo.config>=1.1.0 six +stevedore>=0.7 # Cisco plugin dependencies python-novaclient