]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Add portsecurity extension support
authorYalei Wang <yalei.wang@intel.com>
Mon, 9 Feb 2015 19:22:27 +0000 (03:22 +0800)
committerYalei Wang <yalei.wang@intel.com>
Mon, 16 Mar 2015 21:13:04 +0000 (05:13 +0800)
Add portsecurity extension driver into ML2 plugin and implement it in
iptables_firewall.

The scope of this change is:
    - Abstract a common class PortSecurityDbCommon from the old
      PortSecurityDbMixin
    - Add a new extension driver port-security, implement process_xxx and
      extend_xxx_dict method and provide a db migration from the existing
      networks and ports
    - Update the new added 'unfiltered_ports' in iptables firewall of l2 agent
      to reflect the update of port-security

Co-Authored-By: Shweta P <shpadubi@cisco.com>
Change-Id: I2da53168e2529db7a8094ce90ef3a8a93fe55727
Partially Implements: blueprint ml2-ovs-portsecurity

14 files changed:
neutron/agent/linux/iptables_comments.py
neutron/agent/linux/iptables_firewall.py
neutron/db/migration/alembic_migrations/versions/35a0f3365720_add_port_security_in_ml2.py [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/HEAD
neutron/db/portsecurity_db.py
neutron/db/portsecurity_db_common.py [new file with mode: 0644]
neutron/plugins/ml2/extensions/__init__.py [new file with mode: 0644]
neutron/plugins/ml2/extensions/port_security.py [new file with mode: 0644]
neutron/plugins/ml2/plugin.py
neutron/tests/functional/agent/linux/test_iptables_firewall.py
neutron/tests/unit/ml2/test_ext_portsecurity.py [new file with mode: 0644]
neutron/tests/unit/test_extension_portsecurity.py
neutron/tests/unit/test_iptables_firewall.py
setup.cfg

index 7d158a9d80ceb8eca645beef1232c4a322092e9c..142f82b7257a88fcd00aaca27818f06e1feba36e 100644 (file)
@@ -33,3 +33,4 @@ INVALID_DROP = ("Drop packets that appear related to an existing connection "
 ALLOW_ASSOC = ('Direct packets associated with a known session to the RETURN '
                'chain.')
 IPV6_RA_ALLOW = 'Allow IPv6 ICMP traffic to allow RA packets.'
+PORT_SEC_ACCEPT = 'Accept all packets when port security is disabled.'
index d44f5b90b4607a1c029933fb0b7c41f0e7b51475..0fa77b8a61df5baebb6c86f8d0c5a8af4a26b476 100644 (file)
@@ -24,6 +24,7 @@ from neutron.agent.linux import iptables_comments as ic
 from neutron.agent.linux import iptables_manager
 from neutron.common import constants
 from neutron.common import ipv6_utils
+from neutron.extensions import portsecurity as psec
 from neutron.i18n import _LI
 
 
@@ -57,9 +58,11 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
         self.ipset = ipset_manager.IpsetManager(namespace=namespace)
         # list of port which has security group
         self.filtered_ports = {}
+        self.unfiltered_ports = {}
         self._add_fallback_chain_v4v6()
         self._defer_apply = False
         self._pre_defer_filtered_ports = None
+        self._pre_defer_unfiltered_ports = None
         # List of security group rules for ports residing on this host
         self.sg_rules = {}
         self.pre_sg_rules = None
@@ -71,7 +74,7 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
 
     @property
     def ports(self):
-        return self.filtered_ports
+        return dict(self.filtered_ports, **self.unfiltered_ports)
 
     def update_security_group_rules(self, sg_id, sg_rules):
         LOG.debug("Update rules of security group (%s)", sg_id)
@@ -81,42 +84,72 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
         LOG.debug("Update members of security group (%s)", sg_id)
         self.sg_members[sg_id] = collections.defaultdict(list, sg_members)
 
+    def _ps_enabled(self, port):
+        return port.get(psec.PORTSECURITY, True)
+
+    def _set_ports(self, port):
+        if not self._ps_enabled(port):
+            self.unfiltered_ports[port['device']] = port
+            self.filtered_ports.pop(port['device'], None)
+        else:
+            self.filtered_ports[port['device']] = port
+            self.unfiltered_ports.pop(port['device'], None)
+
+    def _unset_ports(self, port):
+        self.unfiltered_ports.pop(port['device'], None)
+        self.filtered_ports.pop(port['device'], None)
+
     def prepare_port_filter(self, port):
         LOG.debug("Preparing device (%s) filter", port['device'])
         self._remove_chains()
-        self.filtered_ports[port['device']] = port
+        self._set_ports(port)
+
         # each security group has it own chains
         self._setup_chains()
         self.iptables.apply()
 
     def update_port_filter(self, port):
         LOG.debug("Updating device (%s) filter", port['device'])
-        if port['device'] not in self.filtered_ports:
+        if port['device'] not in self.ports:
             LOG.info(_LI('Attempted to update port filter which is not '
                          'filtered %s'), port['device'])
             return
         self._remove_chains()
-        self.filtered_ports[port['device']] = port
+        self._set_ports(port)
         self._setup_chains()
         self.iptables.apply()
 
     def remove_port_filter(self, port):
         LOG.debug("Removing device (%s) filter", port['device'])
-        if not self.filtered_ports.get(port['device']):
+        if port['device'] not in self.ports:
             LOG.info(_LI('Attempted to remove port filter which is not '
                          'filtered %r'), port)
             return
         self._remove_chains()
-        self.filtered_ports.pop(port['device'], None)
+        self._unset_ports(port)
         self._setup_chains()
         self.iptables.apply()
 
+    def _add_accept_rule_port_sec(self, port, direction):
+        self._update_port_sec_rules(port, direction, add=True)
+
+    def _remove_rule_port_sec(self, port, direction):
+        self._update_port_sec_rules(port, direction, add=False)
+
+    def _remove_rule_from_chain_v4v6(self, chain_name, ipv4_rules, ipv6_rules):
+        for rule in ipv4_rules:
+            self.iptables.ipv4['filter'].remove_rule(chain_name, rule)
+
+        for rule in ipv6_rules:
+            self.iptables.ipv6['filter'].remove_rule(chain_name, rule)
+
     def _setup_chains(self):
         """Setup ingress and egress chain for a port."""
         if not self._defer_apply:
-            self._setup_chains_apply(self.filtered_ports)
+            self._setup_chains_apply(self.filtered_ports,
+                                     self.unfiltered_ports)
 
-    def _setup_chains_apply(self, ports):
+    def _setup_chains_apply(self, ports, unfiltered_ports):
         self._add_chain_by_name_v4v6(SG_CHAIN)
         for port in ports.values():
             self._setup_chain(port, INGRESS_DIRECTION)
@@ -124,16 +157,24 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
             self.iptables.ipv4['filter'].add_rule(SG_CHAIN, '-j ACCEPT')
             self.iptables.ipv6['filter'].add_rule(SG_CHAIN, '-j ACCEPT')
 
+        for port in unfiltered_ports.values():
+            self._add_accept_rule_port_sec(port, INGRESS_DIRECTION)
+            self._add_accept_rule_port_sec(port, EGRESS_DIRECTION)
+
     def _remove_chains(self):
         """Remove ingress and egress chain for a port."""
         if not self._defer_apply:
-            self._remove_chains_apply(self.filtered_ports)
+            self._remove_chains_apply(self.filtered_ports,
+                                      self.unfiltered_ports)
 
-    def _remove_chains_apply(self, ports):
+    def _remove_chains_apply(self, ports, unfiltered_ports):
         for port in ports.values():
             self._remove_chain(port, INGRESS_DIRECTION)
             self._remove_chain(port, EGRESS_DIRECTION)
             self._remove_chain(port, SPOOF_FILTER)
+        for port in unfiltered_ports.values():
+            self._remove_rule_port_sec(port, INGRESS_DIRECTION)
+            self._remove_rule_port_sec(port, EGRESS_DIRECTION)
         self._remove_chain_by_name_v4v6(SG_CHAIN)
 
     def _setup_chain(self, port, DIRECTION):
@@ -173,6 +214,30 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
     def _get_device_name(self, port):
         return port['device']
 
+    def _update_port_sec_rules(self, port, direction, add=False):
+        # add/remove rules in FORWARD and INPUT chain
+        device = self._get_device_name(port)
+
+        jump_rule = ['-m physdev --%s %s --physdev-is-bridged '
+                     '-j ACCEPT' % (self.IPTABLES_DIRECTION[direction],
+                                    device)]
+        if add:
+            self._add_rules_to_chain_v4v6(
+                'FORWARD', jump_rule, jump_rule, comment=ic.PORT_SEC_ACCEPT)
+        else:
+            self._remove_rule_from_chain_v4v6('FORWARD', jump_rule, jump_rule)
+
+        if direction == EGRESS_DIRECTION:
+            jump_rule = ['-m physdev --%s %s --physdev-is-bridged '
+                         '-j ACCEPT' % (self.IPTABLES_DIRECTION[direction],
+                                        device)]
+            if add:
+                self._add_rules_to_chain_v4v6('INPUT', jump_rule, jump_rule,
+                                              comment=ic.PORT_SEC_ACCEPT)
+            else:
+                self._remove_rule_from_chain_v4v6(
+                    'INPUT', jump_rule, jump_rule)
+
     def _add_chain(self, port, direction):
         chain_name = self._port_chain_name(port, direction)
         self._add_chain_by_name_v4v6(chain_name)
@@ -496,6 +561,7 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
         if not self._defer_apply:
             self.iptables.defer_apply_on()
             self._pre_defer_filtered_ports = dict(self.filtered_ports)
+            self._pre_defer_unfiltered_ports = dict(self.unfiltered_ports)
             self.pre_sg_members = dict(self.sg_members)
             self.pre_sg_rules = dict(self.sg_rules)
             self._defer_apply = True
@@ -587,11 +653,14 @@ class IptablesFirewallDriver(firewall.FirewallDriver):
     def filter_defer_apply_off(self):
         if self._defer_apply:
             self._defer_apply = False
-            self._remove_chains_apply(self._pre_defer_filtered_ports)
-            self._setup_chains_apply(self.filtered_ports)
+            self._remove_chains_apply(self._pre_defer_filtered_ports,
+                                      self._pre_defer_unfiltered_ports)
+            self._setup_chains_apply(self.filtered_ports,
+                                     self.unfiltered_ports)
             self.iptables.defer_apply_off()
             self._remove_unused_security_group_info()
             self._pre_defer_filtered_ports = None
+            self._pre_defer_unfiltered_ports = None
 
 
 class OVSHybridIptablesFirewallDriver(IptablesFirewallDriver):
diff --git a/neutron/db/migration/alembic_migrations/versions/35a0f3365720_add_port_security_in_ml2.py b/neutron/db/migration/alembic_migrations/versions/35a0f3365720_add_port_security_in_ml2.py
new file mode 100644 (file)
index 0000000..30b2714
--- /dev/null
@@ -0,0 +1,44 @@
+# Copyright 2015 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.
+#
+
+"""add port-security in ml2
+
+Revision ID: 35a0f3365720
+Revises: 341ee8a4ccb5
+Create Date: 2014-09-30 09:41:14.146519
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '35a0f3365720'
+down_revision = '341ee8a4ccb5'
+
+from alembic import op
+
+
+def upgrade():
+    op.execute('INSERT INTO networksecuritybindings (network_id, '
+               'port_security_enabled) SELECT id, True FROM networks '
+               'WHERE id NOT IN (SELECT network_id FROM '
+               'networksecuritybindings);')
+
+    op.execute('INSERT INTO portsecuritybindings (port_id, '
+               'port_security_enabled) SELECT id, True FROM ports '
+               'WHERE id NOT IN (SELECT port_id FROM '
+               'portsecuritybindings);')
+
+
+def downgrade():
+    pass
index 1f9bcd91d65e74f1b346c7c9f5c2c01697685274..c535b02c38ee51912278743a19c9e790d68cd4dd 100644 (file)
@@ -1 +1 @@
-341ee8a4ccb5
+35a0f3365720
index 9890948b2befa35d6d6c95b9b62082f1cfd687b3..bff1555a010f4a1ae4dece4e1183e884c1f1a6b4 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from oslo_log import log as logging
-import sqlalchemy as sa
-from sqlalchemy import orm
-from sqlalchemy.orm import exc
-
 from neutron.api.v2 import attributes as attrs
 from neutron.db import db_base_plugin_v2
-from neutron.db import model_base
-from neutron.db import models_v2
+from neutron.db import portsecurity_db_common
 from neutron.extensions import portsecurity as psec
 
-LOG = logging.getLogger(__name__)
-
-
-class PortSecurityBinding(model_base.BASEV2):
-    port_id = sa.Column(sa.String(36),
-                        sa.ForeignKey('ports.id', ondelete="CASCADE"),
-                        primary_key=True)
-    port_security_enabled = sa.Column(sa.Boolean(), nullable=False)
-
-    # Add a relationship to the Port model in order to be to able to
-    # instruct SQLAlchemy to eagerly load port security binding
-    port = orm.relationship(
-        models_v2.Port,
-        backref=orm.backref("port_security", uselist=False,
-                            cascade='delete', lazy='joined'))
-
-
-class NetworkSecurityBinding(model_base.BASEV2):
-    network_id = sa.Column(sa.String(36),
-                           sa.ForeignKey('networks.id', ondelete="CASCADE"),
-                           primary_key=True)
-    port_security_enabled = sa.Column(sa.Boolean(), nullable=False)
-
-    # Add a relationship to the Port model in order to be able to instruct
-    # SQLAlchemy to eagerly load default port security setting for ports
-    # on this network
-    network = orm.relationship(
-        models_v2.Network,
-        backref=orm.backref("port_security", uselist=False,
-                            cascade='delete', lazy='joined'))
-
 
-class PortSecurityDbMixin(object):
-    """Mixin class to add port security."""
-
-    def _process_network_port_security_create(
-        self, context, network_req, network_res):
-        with context.session.begin(subtransactions=True):
-            db = NetworkSecurityBinding(
-                network_id=network_res['id'],
-                port_security_enabled=network_req[psec.PORTSECURITY])
-            context.session.add(db)
-        network_res[psec.PORTSECURITY] = network_req[psec.PORTSECURITY]
-        return self._make_network_port_security_dict(db)
-
-    def _process_port_port_security_create(
-        self, context, port_req, port_res):
-        with context.session.begin(subtransactions=True):
-            db = PortSecurityBinding(
-                port_id=port_res['id'],
-                port_security_enabled=port_req[psec.PORTSECURITY])
-            context.session.add(db)
-        port_res[psec.PORTSECURITY] = port_req[psec.PORTSECURITY]
-        return self._make_port_security_dict(db)
+class PortSecurityDbMixin(portsecurity_db_common.PortSecurityDbCommon):
+    # Register dict extend functions for ports and networks
+    db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
+        attrs.NETWORKS, ['_extend_port_security_dict'])
+    db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
+        attrs.PORTS, ['_extend_port_security_dict'])
 
     def _extend_port_security_dict(self, response_data, db_data):
         if ('port-security' in
@@ -84,63 +31,6 @@ class PortSecurityDbMixin(object):
             psec_value = db_data['port_security'][psec.PORTSECURITY]
             response_data[psec.PORTSECURITY] = psec_value
 
-    def _get_network_security_binding(self, context, network_id):
-        try:
-            query = self._model_query(context, NetworkSecurityBinding)
-            binding = query.filter(
-                NetworkSecurityBinding.network_id == network_id).one()
-        except exc.NoResultFound:
-            raise psec.PortSecurityBindingNotFound()
-        return binding[psec.PORTSECURITY]
-
-    def _get_port_security_binding(self, context, port_id):
-        try:
-            query = self._model_query(context, PortSecurityBinding)
-            binding = query.filter(
-                PortSecurityBinding.port_id == port_id).one()
-        except exc.NoResultFound:
-            raise psec.PortSecurityBindingNotFound()
-        return binding[psec.PORTSECURITY]
-
-    def _process_port_port_security_update(
-        self, context, port_req, port_res):
-        if psec.PORTSECURITY in port_req:
-            port_security_enabled = port_req[psec.PORTSECURITY]
-        else:
-            return
-        try:
-            query = self._model_query(context, PortSecurityBinding)
-            port_id = port_res['id']
-            binding = query.filter(
-                PortSecurityBinding.port_id == port_id).one()
-
-            binding.port_security_enabled = port_security_enabled
-            port_res[psec.PORTSECURITY] = port_security_enabled
-        except exc.NoResultFound:
-            raise psec.PortSecurityBindingNotFound()
-
-    def _process_network_port_security_update(
-        self, context, network_req, network_res):
-        if psec.PORTSECURITY in network_req:
-            port_security_enabled = network_req[psec.PORTSECURITY]
-        else:
-            return
-        try:
-            query = self._model_query(context, NetworkSecurityBinding)
-            network_id = network_res['id']
-            binding = query.filter(
-                NetworkSecurityBinding.network_id == network_id).one()
-
-            binding.port_security_enabled = port_security_enabled
-            network_res[psec.PORTSECURITY] = port_security_enabled
-        except exc.NoResultFound:
-            raise psec.PortSecurityBindingNotFound()
-
-    def _make_network_port_security_dict(self, port_security, fields=None):
-        res = {'network_id': port_security['network_id'],
-               psec.PORTSECURITY: port_security[psec.PORTSECURITY]}
-        return self._fields(res, fields)
-
     def _determine_port_security_and_has_ip(self, context, port):
         """Returns a tuple of booleans (port_security_enabled, has_ip).
 
@@ -170,16 +60,5 @@ class PortSecurityDbMixin(object):
 
         return (port_security_enabled, has_ip)
 
-    def _make_port_security_dict(self, port, fields=None):
-        res = {'port_id': port['port_id'],
-               psec.PORTSECURITY: port[psec.PORTSECURITY]}
-        return self._fields(res, fields)
-
     def _ip_on_port(self, port):
         return bool(port.get('fixed_ips'))
-
-    # Register dict extend functions for ports and networks
-    db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
-        attrs.NETWORKS, ['_extend_port_security_dict'])
-    db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs(
-        attrs.PORTS, ['_extend_port_security_dict'])
diff --git a/neutron/db/portsecurity_db_common.py b/neutron/db/portsecurity_db_common.py
new file mode 100644 (file)
index 0000000..3fad111
--- /dev/null
@@ -0,0 +1,139 @@
+# Copyright 2013 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 oslo_log import log as logging
+import sqlalchemy as sa
+from sqlalchemy import orm
+from sqlalchemy.orm import exc
+
+from neutron.db import model_base
+from neutron.db import models_v2
+from neutron.extensions import portsecurity as psec
+
+LOG = logging.getLogger(__name__)
+
+
+class PortSecurityBinding(model_base.BASEV2):
+    port_id = sa.Column(sa.String(36),
+                        sa.ForeignKey('ports.id', ondelete="CASCADE"),
+                        primary_key=True)
+    port_security_enabled = sa.Column(sa.Boolean(), nullable=False)
+
+    # Add a relationship to the Port model in order to be to able to
+    # instruct SQLAlchemy to eagerly load port security binding
+    port = orm.relationship(
+        models_v2.Port,
+        backref=orm.backref("port_security", uselist=False,
+                            cascade='delete', lazy='joined'))
+
+
+class NetworkSecurityBinding(model_base.BASEV2):
+    network_id = sa.Column(sa.String(36),
+                           sa.ForeignKey('networks.id', ondelete="CASCADE"),
+                           primary_key=True)
+    port_security_enabled = sa.Column(sa.Boolean(), nullable=False)
+
+    # Add a relationship to the Port model in order to be able to instruct
+    # SQLAlchemy to eagerly load default port security setting for ports
+    # on this network
+    network = orm.relationship(
+        models_v2.Network,
+        backref=orm.backref("port_security", uselist=False,
+                            cascade='delete', lazy='joined'))
+
+
+class PortSecurityDbCommon(object):
+    """Mixin class to add port security."""
+
+    def _process_network_port_security_create(
+        self, context, network_req, network_res):
+        with context.session.begin(subtransactions=True):
+            db = NetworkSecurityBinding(
+                network_id=network_res['id'],
+                port_security_enabled=network_req[psec.PORTSECURITY])
+            context.session.add(db)
+        network_res[psec.PORTSECURITY] = network_req[psec.PORTSECURITY]
+        return self._make_network_port_security_dict(db)
+
+    def _process_port_port_security_create(
+        self, context, port_req, port_res):
+        with context.session.begin(subtransactions=True):
+            db = PortSecurityBinding(
+                port_id=port_res['id'],
+                port_security_enabled=port_req[psec.PORTSECURITY])
+            context.session.add(db)
+        port_res[psec.PORTSECURITY] = port_req[psec.PORTSECURITY]
+        return self._make_port_security_dict(db)
+
+    def _get_network_security_binding(self, context, network_id):
+        try:
+            query = self._model_query(context, NetworkSecurityBinding)
+            binding = query.filter(
+                NetworkSecurityBinding.network_id == network_id).one()
+        except exc.NoResultFound:
+            raise psec.PortSecurityBindingNotFound()
+        return binding[psec.PORTSECURITY]
+
+    def _get_port_security_binding(self, context, port_id):
+        try:
+            query = self._model_query(context, PortSecurityBinding)
+            binding = query.filter(
+                PortSecurityBinding.port_id == port_id).one()
+        except exc.NoResultFound:
+            raise psec.PortSecurityBindingNotFound()
+        return binding[psec.PORTSECURITY]
+
+    def _process_port_port_security_update(
+        self, context, port_req, port_res):
+        if psec.PORTSECURITY in port_req:
+            port_security_enabled = port_req[psec.PORTSECURITY]
+        else:
+            return
+        try:
+            query = self._model_query(context, PortSecurityBinding)
+            port_id = port_res['id']
+            binding = query.filter(
+                PortSecurityBinding.port_id == port_id).one()
+
+            binding.port_security_enabled = port_security_enabled
+            port_res[psec.PORTSECURITY] = port_security_enabled
+        except exc.NoResultFound:
+            raise psec.PortSecurityBindingNotFound()
+
+    def _process_network_port_security_update(
+        self, context, network_req, network_res):
+        if psec.PORTSECURITY in network_req:
+            port_security_enabled = network_req[psec.PORTSECURITY]
+        else:
+            return
+        try:
+            query = self._model_query(context, NetworkSecurityBinding)
+            network_id = network_res['id']
+            binding = query.filter(
+                NetworkSecurityBinding.network_id == network_id).one()
+
+            binding.port_security_enabled = port_security_enabled
+            network_res[psec.PORTSECURITY] = port_security_enabled
+        except exc.NoResultFound:
+            raise psec.PortSecurityBindingNotFound()
+
+    def _make_network_port_security_dict(self, port_security, fields=None):
+        res = {'network_id': port_security['network_id'],
+               psec.PORTSECURITY: port_security[psec.PORTSECURITY]}
+        return self._fields(res, fields)
+
+    def _make_port_security_dict(self, port, fields=None):
+        res = {'port_id': port['port_id'],
+               psec.PORTSECURITY: port[psec.PORTSECURITY]}
+        return self._fields(res, fields)
diff --git a/neutron/plugins/ml2/extensions/__init__.py b/neutron/plugins/ml2/extensions/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/neutron/plugins/ml2/extensions/port_security.py b/neutron/plugins/ml2/extensions/port_security.py
new file mode 100644 (file)
index 0000000..aceec24
--- /dev/null
@@ -0,0 +1,86 @@
+# Copyright 2015 Intel Corporation.
+# 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.v2 import attributes as attrs
+from neutron.db import common_db_mixin
+from neutron.db import portsecurity_db_common as ps_db_common
+from neutron.extensions import portsecurity as psec
+from neutron.i18n import _LI
+from neutron.plugins.ml2 import driver_api as api
+from oslo_log import log as logging
+
+LOG = logging.getLogger(__name__)
+
+
+class PortSecurityExtensionDriver(api.ExtensionDriver,
+                                  ps_db_common.PortSecurityDbCommon,
+                                  common_db_mixin.CommonDbMixin):
+    _supported_extension_alias = 'port-security'
+
+    def initialize(self):
+        LOG.info(_LI("PortSecurityExtensionDriver initialization complete"))
+
+    @property
+    def extension_alias(self):
+        return self._supported_extension_alias
+
+    def process_create_network(self, context, data, result):
+        # Create the network extension attributes.
+        if psec.PORTSECURITY in data:
+            self._process_network_port_security_create(context, data, result)
+
+    def process_update_network(self, context, data, result):
+        # Update the network extension attributes.
+        if psec.PORTSECURITY in data:
+            self._process_network_port_security_update(context, data, result)
+
+    def process_create_port(self, context, data, result):
+        # Create the port extension attributes.
+        data[psec.PORTSECURITY] = self._determine_port_security(context, data)
+        self._process_port_port_security_create(context, data, result)
+
+    def process_update_port(self, context, data, result):
+        if psec.PORTSECURITY in data:
+            self._process_port_port_security_update(
+                context, data, result)
+
+    def extend_network_dict(self, session, db_data, result):
+        self._extend_port_security_dict(result, db_data)
+
+    def extend_port_dict(self, session, db_data, result):
+        self._extend_port_security_dict(result, db_data)
+
+    def _extend_port_security_dict(self, response_data, db_data):
+        response_data[psec.PORTSECURITY] = (
+                                db_data['port_security'][psec.PORTSECURITY])
+
+    def _determine_port_security(self, context, port):
+        """Returns a boolean (port_security_enabled).
+
+        Port_security is the value associated with the port if one is present
+        otherwise the value associated with the network is returned.
+        """
+        # we don't apply security groups for dhcp, router
+        if (port.get('device_owner') and
+                port['device_owner'].startswith('network:')):
+            return False
+
+        if attrs.is_attr_set(port.get(psec.PORTSECURITY)):
+            port_security_enabled = port[psec.PORTSECURITY]
+        else:
+            port_security_enabled = self._get_network_security_binding(
+                context, port['network_id'])
+
+        return port_security_enabled
index aa5620e0421b9f3768c84c174e475041edea204a..d566fb802d8d80e6ebb2ffdfe3d5a41344ff2228 100644 (file)
@@ -57,7 +57,9 @@ from neutron.db import securitygroups_rpc_base as sg_db_rpc
 from neutron.extensions import allowedaddresspairs as addr_pair
 from neutron.extensions import extra_dhcp_opt as edo_ext
 from neutron.extensions import portbindings
+from neutron.extensions import portsecurity as psec
 from neutron.extensions import providernet as provider
+from neutron.extensions import securitygroup as ext_sg
 from neutron.i18n import _LE, _LI, _LW
 from neutron import manager
 from neutron.openstack.common import uuidutils
@@ -900,17 +902,39 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
             # the fact that an error occurred.
             LOG.error(_LE("mechanism_manager.delete_subnet_postcommit failed"))
 
+    # TODO(yalei) - will be simplified after security group and address pair be
+    # converted to ext driver too.
+    def _portsec_ext_port_create_processing(self, context, port_data, port):
+        attrs = port[attributes.PORT]
+        port_security = ((port_data.get(psec.PORTSECURITY) is None) or
+                         port_data[psec.PORTSECURITY])
+
+        # allowed address pair checks
+        if attributes.is_attr_set(attrs.get(addr_pair.ADDRESS_PAIRS)):
+            if not port_security:
+                raise addr_pair.AddressPairAndPortSecurityRequired()
+        else:
+            # remove ATTR_NOT_SPECIFIED
+            attrs[addr_pair.ADDRESS_PAIRS] = []
+
+        if port_security:
+            self._ensure_default_security_group_on_port(context, port)
+        elif attributes.is_attr_set(attrs.get(ext_sg.SECURITYGROUPS)):
+            raise psec.PortSecurityAndIPRequiredForSecurityGroups()
+
     def _create_port_db(self, context, port):
         attrs = port[attributes.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)
             dhcp_opts = attrs.get(edo_ext.EXTRADHCPOPTS, [])
             result = super(Ml2Plugin, self).create_port(context, port)
             self.extension_manager.process_create_port(context, attrs, result)
+            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)
             network = self.get_network(context, result['network_id'])
             binding = db.add_port_binding(session, result['id'])
