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
This commit is contained in:
Bob Kukura 2012-10-05 11:50:39 -04:00 committed by Mark McClain
parent 010bfa6234
commit 546151399d
25 changed files with 2068 additions and 0 deletions

View File

@ -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 <physical_network>[:<vlan_min>:<vlan_max>] 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

View File

@ -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 ###

View File

@ -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'],
]

View File

@ -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'

View File

@ -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},

View File

@ -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

View File

@ -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.

View File

@ -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)

100
quantum/plugins/ml2/db.py Normal file
View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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 <physical_network>:<vlan_min>:<vlan_max> or "
"<physical_network> 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})

View File

@ -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

View File

@ -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)

View File

@ -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)

203
quantum/plugins/ml2/rpc.py Normal file
View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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'

View File

@ -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

View File

@ -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