diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD index 444a940514..09890892c6 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -01a33f93f5fd +e4c503f4133f diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/e4c503f4133f_port_vnic_type_support.py b/vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/e4c503f4133f_port_vnic_type_support.py new file mode 100644 index 0000000000..731ca33587 --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/ocata/expand/e4c503f4133f_port_vnic_type_support.py @@ -0,0 +1,41 @@ +# Copyright 2017 VMware, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Port vnic_type support + +Revision ID: e4c503f4133f +Revises: 01a33f93f5fd +Create Date: 2017-02-20 00:05:30.894680 + +""" + +# revision identifiers, used by Alembic. +revision = 'e4c503f4133f' +down_revision = '01a33f93f5fd' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'nsxv_port_ext_attributes', + sa.Column('port_id', sa.String(length=36), nullable=False), + sa.Column('vnic_type', sa.String(length=64), nullable=False, + server_default='normal'), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['port_id'], ['ports.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('port_id')) diff --git a/vmware_nsx/db/nsxv_db.py b/vmware_nsx/db/nsxv_db.py index 4dfeb05db5..a359fbb3f9 100644 --- a/vmware_nsx/db/nsxv_db.py +++ b/vmware_nsx/db/nsxv_db.py @@ -18,6 +18,7 @@ import neutron.db.api as db from neutron.plugins.common import constants as neutron_const import decorator +from neutron_lib.api.definitions import portbindings as pbin from oslo_db import exception as db_exc from oslo_log import log as logging from oslo_utils import excutils @@ -870,3 +871,26 @@ def update_nsxv_subnet_ext_attributes(session, subnet_id, binding[ext_dns_search_domain.DNS_SEARCH_DOMAIN] = dns_search_domain binding[ext_dhcp_mtu.DHCP_MTU] = dhcp_mtu return binding + + +def add_nsxv_port_ext_attributes(session, port_id, + vnic_type=pbin.VNIC_NORMAL): + with session.begin(subtransactions=True): + binding = nsxv_models.NsxvPortExtAttributes( + port_id=port_id, + vnic_type=vnic_type) + session.add(binding) + return binding + + +def update_nsxv_port_ext_attributes(session, port_id, + vnic_type=pbin.VNIC_NORMAL): + try: + binding = session.query( + nsxv_models.NsxvPortExtAttributes).filter_by( + port_id=port_id).one() + binding['vnic_type'] = vnic_type + return binding + except exc.NoResultFound: + return add_nsxv_port_ext_attributes( + session, port_id, vnic_type=vnic_type) diff --git a/vmware_nsx/db/nsxv_models.py b/vmware_nsx/db/nsxv_models.py index 6c7900c692..18b53fad46 100644 --- a/vmware_nsx/db/nsxv_models.py +++ b/vmware_nsx/db/nsxv_models.py @@ -20,6 +20,7 @@ from sqlalchemy import orm from neutron.db import l3_db from neutron.db import models_v2 +from neutron.extensions import portbindings from oslo_db.sqlalchemy import models from vmware_nsx.common import nsxv_constants @@ -361,3 +362,22 @@ class NsxvSubnetExtAttributes(model_base.BASEV2, models.TimestampMixin): models_v2.Subnet, backref=orm.backref("nsxv_subnet_attributes", lazy='joined', uselist=False, cascade='delete')) + + +class NsxvPortExtAttributes(model_base.BASEV2, models.TimestampMixin): + """Port attributes managed by NSX plugin extensions.""" + + __tablename__ = 'nsxv_port_ext_attributes' + + port_id = sa.Column(sa.String(36), + sa.ForeignKey('ports.id', ondelete="CASCADE"), + primary_key=True) + vnic_type = sa.Column(sa.String(64), nullable=False, + default=portbindings.VNIC_NORMAL, + server_default=portbindings.VNIC_NORMAL) + # Add a relationship to the port model in order to instruct + # SQLAlchemy to eagerly load this association + port = orm.relationship( + models_v2.Port, + backref=orm.backref("nsx_port_attributes", lazy='joined', + uselist=False, cascade='delete')) diff --git a/vmware_nsx/plugins/nsx_v/plugin.py b/vmware_nsx/plugins/nsx_v/plugin.py index ccf25a877f..011e249e18 100644 --- a/vmware_nsx/plugins/nsx_v/plugin.py +++ b/vmware_nsx/plugins/nsx_v/plugin.py @@ -209,13 +209,6 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, neutron_extensions.append_api_extensions_path( [vmware_nsx.NSX_EXT_PATH]) - self.base_binding_dict = { - pbin.VNIC_TYPE: pbin.VNIC_NORMAL, - pbin.VIF_TYPE: nsx_constants.VIF_TYPE_DVS, - pbin.VIF_DETAILS: { - # TODO(rkukura): Replace with new VIF security details - pbin.CAP_PORT_FILTER: - 'security-group' in self.supported_extension_aliases}} # This needs to be set prior to binding callbacks if cfg.CONF.nsxv.use_dvs_features: self._vcm = dvs.VCManager() @@ -402,6 +395,8 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, with db_api.context_manager.writer.using(ctx): self._extension_manager.extend_port_dict( ctx.session, portdb, result) + self._extend_port_dict_binding(portdb, + result) def _ext_extend_subnet_dict(self, result, subnetdb): ctx = n_context.get_admin_context() @@ -687,6 +682,21 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, # TODO(salvatore-orlando): Validate tranport zone uuid # which should be specified in physical_network + def _validate_network_type(self, context, network_id, net_types): + bindings = nsxv_db.get_network_bindings(context.session, + network_id) + multiprovider = nsx_db.is_multiprovider_network(context.session, + network_id) + if bindings: + if not multiprovider: + return bindings[0].binding_type in net_types + else: + for binding in bindings: + if binding.binding_type not in net_types: + return False + return True + return False + def _extend_network_dict_provider(self, context, network, multiprovider=None, bindings=None): if not bindings: @@ -1602,6 +1612,33 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, context, port['port'], created_port) return created_port + def _process_vnic_type(self, context, port_data, attrs, + has_security_groups, port_security): + vnic_type = attrs and attrs.get(pbin.VNIC_TYPE) + if attrs and validators.is_attr_set(vnic_type): + if vnic_type == pbin.VNIC_NORMAL: + pass + elif vnic_type == pbin.VNIC_DIRECT: + if has_security_groups or port_security: + err_msg = _("Direct VNIC type requires no port " + "security and no security groups!") + raise n_exc.InvalidInput(error_message=err_msg) + if not self._validate_network_type( + context, port_data['network_id'], + [c_utils.NsxVNetworkTypes.VLAN, + c_utils.NsxVNetworkTypes.FLAT, + c_utils.NsxVNetworkTypes.PORTGROUP]): + err_msg = _("Direct VNIC type requires VLAN, Flat or " + "Portgroup network!") + raise n_exc.InvalidInput(error_message=err_msg) + else: + err_msg = _("Only direct or normal VNIC types supported") + raise n_exc.InvalidInput(error_message=err_msg) + nsxv_db.update_nsxv_port_ext_attributes( + session=context.session, + port_id=port_data['id'], + vnic_type=vnic_type) + def create_port(self, context, port): port_data = port['port'] with context.session.begin(subtransactions=True): @@ -1663,6 +1700,10 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, context, neutron_db, attrs.get(addr_pair.ADDRESS_PAIRS))) + self._process_vnic_type(context, port_data, attrs, + has_security_groups, + port_security) + try: # Configure NSX - this should not be done in the DB transaction # Configure the DHCP Edge service @@ -1781,7 +1822,6 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, self._validate_address_pairs(attrs, original_port) orig_has_port_security = (cfg.CONF.nsxv.spoofguard_enabled and original_port[psec.PORTSECURITY]) - port_ip_change = port_data.get('fixed_ips') is not None device_owner_change = port_data.get('device_owner') is not None # We do not support updating the port ip and device owner together @@ -1888,6 +1928,10 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, update_assigned_addresses = self.update_address_pairs_on_port( context, id, port, original_port, ret_port) + self._process_vnic_type(context, ret_port, attrs, + has_security_groups, + has_port_security) + if comp_owner_update: # Create dhcp bindings, the port is now owned by an instance self._create_dhcp_static_binding(context, ret_port) @@ -2110,6 +2154,18 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, self._delete_dhcp_static_binding(context, neutron_db_port) + def _extend_port_dict_binding(self, portdb, result): + result[pbin.VIF_TYPE] = nsx_constants.VIF_TYPE_DVS + port_attr = portdb.get('nsx_port_attributes') + if port_attr: + result[pbin.VNIC_TYPE] = port_attr.vnic_type + else: + result[pbin.VNIC_TYPE] = pbin.VNIC_NORMAL + result[pbin.VIF_DETAILS] = { + # TODO(rkukura): Replace with new VIF security details + pbin.CAP_PORT_FILTER: + 'security-group' in self.supported_extension_aliases} + def delete_subnet(self, context, id): subnet = self._get_subnet(context, id) filters = {'fixed_ips': {'subnet_id': [id]}} diff --git a/vmware_nsx/tests/unit/nsx_v/test_plugin.py b/vmware_nsx/tests/unit/nsx_v/test_plugin.py index 932bbd206a..ad761c97f4 100644 --- a/vmware_nsx/tests/unit/nsx_v/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v/test_plugin.py @@ -1534,6 +1534,83 @@ class TestPortsV2(NsxVPluginV2TestCase, port2 = self.deserialize('json', res) self.assertEqual("MacAddressInUse", port2['NeutronError']['type']) + def _test_create_direct_network(self, vlan_id=0): + net_type = vlan_id and 'vlan' or 'flat' + name = 'direct_net' + providernet_args = {pnet.NETWORK_TYPE: net_type, + pnet.PHYSICAL_NETWORK: 'tzuuid'} + if vlan_id: + providernet_args[pnet.SEGMENTATION_ID] = vlan_id + return self.network(name=name, + providernet_args=providernet_args, + arg_list=(pnet.NETWORK_TYPE, + pnet.PHYSICAL_NETWORK, + pnet.SEGMENTATION_ID)) + + def test_create_port_vnic_direct(self): + with self._test_create_direct_network(vlan_id=7) as network: + # Check that port security conflicts + kwargs = {'binding:vnic_type': 'direct'} + net_id = network['network']['id'] + res = self._create_port(self.fmt, net_id=net_id, + arg_list=(portbindings.VNIC_TYPE,), + **kwargs) + self.assertEqual(res.status_int, webob.exc.HTTPBadRequest.code) + + # Check that security group conflicts + kwargs = {'binding:vnic_type': 'direct', + 'security_groups': + ['4cd70774-cc67-4a87-9b39-7d1db38eb087'], + 'port_security_enabled': False} + net_id = network['network']['id'] + res = self._create_port(self.fmt, net_id=net_id, + arg_list=(portbindings.VNIC_TYPE, + 'port_security_enabled'), + **kwargs) + self.assertEqual(res.status_int, webob.exc.HTTPBadRequest.code) + + # All is kosher so we can create the port + kwargs = {'binding:vnic_type': 'direct', + 'port_security_enabled': False} + net_id = network['network']['id'] + res = self._create_port(self.fmt, net_id=net_id, + arg_list=(portbindings.VNIC_TYPE, + 'port_security_enabled'), + **kwargs) + port = self.deserialize('json', res) + self.assertEqual("direct", port['port']['binding:vnic_type']) + + def test_create_port_vnic_direct_invalid_network(self): + with self.network(name='not vlan/flat') as net: + kwargs = {'binding:vnic_type': 'direct', + 'port_security_enabled': False} + net_id = net['network']['id'] + res = self._create_port(self.fmt, net_id=net_id, + arg_list=(portbindings.VNIC_TYPE, + 'port_security_enabled'), + **kwargs) + self.assertEqual(res.status_int, webob.exc.HTTPBadRequest.code) + + def test_update_vnic_direct(self): + with self._test_create_direct_network(vlan_id=7) as network: + with self.subnet(network=network) as subnet: + with self.port(subnet=subnet) as port: + # need to do two updates as the update for port security + # disabled requires that it can only change 2 items + data = {'port': {'port_security_enabled': False, + 'security_groups': []}} + req = self.new_update_request('ports', + data, port['port']['id']) + res = self.deserialize('json', req.get_response(self.api)) + self.assertEqual('normal', + res['port']['binding:vnic_type']) + data = {'port': {'binding:vnic_type': 'direct'}} + req = self.new_update_request('ports', + data, port['port']['id']) + res = self.deserialize('json', req.get_response(self.api)) + self.assertEqual('direct', + res['port']['binding:vnic_type']) + class TestSubnetsV2(NsxVPluginV2TestCase, test_plugin.TestSubnetsV2):