From cbc473e066d835ee687c9b686ac525c8db62fdfa Mon Sep 17 00:00:00 2001 From: Aditya Reddy Nagaram Date: Wed, 6 Jun 2018 15:23:02 +0200 Subject: [PATCH] Support for stateless security groups Blueprint: stateless-security-groups Change-Id: Iae39a89b762786e4f05aa61aa0db634941806d41 --- neutron/agent/linux/iptables_firewall.py | 45 +++++++++++++++++-- neutron/agent/securitygroups_rpc.py | 2 + .../api/rpc/handlers/securitygroups_rpc.py | 5 +++ .../alembic_migrations/versions/EXPAND_HEAD | 2 +- ...8a7e90ae768_add_security_group_stateful.py | 37 +++++++++++++++ neutron/db/models/securitygroup.py | 4 ++ neutron/db/securitygroups_db.py | 44 +++++++++++++----- neutron/db/securitygroups_rpc_base.py | 18 +++++++- neutron/extensions/stateful_security_group.py | 22 +++++++++ neutron/objects/securitygroup.py | 10 ++++- neutron/plugins/ml2/plugin.py | 33 +++++--------- .../tests/contrib/hooks/api_all_extensions | 1 + .../unit/agent/test_securitygroups_rpc.py | 18 +++++--- .../tests/unit/db/test_securitygroups_db.py | 3 +- .../unit/extensions/test_portsecurity.py | 11 ++--- .../unit/extensions/test_securitygroup.py | 12 ++--- neutron/tests/unit/objects/test_objects.py | 2 +- .../tests/unit/objects/test_securitygroup.py | 10 +++++ .../plugins/ml2/test_tracked_resources.py | 5 +++ ...teful-security-group-04b2902ed9c44e4f.yaml | 18 ++++++++ 20 files changed, 241 insertions(+), 61 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/ussuri/expand/18a7e90ae768_add_security_group_stateful.py create mode 100644 neutron/extensions/stateful_security_group.py create mode 100644 releasenotes/notes/stateful-security-group-04b2902ed9c44e4f.yaml diff --git a/neutron/agent/linux/iptables_firewall.py b/neutron/agent/linux/iptables_firewall.py index b5839ff8c9f..bd59ad617bd 100644 --- a/neutron/agent/linux/iptables_firewall.py +++ b/neutron/agent/linux/iptables_firewall.py @@ -396,17 +396,54 @@ class IptablesFirewallDriver(firewall.FirewallDriver): # match by interface for bridge input match_interface = '-i %s' match_physdev = '-m physdev --physdev-in %s' - # comment to prevent duplicate warnings for different devices using - # same bridge. truncate start to remove prefixes - comment = '-m comment --comment "Set zone for %s"' % port['device'][4:] + port_sg_rules = self._get_port_sg_rules(port) + if self._are_sg_rules_stateful(port_sg_rules): + # comment to prevent duplicate warnings for different devices using + # same bridge. truncate start to remove prefixes + comment = 'Set zone for %s' % port['device'][4:] + conntrack = '--zone %s' % self.ipconntrack.get_device_zone(port) + else: + comment = 'Make %s stateless' % port['device'][4:] + conntrack = '--notrack' rules = [] for dev, match in ((br_dev, match_physdev), (br_dev, match_interface), (port_dev, match_physdev)): match = match % dev - rule = '%s %s -j CT --zone %s' % (match, comment, zone) + rule = '%s -m comment --comment "%s" -j CT %s' % (match, comment, + conntrack) rules.append(rule) return rules + def _get_port_sg_rules(self, port): + port_sg_rules = [] + if not any(port.get('device_owner', '').startswith(prefix) + for prefix in constants.DEVICE_OWNER_PREFIXES): + port_sg_ids = port.get('security_groups', []) + if port_sg_ids: + for rule in self.sg_rules.get(port_sg_ids[0], []): + if self.enable_ipset: + port_sg_rules.append(rule) + break + else: + port_sg_rules.extend( + self._expand_sg_rule_with_remote_ips( + rule, port, constants.INGRESS_DIRECTION)) + if port_sg_rules: + break + else: + port_sg_rules.extend( + self._expand_sg_rule_with_remote_ips( + rule, port, constants.EGRESS_DIRECTION)) + if port_sg_rules: + break + return port_sg_rules + + @staticmethod + def _are_sg_rules_stateful(security_group_rules): + for rule in security_group_rules: + return rule.get('stateful', True) + return True + def _add_conntrack_jump(self, port): for jump_rule in self._get_jump_rules(port): self._add_raw_rule('PREROUTING', jump_rule) diff --git a/neutron/agent/securitygroups_rpc.py b/neutron/agent/securitygroups_rpc.py index 8f7fbcd7c10..87365265b04 100644 --- a/neutron/agent/securitygroups_rpc.py +++ b/neutron/agent/securitygroups_rpc.py @@ -17,6 +17,7 @@ import functools from neutron_lib.api.definitions import rbac_security_groups as rbac_sg_apidef +from neutron_lib.api.definitions import stateful_security_group as stateful_sg from oslo_config import cfg from oslo_log import log as logging import oslo_messaging @@ -46,6 +47,7 @@ def disable_security_group_extension_by_config(aliases): LOG.info('Disabled security-group extension.') _disable_extension('security-group', aliases) _disable_extension(rbac_sg_apidef.ALIAS, aliases) + _disable_extension(stateful_sg.ALIAS, aliases) LOG.info('Disabled allowed-address-pairs extension.') _disable_extension('allowed-address-pairs', aliases) diff --git a/neutron/api/rpc/handlers/securitygroups_rpc.py b/neutron/api/rpc/handlers/securitygroups_rpc.py index 01ff3b3decd..32ebdac2799 100644 --- a/neutron/api/rpc/handlers/securitygroups_rpc.py +++ b/neutron/api/rpc/handlers/securitygroups_rpc.py @@ -24,6 +24,7 @@ from neutron_lib.utils import net from oslo_log import log as logging import oslo_messaging +from neutron.api.rpc.callbacks import resources from neutron.api.rpc.handlers import resources_rpc from neutron.db import securitygroups_rpc_base as sg_rpc_base @@ -354,3 +355,7 @@ class SecurityGroupServerAPIShim(sg_rpc_base.SecurityGroupInfoAPIMixin): sg_ids = set((sg_id for p in ports.values() for sg_id in p['security_group_ids'])) return [(sg_id, ) for sg_id in sg_ids] + + def _is_security_group_stateful(self, context, sg_id): + sg = self.rcache.get_resource_by_id(resources.SECURITYGROUP, sg_id) + return sg.stateful diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 593cfb3be89..e75b3acafed 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -2217c4222de6 +18a7e90ae768 diff --git a/neutron/db/migration/alembic_migrations/versions/ussuri/expand/18a7e90ae768_add_security_group_stateful.py b/neutron/db/migration/alembic_migrations/versions/ussuri/expand/18a7e90ae768_add_security_group_stateful.py new file mode 100644 index 00000000000..20101191cb0 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/ussuri/expand/18a7e90ae768_add_security_group_stateful.py @@ -0,0 +1,37 @@ +# Copyright 2018 NOKIA +# +# 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 alembic import op +import sqlalchemy as sa + +"""add security group stateful + +Revision ID: 18a7e90ae768 +Revises: 2217c4222de6 +Create Date: 2018-04-26 14:44:52.635576 + +""" + +# revision identifiers, used by Alembic. +revision = '18a7e90ae768' +down_revision = '2217c4222de6' + + +def upgrade(): + op.add_column('securitygroups', + sa.Column('stateful', + sa.Boolean(), + server_default=sa.sql.true(), + nullable=False)) diff --git a/neutron/db/models/securitygroup.py b/neutron/db/models/securitygroup.py index 239103d8c3f..1f193ff0ebd 100644 --- a/neutron/db/models/securitygroup.py +++ b/neutron/db/models/securitygroup.py @@ -16,6 +16,7 @@ from neutron_lib.db import constants as db_const from neutron_lib.db import model_base import sqlalchemy as sa from sqlalchemy import orm +from sqlalchemy import sql from neutron.db import models_v2 from neutron.db import rbac_db_models @@ -28,6 +29,9 @@ class SecurityGroup(standard_attr.HasStandardAttributes, model_base.BASEV2, """Represents a v2 neutron security group.""" name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE)) + stateful = sa.Column(sa.Boolean, + default=True, server_default=sql.true(), + nullable=False) rbac_entries = sa.orm.relationship(rbac_db_models.SecurityGroupRBAC, backref='security_group', lazy='subquery', diff --git a/neutron/db/securitygroups_db.py b/neutron/db/securitygroups_db.py index 8d08fa7c43f..e0792b8e029 100644 --- a/neutron/db/securitygroups_db.py +++ b/neutron/db/securitygroups_db.py @@ -96,6 +96,7 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, desired_state=s)) tenant_id = s['tenant_id'] + stateful = s.get('stateful', True) if not default_sg: self._ensure_default_security_group(context, tenant_id) @@ -109,7 +110,7 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, sg = sg_obj.SecurityGroup( context, id=s.get('id') or uuidutils.generate_uuid(), description=s['description'], project_id=tenant_id, - name=s['name'], is_default=default_sg) + name=s['name'], is_default=default_sg, stateful=stateful) sg.create() delta = len(ext_sg.sg_supported_ethertypes) @@ -277,6 +278,13 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, @db_api.retry_if_session_inactive() def update_security_group(self, context, id, security_group): s = security_group['security_group'] + if 'stateful' in s: + filters = {'security_group_id': [id]} + with db_api.CONTEXT_READER.using(context): + ports = self._get_port_security_group_bindings(context, + filters) + if ports: + raise ext_sg.SecurityGroupInUse(id=id) kwargs = { 'context': context, @@ -311,6 +319,7 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, def _make_security_group_dict(self, security_group, fields=None): res = {'id': security_group['id'], 'name': security_group['name'], + 'stateful': security_group['stateful'], 'tenant_id': security_group['tenant_id'], 'description': security_group['description']} if security_group.rules: @@ -621,6 +630,15 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, tenant_id=rule['tenant_id']) return security_group_id + @staticmethod + def _validate_sgs_for_port(security_groups): + if not security_groups: + return + if not len(set(sg.stateful for sg in security_groups)) == 1: + msg = ("Cannot apply both stateful and stateless security " + "groups on the same port at the same time") + raise ext_sg.SecurityGroupConflict(reason=msg) + def _validate_security_group_rules(self, context, security_group_rules): sg_id = self._validate_single_tenant_and_group(security_group_rules) for rule in security_group_rules['security_group_rules']: @@ -793,15 +811,16 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, return port_res def _process_port_create_security_group(self, context, port, - security_group_ids): - if validators.is_attr_set(security_group_ids): - for security_group_id in security_group_ids: + security_groups): + self._validate_sgs_for_port(security_groups) + if validators.is_attr_set(security_groups): + for sg in security_groups: self._create_port_security_group_binding(context, port['id'], - security_group_id) + sg.id) # Convert to list as a set might be passed here and # this has to be serialized - port[ext_sg.SECURITYGROUPS] = (security_group_ids and - list(security_group_ids) or []) + port[ext_sg.SECURITYGROUPS] = ([sg.id for sg in security_groups] if + security_groups else []) def _get_default_sg_id(self, context, tenant_id): default_group = sg_obj.DefaultSecurityGroup.get_object( @@ -844,7 +863,8 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, def _get_security_groups_on_port(self, context, port): """Check that all security groups on port belong to tenant. - :returns: all security groups IDs on port belonging to tenant. + :returns: all security groups on port belonging to tenant) + """ port = port['port'] if not validators.is_attr_set(port.get(ext_sg.SECURITYGROUPS)): @@ -869,7 +889,7 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, if port_sg_missing: raise ext_sg.SecurityGroupNotFound(id=', '.join(port_sg_missing)) - return list(requested_groups) + return sg_objs def _ensure_default_security_group_on_port(self, context, port): # we don't apply security groups for dhcp, router @@ -920,13 +940,13 @@ class SecurityGroupDbMixin(ext_sg.SecurityGroupPluginBase, original_port.get(ext_sg.SECURITYGROUPS), port_updates[ext_sg.SECURITYGROUPS])): # delete the port binding and read it with the new rules - port_updates[ext_sg.SECURITYGROUPS] = ( - self._get_security_groups_on_port(context, port)) + sgs = self._get_security_groups_on_port(context, port) + port_updates[ext_sg.SECURITYGROUPS] = [sg.id for sg in sgs] self._delete_port_security_group_bindings(context, id) self._process_port_create_security_group( context, updated_port, - port_updates[ext_sg.SECURITYGROUPS]) + sgs) need_notify = True else: updated_port[ext_sg.SECURITYGROUPS] = ( diff --git a/neutron/db/securitygroups_rpc_base.py b/neutron/db/securitygroups_rpc_base.py index 858a9461264..eeab00f6ffb 100644 --- a/neutron/db/securitygroups_rpc_base.py +++ b/neutron/db/securitygroups_rpc_base.py @@ -27,6 +27,7 @@ from neutron.db.models import securitygroup as sg_models from neutron.db import models_v2 from neutron.db import securitygroups_db as sg_db from neutron.extensions import securitygroup as ext_sg +from neutron.objects import securitygroup as sg_obj DIRECTION_IP_PREFIX = {'ingress': 'source_ip_prefix', @@ -189,9 +190,12 @@ class SecurityGroupInfoAPIMixin(object): remote_security_group_info[remote_gid][ethertype] = set() direction = rule_in_db['direction'] + stateful = self._is_security_group_stateful(context, + security_group_id) rule_dict = { 'direction': direction, - 'ethertype': ethertype} + 'ethertype': ethertype, + 'stateful': stateful} for key in ('protocol', 'port_range_min', 'port_range_max', 'remote_ip_prefix', 'remote_group_id'): @@ -351,6 +355,14 @@ class SecurityGroupInfoAPIMixin(object): """ raise NotImplementedError() + def _is_security_group_stateful(self, context, sg_id): + """Return whether the security group is stateful or not. + + Return True if the security group associated with the given ID + is stateful, else False. + """ + return True + class SecurityGroupServerRpcMixin(SecurityGroupInfoAPIMixin, SecurityGroupServerNotifierRpcMixin): @@ -415,3 +427,7 @@ class SecurityGroupServerRpcMixin(SecurityGroupInfoAPIMixin, if allowed_addr_ip: ips_by_group[security_group_id].add(allowed_addr_ip) return ips_by_group + + @db_api.retry_if_session_inactive() + def _is_security_group_stateful(self, context, sg_id): + return sg_obj.SecurityGroup.get_sg_by_id(context, sg_id).stateful diff --git a/neutron/extensions/stateful_security_group.py b/neutron/extensions/stateful_security_group.py new file mode 100644 index 00000000000..6e81f245647 --- /dev/null +++ b/neutron/extensions/stateful_security_group.py @@ -0,0 +1,22 @@ +# Copyright (c) 2018 Nokia. 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_lib.api.definitions import stateful_security_group +from neutron_lib.api import extensions + + +class Stateful_security_group(extensions.APIExtensionDescriptor): + """Extension class supporting stateful security group.""" + + api_definition = stateful_security_group diff --git a/neutron/objects/securitygroup.py b/neutron/objects/securitygroup.py index b06e60f5dc0..6bb65fdbacc 100644 --- a/neutron/objects/securitygroup.py +++ b/neutron/objects/securitygroup.py @@ -35,7 +35,8 @@ class SecurityGroupRBAC(rbac.RBACBaseObject): class SecurityGroup(rbac_db.NeutronRbacObject): # Version 1.0: Initial version # Version 1.1: Add RBAC support - VERSION = '1.1' + # Version 1.2: Added stateful support + VERSION = '1.2' # required by RbacNeutronMetaclass rbac_db_cls = SecurityGroupRBAC @@ -46,6 +47,7 @@ class SecurityGroup(rbac_db.NeutronRbacObject): 'name': obj_fields.StringField(nullable=True), 'project_id': obj_fields.StringField(nullable=True), 'shared': obj_fields.BooleanField(default=False), + 'stateful': obj_fields.BooleanField(default=True), 'is_default': obj_fields.BooleanField(default=False), 'rules': obj_fields.ListOfObjectsField( 'SecurityGroupRule', nullable=True @@ -83,10 +85,16 @@ class SecurityGroup(rbac_db.NeutronRbacObject): bool(db_obj.get('default_security_group'))) self.obj_reset_changes(['is_default']) + @classmethod + def get_sg_by_id(cls, context, sg_id): + return super(SecurityGroup, cls).get_object(context, id=sg_id) + def obj_make_compatible(self, primitive, target_version): _target_version = versionutils.convert_version_to_tuple(target_version) if _target_version < (1, 1): primitive.pop('shared') + if _target_version < (1, 2): + primitive.pop('stateful') @classmethod def get_bound_tenant_ids(cls, context, obj_id): diff --git a/neutron/plugins/ml2/plugin.py b/neutron/plugins/ml2/plugin.py index 9663418fb9d..07c328f974d 100644 --- a/neutron/plugins/ml2/plugin.py +++ b/neutron/plugins/ml2/plugin.py @@ -45,6 +45,7 @@ from neutron_lib.api.definitions import portbindings_extended as pbe_ext from neutron_lib.api.definitions import provider_net from neutron_lib.api.definitions import rbac_security_groups as rbac_sg_apidef from neutron_lib.api.definitions import security_groups_port_filtering +from neutron_lib.api.definitions import stateful_security_group from neutron_lib.api.definitions import subnet as subnet_def from neutron_lib.api.definitions import subnet_onboard as subnet_onboard_def from neutron_lib.api.definitions import subnetpool_prefix_ops \ @@ -203,7 +204,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, pbe_ext.ALIAS, agent_resources_synced.ALIAS, subnet_onboard_def.ALIAS, - subnetpool_prefix_ops_def.ALIAS] + subnetpool_prefix_ops_def.ALIAS, + stateful_security_group.ALIAS] # List of agent types for which all binding_failed ports should try to be # rebound when agent revive @@ -1373,8 +1375,8 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, self._portsec_ext_port_create_processing(context, result, port) # sgids must be got after portsec checked with security group - sgids = self._get_security_groups_on_port(context, port) - self._process_port_create_security_group(context, result, sgids) + sgs = self._get_security_groups_on_port(context, port) + self._process_port_create_security_group(context, result, sgs) network = self.get_network(context, result['network_id']) binding = db.add_port_binding(context, result['id']) mech_context = driver_context.PortContext(self, context, result, @@ -1425,11 +1427,6 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, return bound_context.current - def _ensure_security_groups_on_port(self, context, port_dict): - port_compat = {'port': port_dict} - sgids = self._get_security_groups_on_port(context, port_compat) - self._process_port_create_security_group(context, port_dict, sgids) - @utils.transaction_guard @db_api.retry_if_session_inactive() def create_port_bulk(self, context, ports): @@ -1464,7 +1461,6 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, const.PORT_STATUS_ACTIVE), device_id=pdata.get('device_id'), device_owner=pdata.get('device_owner'), - security_group_ids=security_group_ids, description=pdata.get('description')) # Ensure that the networks exist. @@ -1527,18 +1523,16 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, process_extensions=False) port_dict[portbindings.HOST_ID] = pdata.get( portbindings.HOST_ID) - port_compat = {'port': port_dict} # Activities immediately post-port-creation self.extension_manager.process_create_port(context, pdata, port_dict) self._portsec_ext_port_create_processing(context, port_dict, - port_compat) + port) - # Ensure the default security group is assigned, unless one was - # specifically requested - if security_group_ids is None: - self._ensure_security_groups_on_port(context, port_dict) + sgs = self._get_security_groups_on_port(context, port) + self._process_port_create_security_group(context, port_dict, + sgs) # process port binding binding = db.add_port_binding(context, port_dict['id']) @@ -1554,7 +1548,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, # process allowed address pairs db_port_obj[addr_apidef.ADDRESS_PAIRS] = ( self._process_create_allowed_address_pairs( - context, port_compat, + context, port_dict, port_dict.get(addr_apidef.ADDRESS_PAIRS))) # handle DHCP setup @@ -1582,13 +1576,6 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2, # Perform actions after the transaction is committed completed_ports = [] for port in port_data: - # Ensure security groups are assigned to the port, if - # specifically requested - port_dict = port['port_dict'] - if port_dict.get('security_group_ids') is not None: - with db_api.CONTEXT_WRITER.using(context): - self._ensure_security_groups_on_port(context, port_dict) - resource_extend.apply_funcs('ports', port['port_dict'], port['port_obj'].db_obj) diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index 0c7bd9b6a8c..bf1f0a3a7be 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -61,6 +61,7 @@ NETWORK_API_EXTENSIONS+=",standard-attr-revisions" NETWORK_API_EXTENSIONS+=",standard-attr-segment" NETWORK_API_EXTENSIONS+=",standard-attr-timestamp" NETWORK_API_EXTENSIONS+=",standard-attr-tag" +NETWORK_API_EXTENSIONS+=",stateful-security-group" NETWORK_API_EXTENSIONS+=",subnet_allocation" NETWORK_API_EXTENSIONS+=",subnet-dns-publish-fixed-ip" NETWORK_API_EXTENSIONS+=",tag-ports-during-bulk-creation" diff --git a/neutron/tests/unit/agent/test_securitygroups_rpc.py b/neutron/tests/unit/agent/test_securitygroups_rpc.py index c3418cbbdca..503af21834a 100644 --- a/neutron/tests/unit/agent/test_securitygroups_rpc.py +++ b/neutron/tests/unit/agent/test_securitygroups_rpc.py @@ -513,13 +513,16 @@ class SGServerRpcCallBackTestCase(test_sg.SecurityGroupDBTestCase): ctx, devices=devices) expected = { 'security_groups': {sg1_id: [ - {'direction': 'egress', 'ethertype': const.IPv4}, - {'direction': 'egress', 'ethertype': const.IPv6}, + {'direction': 'egress', 'ethertype': const.IPv4, + 'stateful': True}, + {'direction': 'egress', 'ethertype': const.IPv6, + 'stateful': True}, {'direction': u'ingress', 'protocol': const.PROTO_NAME_TCP, 'ethertype': const.IPv4, 'port_range_max': 25, 'port_range_min': 24, - 'remote_group_id': sg2_id} + 'remote_group_id': sg2_id, + 'stateful': True} ]}, 'sg_member_ips': {sg2_id: { 'IPv4': set([port_ip2]), @@ -628,13 +631,16 @@ class SGServerRpcCallBackTestCase(test_sg.SecurityGroupDBTestCase): ctx, devices=devices) expected = { 'security_groups': {sg1_id: [ - {'direction': 'egress', 'ethertype': const.IPv4}, - {'direction': 'egress', 'ethertype': const.IPv6}, + {'direction': 'egress', 'ethertype': const.IPv4, + 'stateful': True}, + {'direction': 'egress', 'ethertype': const.IPv6, + 'stateful': True}, {'direction': u'ingress', 'protocol': const.PROTO_NAME_TCP, 'ethertype': const.IPv6, 'port_range_max': 22, 'port_range_min': 22, - 'remote_group_id': sg1_id} + 'remote_group_id': sg1_id, + 'stateful': True} ]}, 'sg_member_ips': {sg1_id: { 'IPv6': set(), diff --git a/neutron/tests/unit/db/test_securitygroups_db.py b/neutron/tests/unit/db/test_securitygroups_db.py index 0aa0ccf5f56..30fcae0cd72 100644 --- a/neutron/tests/unit/db/test_securitygroups_db.py +++ b/neutron/tests/unit/db/test_securitygroups_db.py @@ -97,7 +97,7 @@ class SecurityGroupDbMixinTestCase(testlib_api.SqlTestCase): def test_update_security_group_conflict(self): with mock.patch.object(registry, "notify") as mock_notify: mock_notify.side_effect = exceptions.CallbackFailure(Exception()) - secgroup = {'security_group': mock.ANY} + secgroup = {'security_group': FAKE_SECGROUP} with testtools.ExpectedException( securitygroup.SecurityGroupConflict): self.mixin.update_security_group(self.ctx, 'foo_id', secgroup) @@ -272,6 +272,7 @@ class SecurityGroupDbMixinTestCase(testlib_api.SqlTestCase): 'project_id': FAKE_SECGROUP['security_group']['tenant_id'], 'name': 'default', 'description': 'Default security group', + 'stateful': mock.ANY, 'security_group_rules': [ # Four rules for egress/ingress and ipv4/ipv6 mock.ANY, mock.ANY, mock.ANY, mock.ANY, diff --git a/neutron/tests/unit/extensions/test_portsecurity.py b/neutron/tests/unit/extensions/test_portsecurity.py index 3f667a06be7..4251f9a21ce 100644 --- a/neutron/tests/unit/extensions/test_portsecurity.py +++ b/neutron/tests/unit/extensions/test_portsecurity.py @@ -92,8 +92,6 @@ class PortSecurityTestPlugin(db_base_plugin_v2.NeutronDbPluginV2, def create_port(self, context, port): p = port['port'] - p[ext_sg.SECURITYGROUPS] = self._get_security_groups_on_port( - context, port) neutron_db = super(PortSecurityTestPlugin, self).create_port( context, port) p.update(neutron_db) @@ -111,9 +109,12 @@ class PortSecurityTestPlugin(db_base_plugin_v2.NeutronDbPluginV2, if has_ip and port_security: self._ensure_default_security_group_on_port(context, port) + sgs = self._get_security_groups_on_port(context, port) + p[ext_sg.SECURITYGROUPS] = [sg['id'] for sg in sgs] if sgs else None + if (p.get(ext_sg.SECURITYGROUPS) and p[psec.PORTSECURITY]): self._process_port_create_security_group( - context, p, p[ext_sg.SECURITYGROUPS]) + context, p, sgs) return port['port'] @@ -156,11 +157,11 @@ class PortSecurityTestPlugin(db_base_plugin_v2.NeutronDbPluginV2, if (delete_security_groups or has_security_groups): # delete the port binding and read it with the new rules. self._delete_port_security_group_bindings(context, id) - sgids = self._get_security_groups_on_port(context, port) + sgs = self._get_security_groups_on_port(context, port) # process port create sec groups needs port id port['id'] = id self._process_port_create_security_group(context, - ret_port, sgids) + ret_port, sgs) if psec.PORTSECURITY in port['port']: self._process_port_port_security_update( diff --git a/neutron/tests/unit/extensions/test_securitygroup.py b/neutron/tests/unit/extensions/test_securitygroup.py index 17b47bb0547..49686b2d8f6 100644 --- a/neutron/tests/unit/extensions/test_securitygroup.py +++ b/neutron/tests/unit/extensions/test_securitygroup.py @@ -210,24 +210,24 @@ class SecurityGroupTestPlugin(db_base_plugin_v2.NeutronDbPluginV2, if not validators.is_attr_set(port['port'].get(ext_sg.SECURITYGROUPS)): port['port'][ext_sg.SECURITYGROUPS] = [default_sg] with db_api.CONTEXT_WRITER.using(context): - sgids = self._get_security_groups_on_port(context, port) + sgs = self._get_security_groups_on_port(context, port) port = super(SecurityGroupTestPlugin, self).create_port(context, port) self._process_port_create_security_group(context, port, - sgids) + sgs) return port def update_port(self, context, id, port): with db_api.CONTEXT_WRITER.using(context): if ext_sg.SECURITYGROUPS in port['port']: - port['port'][ext_sg.SECURITYGROUPS] = ( - self._get_security_groups_on_port(context, port)) + sgs = self._get_security_groups_on_port(context, port) + port['port'][ext_sg.SECURITYGROUPS] = [ + sg['id'] for sg in sgs] if sgs else None # delete the port binding and read it with the new rules self._delete_port_security_group_bindings(context, id) port['port']['id'] = id self._process_port_create_security_group( - context, port['port'], - port['port'].get(ext_sg.SECURITYGROUPS)) + context, port['port'], sgs) port = super(SecurityGroupTestPlugin, self).update_port( context, id, port) return port diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index e3b0e6c9cc5..c5c5235f4e4 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -98,7 +98,7 @@ object_data = { 'RouterL3AgentBinding': '1.0-c5ba6c95e3a4c1236a55f490cd67da82', 'RouterPort': '1.0-c8c8f499bcdd59186fcd83f323106908', 'RouterRoute': '1.0-07fc5337c801fb8c6ccfbcc5afb45907', - 'SecurityGroup': '1.1-f712265418f154f7c080e02857ffe2ef', + 'SecurityGroup': '1.2-7b63b834e511856f54a09282d6843ecc', 'SecurityGroupPortBinding': '1.0-6879d5c0af80396ef5a72934b6a6ef20', 'SecurityGroupRBAC': '1.0-192845c5ed0718e1c54fac36936fcd7d', 'SecurityGroupRule': '1.0-e9b8dace9d48b936c62ad40fe1f339d5', diff --git a/neutron/tests/unit/objects/test_securitygroup.py b/neutron/tests/unit/objects/test_securitygroup.py index e0aadd4bd62..a94add2a82e 100644 --- a/neutron/tests/unit/objects/test_securitygroup.py +++ b/neutron/tests/unit/objects/test_securitygroup.py @@ -80,6 +80,16 @@ class SecurityGroupDbObjTestCase(test_base.BaseDbObjectTestCase, # generated; we picked the former here rule['remote_group_id'] = None + def _create_test_security_group(self): + self.objs[0].create() + return self.objs[0] + + def test_object_version_degradation_1_2_to_1_1_no_stateful(self): + sg_stateful_obj = self._create_test_security_group() + sg_no_stateful_obj = sg_stateful_obj.obj_to_primitive('1.1') + self.assertNotIn('stateful', + sg_no_stateful_obj['versioned_object.data']) + def test_is_default_True(self): fields = self.obj_fields[0].copy() sg_obj = self._make_object(fields) diff --git a/neutron/tests/unit/plugins/ml2/test_tracked_resources.py b/neutron/tests/unit/plugins/ml2/test_tracked_resources.py index 3c1f724e26c..b06c4835770 100644 --- a/neutron/tests/unit/plugins/ml2/test_tracked_resources.py +++ b/neutron/tests/unit/plugins/ml2/test_tracked_resources.py @@ -56,7 +56,12 @@ class BaseTestEventHandler(object): get_sec_group_port_patch = mock.patch( 'neutron.db.securitygroups_db.SecurityGroupDbMixin.' '_get_security_groups_on_port') + get_sec_group_port_patch.start() + process_port_create_security_group_patch = mock.patch( + 'neutron.db.securitygroups_db.SecurityGroupDbMixin.' + '_process_port_create_security_group') + process_port_create_security_group_patch.start() handler_patch = mock.patch( 'neutron.quota.resource.TrackedResource._db_event_handler') self.handler_mock = handler_patch.start() diff --git a/releasenotes/notes/stateful-security-group-04b2902ed9c44e4f.yaml b/releasenotes/notes/stateful-security-group-04b2902ed9c44e4f.yaml new file mode 100644 index 00000000000..276a1b7786a --- /dev/null +++ b/releasenotes/notes/stateful-security-group-04b2902ed9c44e4f.yaml @@ -0,0 +1,18 @@ +--- +prelude: > + Added support to create stateless security groups. +features: + - | + Added support for a new stateful-security-group api extension that + implements stateless security groups for the iptables drivers. +upgrade: + - | + Currently existing security groups will all be set to stateful during + the alembic migration. +security: + - | + The ``stateless security group`` feature does not work with + OVS nor OVN driver as the driver is not aware of the ``stateful`` attribute + in the security group. If ``stateful`` attribute is provided with a + ``False`` value then the attribute value is ignored and the security + group would behave as stateful.