From 7c53f92d95c7a5f48a2fb5004bc5a2d0d40f3d2e Mon Sep 17 00:00:00 2001 From: Irena Berezovsky Date: Mon, 10 Feb 2014 14:55:49 +0200 Subject: [PATCH] Add support to request vnic type on port This patch adds support for requested vnic_type to be plugged to neutron port to ML2 plugin. This patch contains: 1. New attribute 'binding:vnic_type' added to port binding extension. Possible values are 'direct', 'macvtap' and 'normal'. 'binding:vnic_type' is allowed to be defined on port creation or changed on port update by admin or tenant user. 'binding:vnic_type' can be also skipped in port defintion 2. Management of vnic_type by ML2 plugin, assuming default vnic_type=normal 3. Add 'vnic_type' to ml2_port_bindings DB table 4. Add supported vnic_types for MechanismDrivers that are capable to bind port. 5. Add DB migration script for ml2_vnic_type. DocImpact: Need to update portbindings API docs and include in SR-IOV user docs Change-Id: Ic88708fa9ece742f807c1d09bb49e499f99bd092 Implements: blueprint ml2-request-vnic-type --- etc/policy.json | 3 + .../versions/27cc183af192_ml2_vnic_type.py | 55 ++++++++++++ neutron/db/portbindings_db.py | 10 +++ neutron/extensions/portbindings.py | 12 ++- neutron/plugins/ml2/drivers/mech_agent.py | 11 ++- neutron/plugins/ml2/managers.py | 8 +- neutron/plugins/ml2/models.py | 3 + neutron/plugins/ml2/plugin.py | 14 ++- .../unit/_test_extension_portbindings.py | 87 +++++++++++++++++++ neutron/tests/unit/ml2/_test_mech_agent.py | 2 + neutron/tests/unit/ml2/test_ml2_plugin.py | 5 ++ 11 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/27cc183af192_ml2_vnic_type.py diff --git a/etc/policy.json b/etc/policy.json index a72d3a93d1..bd0bc9274c 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -47,6 +47,7 @@ "create_port:port_security_enabled": "rule:admin_or_network_owner", "create_port:binding:host_id": "rule:admin_only", "create_port:binding:profile": "rule:admin_only", + "create_port:binding:vnic_type": "rule:admin_or_owner", "create_port:mac_learning_enabled": "rule:admin_or_network_owner", "get_port": "rule:admin_or_owner", "get_port:queue_id": "rule:admin_only", @@ -54,11 +55,13 @@ "get_port:binding:capabilities": "rule:admin_only", "get_port:binding:host_id": "rule:admin_only", "get_port:binding:profile": "rule:admin_only", + "get_port:binding:vnic_type": "rule:admin_or_owner", "update_port": "rule:admin_or_owner", "update_port:fixed_ips": "rule:admin_or_network_owner", "update_port:port_security_enabled": "rule:admin_or_network_owner", "update_port:binding:host_id": "rule:admin_only", "update_port:binding:profile": "rule:admin_only", + "update_port:binding:vnic_type": "rule:admin_or_owner", "update_port:mac_learning_enabled": "rule:admin_or_network_owner", "delete_port": "rule:admin_or_owner", diff --git a/neutron/db/migration/alembic_migrations/versions/27cc183af192_ml2_vnic_type.py b/neutron/db/migration/alembic_migrations/versions/27cc183af192_ml2_vnic_type.py new file mode 100644 index 0000000000..00cf029ada --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/27cc183af192_ml2_vnic_type.py @@ -0,0 +1,55 @@ +# Copyright 2014 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_vnic_type + +Revision ID: 27cc183af192 +Revises: 4ca36cfc898c +Create Date: 2014-02-09 12:19:21.362967 + +""" + +# revision identifiers, used by Alembic. +revision = '27cc183af192' +down_revision = '4ca36cfc898c' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = [ + 'neutron.plugins.ml2.plugin.Ml2Plugin' +] + +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration +from neutron.extensions import portbindings + + +def upgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.add_column('ml2_port_bindings', + sa.Column('vnic_type', sa.String(length=64), + nullable=False, + server_default=portbindings.VNIC_NORMAL)) + + +def downgrade(active_plugins=None, options=None): + if not migration.should_run(active_plugins, migration_for_plugins): + return + + op.drop_column('ml2_port_bindings', 'vnic_type') diff --git a/neutron/db/portbindings_db.py b/neutron/db/portbindings_db.py index c7ab4d37e2..9eacaedf86 100644 --- a/neutron/db/portbindings_db.py +++ b/neutron/db/portbindings_db.py @@ -71,6 +71,16 @@ class PortBindingMixin(portbindings_base.PortBindingBaseMixin): binding_profile_set = attributes.is_attr_set(binding_profile) if not binding_profile_set and binding_profile is not None: del port[portbindings.PROFILE] + + binding_vnic = port.get(portbindings.VNIC_TYPE) + binding_vnic_set = attributes.is_attr_set(binding_vnic) + if not binding_vnic_set and binding_vnic is not None: + del port[portbindings.VNIC_TYPE] + # REVISIT(irenab) Add support for vnic_type for plugins that + # can handle more than one type. + # Currently implemented for ML2 plugin that does not use + # PortBindingMixin. + host = port_data.get(portbindings.HOST_ID) host_set = attributes.is_attr_set(host) with context.session.begin(subtransactions=True): diff --git a/neutron/extensions/portbindings.py b/neutron/extensions/portbindings.py index dbef59289c..4ed38fc239 100644 --- a/neutron/extensions/portbindings.py +++ b/neutron/extensions/portbindings.py @@ -18,7 +18,8 @@ from neutron.api import extensions from neutron.api.v2 import attributes - +# The type of vnic that this port should be attached to +VNIC_TYPE = 'binding:vnic_type' # The service will return the vif type for the specific port. VIF_TYPE = 'binding:vif_type' # In some cases different implementations may be run on different hosts. @@ -51,6 +52,10 @@ VIF_TYPES = [VIF_TYPE_UNBOUND, VIF_TYPE_BINDING_FAILED, VIF_TYPE_OVS, VIF_TYPE_802_QBH, VIF_TYPE_HYPERV, VIF_TYPE_MIDONET, VIF_TYPE_OTHER] +VNIC_NORMAL = 'normal' +VNIC_DIRECT = 'direct' +VNIC_MACVTAP = 'macvtap' +VNIC_TYPES = [VNIC_NORMAL, VNIC_DIRECT, VNIC_MACVTAP] EXTENDED_ATTRIBUTES_2_0 = { 'ports': { @@ -58,6 +63,11 @@ EXTENDED_ATTRIBUTES_2_0 = { 'default': attributes.ATTR_NOT_SPECIFIED, 'enforce_policy': True, 'is_visible': True}, + VNIC_TYPE: {'allow_post': True, 'allow_put': True, + 'default': VNIC_NORMAL, + 'is_visible': True, + 'validate': {'type:values': VNIC_TYPES}, + 'enforce_policy': True}, HOST_ID: {'allow_post': True, 'allow_put': True, 'default': attributes.ATTR_NOT_SPECIFIED, 'is_visible': True, diff --git a/neutron/plugins/ml2/drivers/mech_agent.py b/neutron/plugins/ml2/drivers/mech_agent.py index 7bf21a4975..2081057dad 100644 --- a/neutron/plugins/ml2/drivers/mech_agent.py +++ b/neutron/plugins/ml2/drivers/mech_agent.py @@ -17,6 +17,7 @@ from abc import ABCMeta, abstractmethod import six +from neutron.extensions import portbindings from neutron.openstack.common import log from neutron.plugins.ml2 import driver_api as api @@ -38,7 +39,8 @@ class AgentMechanismDriverBase(api.MechanismDriver): check_segment_for_agent(). """ - def __init__(self, agent_type, vif_type, cap_port_filter): + def __init__(self, agent_type, vif_type, cap_port_filter, + supported_vnic_types=[portbindings.VNIC_NORMAL]): """Initialize base class for specific L2 agent type. :param agent_type: Constant identifying agent type in agents_db @@ -47,6 +49,7 @@ class AgentMechanismDriverBase(api.MechanismDriver): self.agent_type = agent_type self.vif_type = vif_type self.cap_port_filter = cap_port_filter + self.supported_vnic_types = supported_vnic_types def initialize(self): pass @@ -56,6 +59,12 @@ class AgentMechanismDriverBase(api.MechanismDriver): "network %(network)s"), {'port': context.current['id'], 'network': context.network.current['id']}) + vnic_type = context.current.get(portbindings.VNIC_TYPE, + portbindings.VNIC_NORMAL) + if vnic_type not in self.supported_vnic_types: + LOG.debug(_("Refusing to bind due to unsupported vnic_type: %s"), + vnic_type) + return for agent in context.host_agents(self.agent_type): LOG.debug(_("Checking agent: %s"), agent) if agent['alive']: diff --git a/neutron/plugins/ml2/managers.py b/neutron/plugins/ml2/managers.py index a60e20987b..424f9f5d09 100644 --- a/neutron/plugins/ml2/managers.py +++ b/neutron/plugins/ml2/managers.py @@ -437,15 +437,18 @@ class MechanismManager(stevedore.named.NamedExtensionManager): attempt to establish a port binding. """ binding = context._binding - LOG.debug(_("Attempting to bind port %(port)s on host %(host)s"), + LOG.debug(_("Attempting to bind port %(port)s on host %(host)s " + "for vnic_type %(vnic_type)s"), {'port': context._port['id'], - 'host': binding.host}) + 'host': binding.host, + 'vnic_type': binding.vnic_type}) for driver in self.ordered_mech_drivers: try: driver.obj.bind_port(context) if binding.segment: binding.driver = driver.name LOG.debug(_("Bound port: %(port)s, host: %(host)s, " + "vnic_type: %(vnic_type)s, " "driver: %(driver)s, vif_type: %(vif_type)s, " "cap_port_filter: %(cap_port_filter)s, " "segment: %(segment)s"), @@ -453,6 +456,7 @@ class MechanismManager(stevedore.named.NamedExtensionManager): 'host': binding.host, 'driver': binding.driver, 'vif_type': binding.vif_type, + 'vnic_type': binding.vnic_type, 'cap_port_filter': binding.cap_port_filter, 'segment': binding.segment}) return diff --git a/neutron/plugins/ml2/models.py b/neutron/plugins/ml2/models.py index 9b215be274..f17fa1cdb5 100644 --- a/neutron/plugins/ml2/models.py +++ b/neutron/plugins/ml2/models.py @@ -18,6 +18,7 @@ from sqlalchemy import orm from neutron.db import model_base from neutron.db import models_v2 +from neutron.extensions import portbindings class NetworkSegment(model_base.BASEV2, models_v2.HasId): @@ -53,6 +54,8 @@ class PortBinding(model_base.BASEV2): sa.ForeignKey('ports.id', ondelete="CASCADE"), primary_key=True) host = sa.Column(sa.String(255), nullable=False) + vnic_type = sa.Column(sa.String(64), nullable=False, + default=portbindings.VNIC_NORMAL) vif_type = sa.Column(sa.String(64), nullable=False) cap_port_filter = sa.Column(sa.Boolean, nullable=False) driver = sa.Column(sa.String(64)) diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 8ca62d7df8..dcfcade1fb 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -208,23 +208,30 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, self._update_port_dict_binding(port, binding) host = attrs and attrs.get(portbindings.HOST_ID) host_set = attributes.is_attr_set(host) + vnic_type = attrs and attrs.get(portbindings.VNIC_TYPE) + vnic_type_set = attributes.is_attr_set(vnic_type) if binding.vif_type != portbindings.VIF_TYPE_UNBOUND: - if (not host_set and binding.segment and + if (not host_set and not vnic_type_set and binding.segment and self.mechanism_manager.validate_port_binding(mech_context)): return False self.mechanism_manager.unbind_port(mech_context) self._update_port_dict_binding(port, binding) # Return True only if an agent notification is needed. - # This will happen if a new host was specified and that host + # This will happen if a new host or vnic_type was specified that # differs from the current one. Note that host_set is True # even if the host is an empty string - ret_value = host_set and binding.get('host') != host + ret_value = ((host_set and binding.get('host') != host) or + (vnic_type_set and binding.get('vnic_type') != vnic_type)) if host_set: binding.host = host port[portbindings.HOST_ID] = host + if vnic_type_set: + binding.vnic_type = vnic_type + port[portbindings.VNIC_TYPE] = vnic_type + if binding.host: self.mechanism_manager.bind_port(mech_context) self._update_port_dict_binding(port, binding) @@ -233,6 +240,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, def _update_port_dict_binding(self, port, binding): port[portbindings.HOST_ID] = binding.host + port[portbindings.VNIC_TYPE] = binding.vnic_type port[portbindings.VIF_TYPE] = binding.vif_type port[portbindings.CAPABILITIES] = { portbindings.CAP_PORT_FILTER: binding.cap_port_filter} diff --git a/neutron/tests/unit/_test_extension_portbindings.py b/neutron/tests/unit/_test_extension_portbindings.py index f362f92ead..5e52629a57 100644 --- a/neutron/tests/unit/_test_extension_portbindings.py +++ b/neutron/tests/unit/_test_extension_portbindings.py @@ -19,6 +19,7 @@ # import contextlib +import httplib from oslo.config import cfg from webob import exc @@ -279,3 +280,89 @@ class PortBindingsHostTestCaseMixin(object): self._test_list_resources( 'port', (port1, port3), query_params='%s=%s' % (portbindings.HOST_ID, self.hostname)) + + +class PortBindingsVnicTestCaseMixin(object): + fmt = 'json' + vnic_type = portbindings.VNIC_NORMAL + + def _check_response_portbindings_vnic_type(self, port): + self.assertIn('status', port) + self.assertEqual(port[portbindings.VNIC_TYPE], self.vnic_type) + + def test_port_vnic_type_non_admin(self): + with self.network(set_context=True, + tenant_id='test') as net1: + with self.subnet(network=net1) as subnet1: + vnic_arg = {portbindings.VNIC_TYPE: self.vnic_type} + with self.port(subnet=subnet1, + expected_res_status=httplib.CREATED, + arg_list=(portbindings.VNIC_TYPE,), + set_context=True, + tenant_id='test', + **vnic_arg) as port: + # Check a response of create_port + self._check_response_portbindings_vnic_type(port['port']) + + def test_port_vnic_type(self): + vnic_arg = {portbindings.VNIC_TYPE: self.vnic_type} + with self.port(name='name', arg_list=(portbindings.VNIC_TYPE,), + **vnic_arg) as port: + port_id = port['port']['id'] + # Check a response of create_port + self._check_response_portbindings_vnic_type(port['port']) + # Check a response of get_port + ctx = context.get_admin_context() + port = self._show('ports', port_id, neutron_context=ctx)['port'] + self._check_response_portbindings_vnic_type(port) + # By default user is admin - now test non admin user + ctx = context.Context(user_id=None, + tenant_id=self._tenant_id, + is_admin=False, + read_deleted="no") + non_admin_port = self._show( + 'ports', port_id, neutron_context=ctx)['port'] + self._check_response_portbindings_vnic_type(non_admin_port) + + def test_ports_vnic_type(self): + cfg.CONF.set_default('allow_overlapping_ips', True) + vnic_arg = {portbindings.VNIC_TYPE: self.vnic_type} + with contextlib.nested( + self.port(name='name1', + arg_list=(portbindings.VNIC_TYPE,), + **vnic_arg), + self.port(name='name2')): + ctx = context.get_admin_context() + ports = self._list('ports', neutron_context=ctx)['ports'] + self.assertEqual(2, len(ports)) + for port in ports: + if port['name'] == 'name1': + self._check_response_portbindings_vnic_type(port) + else: + self.assertEqual(portbindings.VNIC_NORMAL, + port[portbindings.VNIC_TYPE]) + # By default user is admin - now test non admin user + ctx = context.Context(user_id=None, + tenant_id=self._tenant_id, + is_admin=False, + read_deleted="no") + ports = self._list('ports', neutron_context=ctx)['ports'] + self.assertEqual(2, len(ports)) + for non_admin_port in ports: + self._check_response_portbindings_vnic_type(non_admin_port) + + def test_ports_vnic_type_list(self): + cfg.CONF.set_default('allow_overlapping_ips', True) + vnic_arg = {portbindings.VNIC_TYPE: self.vnic_type} + with contextlib.nested( + self.port(name='name1', + arg_list=(portbindings.VNIC_TYPE,), + **vnic_arg), + self.port(name='name2'), + self.port(name='name3', + arg_list=(portbindings.VNIC_TYPE,), + **vnic_arg),) as (port1, port2, port3): + self._test_list_resources( + 'port', (port1, port2, port3), + query_params='%s=%s' % (portbindings.VNIC_TYPE, + self.vnic_type)) diff --git a/neutron/tests/unit/ml2/_test_mech_agent.py b/neutron/tests/unit/ml2/_test_mech_agent.py index 83e772b7bf..876cb1d754 100644 --- a/neutron/tests/unit/ml2/_test_mech_agent.py +++ b/neutron/tests/unit/ml2/_test_mech_agent.py @@ -14,6 +14,7 @@ # under the License. +from neutron.extensions import portbindings from neutron.plugins.ml2 import driver_api as api from neutron.tests import base @@ -45,6 +46,7 @@ class FakePortContext(api.PortContext): self._network_context = FakeNetworkContext(segments) self._bound_segment_id = None self._bound_vif_type = None + self._bound_vnic_type = portbindings.VNIC_NORMAL self._bound_cap_port_filter = None @property diff --git a/neutron/tests/unit/ml2/test_ml2_plugin.py b/neutron/tests/unit/ml2/test_ml2_plugin.py index 1c82de6dcf..b6b1cdeba2 100644 --- a/neutron/tests/unit/ml2/test_ml2_plugin.py +++ b/neutron/tests/unit/ml2/test_ml2_plugin.py @@ -137,6 +137,11 @@ class TestMl2PortBindingHost(Ml2PluginV2TestCase, pass +class TestMl2PortBindingVnicType(Ml2PluginV2TestCase, + test_bindings.PortBindingsVnicTestCaseMixin): + pass + + class TestMultiSegmentNetworks(Ml2PluginV2TestCase): def setUp(self, plugin=None):