@@ -987,6 +1011,47 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
                           resource_ids)
                 self._delete_objects(context, attributes.PORT, objects)
 
+    # TODO(yalei) - will be simplified after security group and address pair be
+    # converted to ext driver too.
+    def _portsec_ext_port_update_processing(self, updated_port, context, port,
+                                            id):
+        port_security = ((updated_port.get(psec.PORTSECURITY) is None) or
+                         updated_port[psec.PORTSECURITY])
+
+        if port_security:
+            return
+
+        # check the address-pairs
+        if self._check_update_has_allowed_address_pairs(port):
+            #  has address pairs in request
+            raise addr_pair.AddressPairAndPortSecurityRequired()
+        elif (not
+         self._check_update_deletes_allowed_address_pairs(port)):
+            # not a request for deleting the address-pairs
+            updated_port[addr_pair.ADDRESS_PAIRS] = (
+                    self.get_allowed_address_pairs(context, id))
+
+            # check if address pairs has been in db, if address pairs could
+            # be put in extension driver, we can refine here.
+            if updated_port[addr_pair.ADDRESS_PAIRS]:
+                raise addr_pair.AddressPairAndPortSecurityRequired()
+
+        # checks if security groups were updated adding/modifying
+        # security groups, port security is set
+        if self._check_update_has_security_groups(port):
+            raise psec.PortSecurityAndIPRequiredForSecurityGroups()
+        elif (not
+          self._check_update_deletes_security_groups(port)):
+            # Update did not have security groups passed in. Check
+            # that port does not have any security groups already on it.
+            filters = {'port_id': [id]}
+            security_groups = (
+                super(Ml2Plugin, self)._get_port_security_group_bindings(
+                        context, filters)
+                     )
+            if security_groups:
+                raise psec.PortSecurityPortHasSecurityGroup()
+
     def update_port(self, context, id, port):
         attrs = port[attributes.PORT]
         need_port_update_notify = False
