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