From c60f22384c4eb9fa97e81e4e620b271d50e55a2b Mon Sep 17 00:00:00 2001 From: Roey Chen Date: Thu, 18 Jun 2015 02:33:36 -0700 Subject: [PATCH] Extending security-group ingress rule This adds an extension to the security-group API, using this extension will allow a user to define rules with the notation of local-prefix-ip, which matches on the destination address of packets going into the port. One may use this extended API in order to specify a specific set of multicast groups addresses in which a port (or group of ports) should be allowed to accept packets from. Change-Id: I9756cb27395b7b936dbfa94f403d98ac43c2e872 --- vmware_nsx/db/extended_security_group_rule.py | 67 ++++++++++ .../alembic_migrations/versions/EXPAND_HEAD | 2 +- .../4c45bcadccf9_extend_secgroup_rule.py | 38 ++++++ vmware_nsx/db/nsxv_models.py | 21 +++ .../secgroup_rule_local_ip_prefix.py | 63 +++++++++ vmware_nsx/plugins/nsx_v/plugin.py | 19 ++- .../test_secgroup_rule_local_ip_prefix.py | 124 ++++++++++++++++++ vmware_nsx/tests/unit/nsx_v/test_plugin.py | 13 +- 8 files changed, 335 insertions(+), 12 deletions(-) create mode 100644 vmware_nsx/db/extended_security_group_rule.py create mode 100644 vmware_nsx/db/migration/alembic_migrations/versions/mitaka/expand/4c45bcadccf9_extend_secgroup_rule.py create mode 100644 vmware_nsx/extensions/secgroup_rule_local_ip_prefix.py create mode 100644 vmware_nsx/tests/unit/extensions/test_secgroup_rule_local_ip_prefix.py diff --git a/vmware_nsx/db/extended_security_group_rule.py b/vmware_nsx/db/extended_security_group_rule.py new file mode 100644 index 0000000000..970ab13dfe --- /dev/null +++ b/vmware_nsx/db/extended_security_group_rule.py @@ -0,0 +1,67 @@ +# Copyright 2016 VMware, 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. +from sqlalchemy.orm import exc + +from neutron.api.v2 import attributes as attr +from neutron.db import securitygroups_db as secgroup_db +from neutron_lib import exceptions as nexception +from vmware_nsx._i18n import _ +from vmware_nsx.db import nsxv_models +from vmware_nsx.extensions import secgroup_rule_local_ip_prefix as ext_loip + + +class NotIngressRule(nexception.BadRequest): + message = _("Specifying local_ip_prefix is supported " + "with ingress rules only.") + + +class ExtendedSecurityGroupRuleMixin(object): + + def _check_local_ip_prefix(self, context, rule): + rule_specify_local_ip_prefix = attr.is_attr_set( + rule.get(ext_loip.LOCAL_IP_PREFIX)) + if rule_specify_local_ip_prefix and rule['direction'] != 'ingress': + raise NotIngressRule() + return rule_specify_local_ip_prefix + + def _save_extended_rule_properties(self, context, rule): + if not attr.is_attr_set(rule.get(ext_loip.LOCAL_IP_PREFIX)): + return + with context.session.begin(subtransactions=True): + properties = nsxv_models.NsxvExtendedSecurityGroupRuleProperties( + rule_id=rule['id'], + local_ip_prefix=rule[ext_loip.LOCAL_IP_PREFIX]) + context.session.add(properties) + + def _get_security_group_rule_properties(self, context, sgr): + try: + properties = (context.session.query( + nsxv_models.NsxvExtendedSecurityGroupRuleProperties).filter_by( + rule_id=sgr['id']).one()) + except exc.NoResultFound: + sgr[ext_loip.LOCAL_IP_PREFIX] = None + else: + sgr[ext_loip.LOCAL_IP_PREFIX] = properties.local_ip_prefix + return sgr + + def _make_security_group_rule_dict(self, rule_db, fields=None): + res = secgroup_db.SecurityGroupDbMixin._make_security_group_rule_dict( + self, rule_db, fields=None) + if rule_db.ext_properties: + res[ext_loip.LOCAL_IP_PREFIX] = ( + rule_db.ext_properties.local_ip_prefix) + else: + res[ext_loip.LOCAL_IP_PREFIX] = None + return self._fields(res, fields) diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD index c46661a2eb..d85661892a 100644 --- a/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/vmware_nsx/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -20483029f1ff +4c45bcadccf9 diff --git a/vmware_nsx/db/migration/alembic_migrations/versions/mitaka/expand/4c45bcadccf9_extend_secgroup_rule.py b/vmware_nsx/db/migration/alembic_migrations/versions/mitaka/expand/4c45bcadccf9_extend_secgroup_rule.py new file mode 100644 index 0000000000..78bb1dc3d2 --- /dev/null +++ b/vmware_nsx/db/migration/alembic_migrations/versions/mitaka/expand/4c45bcadccf9_extend_secgroup_rule.py @@ -0,0 +1,38 @@ +# Copyright 2016 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. + +"""extend_secgroup_rule + +Revision ID: 4c45bcadccf9 +Revises: 20483029f1ff +Create Date: 2016-03-01 06:12:09.450116 + +""" + +# revision identifiers, used by Alembic. +revision = '4c45bcadccf9' +down_revision = '20483029f1ff' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'nsxv_extended_security_group_rule_properties', + sa.Column('rule_id', sa.String(36), nullable=False), + sa.Column('local_ip_prefix', sa.String(255), nullable=False), + sa.ForeignKeyConstraint(['rule_id'], ['securitygrouprules.id'], + ondelete='CASCADE'), + sa.PrimaryKeyConstraint('rule_id')) diff --git a/vmware_nsx/db/nsxv_models.py b/vmware_nsx/db/nsxv_models.py index 2d4b432d86..29153bea35 100644 --- a/vmware_nsx/db/nsxv_models.py +++ b/vmware_nsx/db/nsxv_models.py @@ -21,6 +21,7 @@ from sqlalchemy import orm from neutron.db import l3_db from neutron.db import model_base from neutron.db import models_v2 +from neutron.db import securitygroups_db from vmware_nsx.common import nsxv_constants @@ -327,3 +328,23 @@ class NsxvSubnetExtAttributes(model_base.BASEV2): models_v2.Subnet, backref=orm.backref("nsxv_subnet_attributes", lazy='joined', uselist=False, cascade='delete')) + + +class NsxvExtendedSecurityGroupRuleProperties(model_base.BASEV2): + """Persist security group rule properties for the + extended-security-group-rule extension. + """ + + __tablename__ = 'nsxv_extended_security_group_rule_properties' + + rule_id = sa.Column(sa.String(36), + sa.ForeignKey('securitygrouprules.id', + ondelete='CASCADE'), + primary_key=True, + nullable=False) + local_ip_prefix = sa.Column(sa.String(255), nullable=False) + + rule = orm.relationship( + securitygroups_db.SecurityGroupRule, + backref=orm.backref('ext_properties', lazy='joined', + uselist=False, cascade='delete')) diff --git a/vmware_nsx/extensions/secgroup_rule_local_ip_prefix.py b/vmware_nsx/extensions/secgroup_rule_local_ip_prefix.py new file mode 100644 index 0000000000..e0dad65f05 --- /dev/null +++ b/vmware_nsx/extensions/secgroup_rule_local_ip_prefix.py @@ -0,0 +1,63 @@ +# Copyright 2016 VMware, 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. + +from neutron.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.extensions import securitygroup + +LOCAL_IP_PREFIX = 'local_ip_prefix' + +RESOURCE_ATTRIBUTE_MAP = { + 'security_group_rules': { + LOCAL_IP_PREFIX: { + 'allow_post': True, + 'allow_put': False, + 'convert_to': securitygroup.convert_ip_prefix_to_cidr, + 'default': attr.ATTR_NOT_SPECIFIED, + 'enforce_policy': True, + 'is_visible': True} + } +} + + +class Secgroup_rule_local_ip_prefix(extensions.ExtensionDescriptor): + """Extension class to add support for specifying local-ip-prefix in a + security-group rule. + """ + + @classmethod + def get_name(cls): + return "Security Group rule local ip prefix" + + @classmethod + def get_alias(cls): + return "secgroup-rule-local-ip-prefix" + + @classmethod + def get_description(cls): + return ("Enable to specify the 'local-ip-prefix' when creating a " + "security-group rule.") + + @classmethod + def get_updated(cls): + return "2016-03-01T10:00:00-00:00" + + def get_required_extensions(self): + return ["security-group"] + + def get_extended_resources(self, version): + if version == "2.0": + return RESOURCE_ATTRIBUTE_MAP + else: + return {} diff --git a/vmware_nsx/plugins/nsx_v/plugin.py b/vmware_nsx/plugins/nsx_v/plugin.py index c0a70fa834..ef7d9fc335 100644 --- a/vmware_nsx/plugins/nsx_v/plugin.py +++ b/vmware_nsx/plugins/nsx_v/plugin.py @@ -63,6 +63,8 @@ from vmware_nsx.common import locking from vmware_nsx.common import nsx_constants from vmware_nsx.common import nsxv_constants from vmware_nsx.common import utils as c_utils +from vmware_nsx.db import ( + extended_security_group_rule as extend_sg_rule) from vmware_nsx.db import ( routertype as rt_rtr) from vmware_nsx.db import db as nsx_db @@ -74,6 +76,7 @@ from vmware_nsx.extensions import ( vnicindex as ext_vnic_idx) from vmware_nsx.extensions import dns_search_domain as ext_dns_search_domain from vmware_nsx.extensions import routersize +from vmware_nsx.extensions import secgroup_rule_local_ip_prefix as ext_loip from vmware_nsx.plugins.nsx_v import managers from vmware_nsx.plugins.nsx_v import md_proxy as nsx_v_md_proxy from vmware_nsx.plugins.nsx_v.vshield.common import ( @@ -99,6 +102,7 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, l3_gwmode_db.L3_NAT_db_mixin, portbindings_db.PortBindingMixin, portsecurity_db.PortSecurityDbMixin, + extend_sg_rule.ExtendedSecurityGroupRuleMixin, securitygroups_db.SecurityGroupDbMixin, vnic_index_db.VnicIndexDbMixin, dns_db.DNSDbMixin): @@ -117,6 +121,7 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, "extraroute", "router", "security-group", + "secgroup-rule-local-ip-prefix", "nsxv-router-type", "nsxv-router-size", "vnic-index", @@ -2029,9 +2034,12 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, # Get source and destination containers from rule if rule['direction'] == 'ingress': + if rule.get(ext_loip.LOCAL_IP_PREFIX): + dest = self.nsx_sg_utils.get_remote_container( + None, rule[ext_loip.LOCAL_IP_PREFIX]) src = self.nsx_sg_utils.get_remote_container( remote_nsx_sg_id, rule['remote_ip_prefix']) - dest = self.nsx_sg_utils.get_container(nsx_sg_id) + dest = dest or self.nsx_sg_utils.get_container(nsx_sg_id) flags['direction'] = 'in' else: dest = self.nsx_sg_utils.get_remote_container( @@ -2075,13 +2083,16 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, :param security_group_rules: list of rules to create """ + sg_rules = security_group_rules['security_group_rules'] ruleids = set() nsx_rules = [] self._validate_security_group_rules(context, security_group_rules) # Translating Neutron rules to Nsx DFW rules - for r in security_group_rules['security_group_rules']: + for r in sg_rules: rule = r['security_group_rule'] + if not self._check_local_ip_prefix(context, rule): + rule[ext_loip.LOCAL_IP_PREFIX] = None rule['id'] = uuidutils.generate_uuid() ruleids.add(rule['id']) nsx_rules.append(self._create_nsx_rule(context, rule)) @@ -2110,6 +2121,10 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin, if neutron_rule_id in ruleids: nsxv_db.add_neutron_nsx_rule_mapping( context.session, neutron_rule_id, nsx_rule_id) + for i, r in enumerate(sg_rules): + self._save_extended_rule_properties(context, rule) + self._get_security_group_rule_properties(context, + new_rule_list[i]) except Exception: with excutils.save_and_reraise_exception(): for nsx_rule_id in [p['nsx_id'] for p in rule_pairs]: diff --git a/vmware_nsx/tests/unit/extensions/test_secgroup_rule_local_ip_prefix.py b/vmware_nsx/tests/unit/extensions/test_secgroup_rule_local_ip_prefix.py new file mode 100644 index 0000000000..e080f442d9 --- /dev/null +++ b/vmware_nsx/tests/unit/extensions/test_secgroup_rule_local_ip_prefix.py @@ -0,0 +1,124 @@ +# Copyright 2015 VMware, 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 +import webob.exc + +from oslo_utils import uuidutils + +from neutron.api.v2 import attributes +from neutron.db import db_base_plugin_v2 +from neutron.db import securitygroups_db +from neutron import manager +from neutron.tests.unit.extensions import test_securitygroup +from neutron_lib import constants as const + +from vmware_nsx.db import extended_security_group_rule as ext_rule_db +from vmware_nsx.extensions import secgroup_rule_local_ip_prefix as ext_loip +from vmware_nsx.plugins.nsx_v.vshield import securitygroup_utils +from vmware_nsx.tests.unit.nsx_v import test_plugin as test_nsxv_plugin + + +PLUGIN_NAME = ('vmware_nsx.tests.unit.extensions.' + 'test_secgroup_rule_local_ip_prefix.ExtendedRuleTestPlugin') + +_uuid = uuidutils.generate_uuid + + +class ExtendedRuleTestPlugin(db_base_plugin_v2.NeutronDbPluginV2, + ext_rule_db.ExtendedSecurityGroupRuleMixin, + securitygroups_db.SecurityGroupDbMixin): + + supported_extension_aliases = ["security-group", + "secgroup-rule-local-ip-prefix"] + + def create_security_group_rule(self, context, security_group_rule): + rule = security_group_rule['security_group_rule'] + rule['id'] = _uuid() + self._check_local_ip_prefix(context, rule) + with context.session.begin(subtransactions=True): + res = super(ExtendedRuleTestPlugin, + self).create_security_group_rule( + context, security_group_rule) + self._save_extended_rule_properties(context, rule) + self._get_security_group_rule_properties(context, res) + return res + + +class LocalIPPrefixExtTestCase(test_securitygroup.SecurityGroupDBTestCase): + def setUp(self, plugin=PLUGIN_NAME, ext_mgr=None): + super(LocalIPPrefixExtTestCase, self).setUp( + plugin=plugin, ext_mgr=ext_mgr) + attributes.RESOURCE_ATTRIBUTE_MAP['security_group_rules'].update( + ext_loip.RESOURCE_ATTRIBUTE_MAP['security_group_rules']) + + def tearDown(self): + # Remove attributes which were written to global attr map, they may + # interfer with tests for other plugins which doesn't support this + # extension. + del attributes.RESOURCE_ATTRIBUTE_MAP[ + 'security_group_rules']['local_ip_prefix'] + super(LocalIPPrefixExtTestCase, self).tearDown() + + def _build_ingress_rule_with_local_ip_prefix(self, security_group_id, + local_ip_prefix, + remote_ip_prefix, + direction='ingress'): + rule = self._build_security_group_rule( + security_group_id, remote_ip_prefix=remote_ip_prefix, + direction=direction, proto=const.PROTO_NAME_UDP) + rule['security_group_rule']['local_ip_prefix'] = local_ip_prefix + return rule + + def test_raise_rule_not_ingress_when_local_ip_specified(self): + local_ip_prefix = '239.255.0.0/16' + remote_ip_prefix = '10.0.0.0/24' + with self.security_group() as sg: + rule = self._build_ingress_rule_with_local_ip_prefix( + sg['security_group']['id'], local_ip_prefix, + remote_ip_prefix, direction='egress') + res = self._create_security_group_rule(self.fmt, rule) + self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int) + + def test_create_rule_with_local_ip_prefix(self): + local_ip_prefix = '239.255.0.0/16' + remote_ip_prefix = '10.0.0.0/24' + with self.security_group() as sg: + rule = self._build_ingress_rule_with_local_ip_prefix( + sg['security_group']['id'], local_ip_prefix, remote_ip_prefix) + res = self._make_security_group_rule(self.fmt, rule) + self.assertEqual(local_ip_prefix, + res['security_group_rule']['local_ip_prefix']) + + +class TestNsxVExtendedSGRule(test_nsxv_plugin.NsxVSecurityGroupsTestCase, + LocalIPPrefixExtTestCase): + def test_create_rule_with_local_ip_prefix(self): + sg_utils = securitygroup_utils.NsxSecurityGroupUtils(None) + local_ip_prefix = '239.255.0.0/16' + plugin = manager.NeutronManager.get_plugin() + dest = {'type': 'Ipv4Address', 'value': local_ip_prefix} + + def _assert_destination_as_expected(*args, **kwargs): + self.assertEqual(dest, kwargs['destination']) + return sg_utils.get_rule_config(*args, **kwargs) + + plugin.nsx_sg_utils.get_rule_config = mock.Mock( + side_effect=sg_utils.get_rule_config) + super(TestNsxVExtendedSGRule, + self).test_create_rule_with_local_ip_prefix() + plugin.nsx_sg_utils.get_rule_config.assert_called_with( + destination=dest, applied_to_ids=mock.ANY, name=mock.ANY, + services=mock.ANY, source=mock.ANY, flags=mock.ANY) diff --git a/vmware_nsx/tests/unit/nsx_v/test_plugin.py b/vmware_nsx/tests/unit/nsx_v/test_plugin.py index 5e9720a1f2..c9ee8f30a0 100644 --- a/vmware_nsx/tests/unit/nsx_v/test_plugin.py +++ b/vmware_nsx/tests/unit/nsx_v/test_plugin.py @@ -30,7 +30,6 @@ from neutron.extensions import portsecurity as psec from neutron.extensions import providernet as pnet from neutron.extensions import securitygroup as secgrp from neutron import manager - from neutron.tests.unit import _test_extension_portbindings as test_bindings import neutron.tests.unit.db.test_allowedaddresspairs_db as test_addr_pair import neutron.tests.unit.db.test_db_base_plugin_v2 as test_plugin @@ -49,14 +48,10 @@ from vmware_nsx._i18n import _ from vmware_nsx.common import exceptions as nsxv_exc from vmware_nsx.common import nsx_constants from vmware_nsx.db import nsxv_db -from vmware_nsx.extensions import ( - routersize as router_size) -from vmware_nsx.extensions import ( - routertype as router_type) -from vmware_nsx.extensions import ( - vnicindex as ext_vnic_idx) -from vmware_nsx.plugins.nsx_v.vshield.common import ( - constants as vcns_const) +from vmware_nsx.extensions import routersize as router_size +from vmware_nsx.extensions import routertype as router_type +from vmware_nsx.extensions import vnicindex as ext_vnic_idx +from vmware_nsx.plugins.nsx_v.vshield.common import constants as vcns_const from vmware_nsx.plugins.nsx_v.vshield import edge_utils from vmware_nsx.tests import unit as vmware from vmware_nsx.tests.unit.extensions import test_vnic_index