@@ -1009,6 +1074,14 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
                                                               port)
             self.extension_manager.process_update_port(context, attrs,
                                                        updated_port)
+            self._portsec_ext_port_update_processing(updated_port, context,
+                                                     port, id)
+
+            if (psec.PORTSECURITY in attrs) and (
+                        original_port[psec.PORTSECURITY] !=
+                        updated_port[psec.PORTSECURITY]):
+                need_port_update_notify = True
+
             if addr_pair.ADDRESS_PAIRS in attrs:
                 need_port_update_notify |= (
                     self.update_address_pairs_on_port(context, id, port,
index b902604fe2d69e4fad97fca83ca28f5ae5d7f5ca..d0cfa18286c7f96be3ab8606505f79bfb1e37d64 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from neutron.agent.linux import ip_lib
 from neutron.agent.linux import iptables_firewall
+from neutron.agent import securitygroups_rpc as sg_cfg
 from neutron.tests.functional.agent.linux import base
+from neutron.tests.functional.agent.linux import helpers
+from oslo_config import cfg
 
 
 class IptablesFirewallTestCase(base.BaseBridgeTestCase):
+    MAC_REAL = "fa:16:3e:9a:2f:49"
+    MAC_SPOOFED = "fa:16:3e:9a:2f:48"
+    FAKE_SECURITY_GROUP_ID = "fake_sg_id"
+
+    def _set_src_mac(self, mac):
+        self.src_veth.link.set_down()
+        self.src_veth.link.set_address(mac)
+        self.src_veth.link.set_up()
+
     def setUp(self):
+        cfg.CONF.register_opts(sg_cfg.security_group_opts, 'SECURITYGROUP')
         super(IptablesFirewallTestCase, self).setUp()
         self.bridge = self.create_bridge()
 
@@ -40,8 +54,39 @@ class IptablesFirewallTestCase(base.BaseBridgeTestCase):
         self.firewall = iptables_firewall.IptablesFirewallDriver(
             namespace=self.bridge.namespace)
 
-    # TODO(yamahata): add tests...
+        self._set_src_mac(self.MAC_REAL)
+
+        self.src_port = {'admin_state_up': True,
+                         'device': self.src_br_veth.name,
+                         'device_owner': 'compute:None',
+                         'fixed_ips': [self.SRC_ADDRESS],
+                         'mac_address': self.MAC_REAL,
+                         'port_security_enabled': True,
+                         'security_groups': [self.FAKE_SECURITY_GROUP_ID],
+                         'status': 'ACTIVE'}
+
     # setup firewall on bridge and send packet from src_veth and observe
     # if sent packet can be observed on dst_veth
-    def test_firewall(self):
-        pass
+    def test_port_sec_within_firewall(self):
+        pinger = helpers.Pinger(ip_lib.IPWrapper(self.src_veth.namespace))
+
+        # update the sg_group to make ping pass
+        sg_rules = [{'ethertype': 'IPv4', 'direction': 'ingress',
+                     'source_ip_prefix': '0.0.0.0/0', 'protocol': 'icmp'},
+                    {'ethertype': 'IPv4', 'direction': 'egress'}]
+
+        with self.firewall.defer_apply():
+            self.firewall.update_security_group_rules(
+                                                self.FAKE_SECURITY_GROUP_ID,
+                                                sg_rules)
+        self.firewall.prepare_port_filter(self.src_port)
+        pinger.assert_ping(self.DST_ADDRESS)
+
+        # modify the src_veth's MAC and test again
+        self._set_src_mac(self.MAC_SPOOFED)
+        pinger.assert_no_ping(self.DST_ADDRESS)
+
+        # update the port's port_security_enabled value and test again
+        self.src_port['port_security_enabled'] = False
+        self.firewall.update_port_filter(self.src_port)
+        pinger.assert_ping(self.DST_ADDRESS)
diff --git a/neutron/tests/unit/ml2/test_ext_portsecurity.py b/neutron/tests/unit/ml2/test_ext_portsecurity.py
new file mode 100644 (file)
index 0000000..28fa68d
--- /dev/null
@@ -0,0 +1,29 @@
+# Copyright (c) 2015 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 neutron.plugins.ml2 import config
+from neutron.tests.unit.ml2 import test_ml2_plugin
+from neutron.tests.unit import test_extension_portsecurity as test_psec
+
+
+class PSExtDriverTestCase(test_ml2_plugin.Ml2PluginV2TestCase,
+                          test_psec.TestPortSecurity):
+    _extension_drivers = ['port_security']
+
+    def setUp(self):
+        config.cfg.CONF.set_override('extension_drivers',
+                                     self._extension_drivers,
+                                     group='ml2')
+        super(PSExtDriverTestCase, self).setUp()
index 591a9fac28e4dfd577a16d8aa2f435bb98842cc8..a7664058e7b8c742b957849867d91b6e64fa3b08 100644 (file)
@@ -166,7 +166,7 @@ class PortSecurityTestPlugin(db_base_plugin_v2.NeutronDbPluginV2,
 
 
 class PortSecurityDBTestCase(PortSecurityTestCase):
-    def setUp(self, plugin=None):
+    def setUp(self, plugin=None, service_plugins=None):
         plugin = plugin or DB_PLUGIN_KLASS
         super(PortSecurityDBTestCase, self).setUp(plugin)
 
@@ -279,7 +279,9 @@ class TestPortSecurity(PortSecurityDBTestCase):
             'json', self._create_security_group(self.fmt, 'asdf', 'asdf'))
         security_group_id = security_group['security_group']['id']
         res = self._create_port('json', net['network']['id'],
-                                arg_list=('security_groups',),
+                                arg_list=('security_groups',
+                                    'port_security_enabled'),
+                                port_security_enabled=True,
                                 security_groups=[security_group_id])
         port = self.deserialize('json', res)
         self.assertEqual(port['port'][psec.PORTSECURITY], True)
index 9e8db6732023c70d30e61bbb151ef1bff97c0094..8a22f827dae663b12708ff81b9aa49c76387048f 100644 (file)
@@ -1213,12 +1213,12 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
         self.firewall.prepare_port_filter(port_prepare)
         self.firewall.update_port_filter(port_update)
         self.firewall.remove_port_filter(port_update)
-        chain_applies.assert_has_calls([mock.call.remove({}),
-                                        mock.call.setup({'d1': port_prepare}),
-                                        mock.call.remove({'d1': port_prepare}),
-                                        mock.call.setup({'d1': port_update}),
-                                        mock.call.remove({'d1': port_update}),
-                                        mock.call.setup({})])
+        chain_applies.assert_has_calls([mock.call.remove({}, {}),
+                                mock.call.setup({'d1': port_prepare}, {}),
+                                mock.call.remove({'d1': port_prepare}, {}),
+                                mock.call.setup({'d1': port_update}, {}),
+                                mock.call.remove({'d1': port_update}, {}),
+                                mock.call.setup({}, {})])
 
     def test_defer_chain_apply_need_pre_defer_copy(self):
         chain_applies = self._mock_chain_applies()
@@ -1227,10 +1227,10 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
         self.firewall.prepare_port_filter(port)
         with self.firewall.defer_apply():
             self.firewall.remove_port_filter(port)
-        chain_applies.assert_has_calls([mock.call.remove({}),
-                                        mock.call.setup(device2port),
-                                        mock.call.remove(device2port),
-                                        mock.call.setup({})])
+        chain_applies.assert_has_calls([mock.call.remove({}, {}),
+                                        mock.call.setup(device2port, {}),
+                                        mock.call.remove(device2port, {}),
+                                        mock.call.setup({}, {})])
 
     def test_defer_chain_apply_coalesce_simple(self):
         chain_applies = self._mock_chain_applies()
@@ -1239,8 +1239,8 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
             self.firewall.prepare_port_filter(port)
             self.firewall.update_port_filter(port)
             self.firewall.remove_port_filter(port)
-        chain_applies.assert_has_calls([mock.call.remove({}),
-                                        mock.call.setup({})])
+        chain_applies.assert_has_calls([mock.call.remove({}, {}),
+                                        mock.call.setup({}, {})])
 
     def test_defer_chain_apply_coalesce_multiple_ports(self):
         chain_applies = self._mock_chain_applies()
@@ -1250,8 +1250,8 @@ class IptablesFirewallTestCase(BaseIptablesFirewallTestCase):
         with self.firewall.defer_apply():
             self.firewall.prepare_port_filter(port1)
             self.firewall.prepare_port_filter(port2)
-        chain_applies.assert_has_calls([mock.call.remove({}),
-                                        mock.call.setup(device2port)])
+        chain_applies.assert_has_calls([mock.call.remove({}, {}),
+                                        mock.call.setup(device2port, {})])
 
     def test_ip_spoofing_filter_with_multiple_ips(self):
         port = {'device': 'tapfake_dev',
@@ -1642,6 +1642,7 @@ class IptablesFirewallEnhancedIpsetTestCase(BaseIptablesFirewallTestCase):
         port = self._fake_port()
         self.firewall.filtered_ports['tapfake_dev'] = port
         self.firewall._pre_defer_filtered_ports = {}
+        self.firewall._pre_defer_unfiltered_ports = {}
         self.firewall.filter_defer_apply_off()
         calls = [mock.call.destroy('fake_sgid', 'IPv4')]
 
index 126fa549debc2194e63e932a3aa2a904044638c1..a49631b9bf3b326b8e117ea3554c98733e1a6287 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -183,6 +183,7 @@ neutron.ml2.mechanism_drivers =
 neutron.ml2.extension_drivers =
     test = neutron.tests.unit.ml2.drivers.ext_test:TestExtensionDriver
     testdb = neutron.tests.unit.ml2.drivers.ext_test:TestDBExtensionDriver
+    port_security = neutron.plugins.ml2.extensions.port_security:PortSecurityExtensionDriver
 neutron.openstack.common.cache.backends =
     memory = neutron.openstack.common.cache._backends.memory:MemoryBackend
 # These are for backwards compat with Icehouse notification_driver configuration values