diff --git a/etc/policy.json b/etc/policy.json index a9d44b1f10b..0745f0541a5 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -58,6 +58,26 @@ "create_router:external_gateway_info:enable_snat": "rule:admin_only", "update_router:external_gateway_info:enable_snat": "rule:admin_only", + "create_firewall": "", + "get_firewall": "rule:admin_or_owner", + "create_firewall:shared": "rule:admin_only", + "get_firewall:shared": "rule:admin_only", + "update_firewall": "rule:admin_or_owner", + "delete_firewall": "rule:admin_or_owner", + + "create_firewall_policy": "", + "get_firewall_policy": "rule:admin_or_owner", + "create_firewall_policy:shared": "rule:admin_or_owner", + "update_firewall_policy": "rule:admin_or_owner", + "delete_firewall_policy": "rule:admin_or_owner", + + "create_firewall_rule": "", + "get_firewall_rule": "rule:admin_or_owner", + "create_firewall_rule:shared": "rule:admin_or_owner", + "get_firewall_rule:shared": "rule:admin_or_owner", + "update_firewall_rule": "rule:admin_or_owner", + "delete_firewall_rule": "rule:admin_or_owner", + "create_qos_queue": "rule:admin_only", "get_qos_queue": "rule:admin_only", diff --git a/neutron/common/topics.py b/neutron/common/topics.py index a766430c58b..9b3513e7ea9 100644 --- a/neutron/common/topics.py +++ b/neutron/common/topics.py @@ -25,6 +25,7 @@ UPDATE = 'update' AGENT = 'q-agent-notifier' PLUGIN = 'q-plugin' DHCP = 'q-dhcp-notifer' +FIREWALL_PLUGIN = 'q-firewall-plugin' L3_AGENT = 'l3_agent' DHCP_AGENT = 'dhcp_agent' diff --git a/neutron/db/firewall/__init__.py b/neutron/db/firewall/__init__.py new file mode 100644 index 00000000000..d1e3641618c --- /dev/null +++ b/neutron/db/firewall/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 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. diff --git a/neutron/db/firewall/firewall_db.py b/neutron/db/firewall/firewall_db.py new file mode 100644 index 00000000000..846171f37b7 --- /dev/null +++ b/neutron/db/firewall/firewall_db.py @@ -0,0 +1,470 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Big Switch Networks, 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. +# +# @author: Sumit Naiksatam, sumitnaiksatam@gmail.com, Big Switch Networks, Inc. + +import sqlalchemy as sa +from sqlalchemy.ext.orderinglist import ordering_list +from sqlalchemy import orm +from sqlalchemy.orm import exc + +from neutron.db import db_base_plugin_v2 as base_db +from neutron.db import model_base +from neutron.db import models_v2 +from neutron.extensions import firewall +from neutron import manager +from neutron.openstack.common import log as logging +from neutron.openstack.common import uuidutils +from neutron.plugins.common import constants as const + + +LOG = logging.getLogger(__name__) + + +class FirewallRule(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant): + """Represents a Firewall rule.""" + __tablename__ = 'firewall_rules' + name = sa.Column(sa.String(255)) + description = sa.Column(sa.String(1024)) + firewall_policy_id = sa.Column(sa.String(36), + sa.ForeignKey('firewall_policies.id'), + nullable=True) + shared = sa.Column(sa.Boolean) + protocol = sa.Column(sa.String(40)) + ip_version = sa.Column(sa.Integer, nullable=False) + source_ip_address = sa.Column(sa.String(46)) + destination_ip_address = sa.Column(sa.String(46)) + source_port_range_min = sa.Column(sa.Integer) + source_port_range_max = sa.Column(sa.Integer) + destination_port_range_min = sa.Column(sa.Integer) + destination_port_range_max = sa.Column(sa.Integer) + action = sa.Column(sa.Enum('allow', 'deny', name='firewallrules_action')) + enabled = sa.Column(sa.Boolean) + position = sa.Column(sa.Integer) + + +class Firewall(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant): + """Represents a Firewall resource.""" + __tablename__ = 'firewalls' + name = sa.Column(sa.String(255)) + description = sa.Column(sa.String(1024)) + shared = sa.Column(sa.Boolean) + admin_state_up = sa.Column(sa.Boolean) + status = sa.Column(sa.String(16)) + firewall_policy_id = sa.Column(sa.String(36), + sa.ForeignKey('firewall_policies.id'), + nullable=True) + + +class FirewallPolicy(model_base.BASEV2, models_v2.HasId, models_v2.HasTenant): + """Represents a Firewall Policy resource.""" + __tablename__ = 'firewall_policies' + name = sa.Column(sa.String(255)) + description = sa.Column(sa.String(1024)) + shared = sa.Column(sa.Boolean) + firewall_rules = orm.relationship( + FirewallRule, + backref=orm.backref('firewall_policies', cascade='all, delete'), + order_by='FirewallRule.position', + collection_class=ordering_list('position', count_from=1)) + audited = sa.Column(sa.Boolean) + firewalls = orm.relationship(Firewall, backref='firewall_policies') + + +class Firewall_db_mixin(firewall.FirewallPluginBase, base_db.CommonDbMixin): + """Mixin class for Firewall DB implementation.""" + + @property + def _core_plugin(self): + return manager.NeutronManager.get_plugin() + + def _get_firewall(self, context, id): + try: + return self._get_by_id(context, Firewall, id) + except exc.NoResultFound: + raise firewall.FirewallNotFound(firewall_id=id) + + def _get_firewall_policy(self, context, id): + try: + return self._get_by_id(context, FirewallPolicy, id) + except exc.NoResultFound: + raise firewall.FirewallPolicyNotFound(firewall_policy_id=id) + + def _get_firewall_rule(self, context, id): + try: + return self._get_by_id(context, FirewallRule, id) + except exc.NoResultFound: + raise firewall.FirewallRuleNotFound(firewall_rule_id=id) + + def _make_firewall_dict(self, fw, fields=None): + res = {'id': fw['id'], + 'tenant_id': fw['tenant_id'], + 'name': fw['name'], + 'description': fw['description'], + 'shared': fw['shared'], + 'admin_state_up': fw['admin_state_up'], + 'status': fw['status'], + 'firewall_policy_id': fw['firewall_policy_id']} + return self._fields(res, fields) + + def _make_firewall_policy_dict(self, firewall_policy, fields=None): + fw_rules = [rule['id'] for rule in firewall_policy['firewall_rules']] + firewalls = [fw['id'] for fw in firewall_policy['firewalls']] + res = {'id': firewall_policy['id'], + 'tenant_id': firewall_policy['tenant_id'], + 'name': firewall_policy['name'], + 'description': firewall_policy['description'], + 'shared': firewall_policy['shared'], + 'audited': firewall_policy['audited'], + 'firewall_rules': fw_rules, + 'firewall_list': firewalls} + return self._fields(res, fields) + + def _make_firewall_rule_dict(self, firewall_rule, fields=None): + position = None + # We return the position only if the firewall_rule is bound to a + # firewall_policy. + if firewall_rule['firewall_policy_id']: + position = firewall_rule['position'] + src_port_range = self._get_port_range_from_min_max_ports( + firewall_rule['source_port_range_min'], + firewall_rule['source_port_range_max']) + dst_port_range = self._get_port_range_from_min_max_ports( + firewall_rule['destination_port_range_min'], + firewall_rule['destination_port_range_max']) + res = {'id': firewall_rule['id'], + 'tenant_id': firewall_rule['tenant_id'], + 'name': firewall_rule['name'], + 'description': firewall_rule['description'], + 'firewall_policy_id': firewall_rule['firewall_policy_id'], + 'shared': firewall_rule['shared'], + 'protocol': firewall_rule['protocol'], + 'ip_version': firewall_rule['ip_version'], + 'source_ip_address': firewall_rule['source_ip_address'], + 'destination_ip_address': + firewall_rule['destination_ip_address'], + 'source_port': src_port_range, + 'destination_port': dst_port_range, + 'action': firewall_rule['action'], + 'position': position, + 'enabled': firewall_rule['enabled']} + return self._fields(res, fields) + + def _set_rules_for_policy(self, context, firewall_policy_db, rule_id_list): + fwp_db = firewall_policy_db + with context.session.begin(subtransactions=True): + if not rule_id_list: + fwp_db.firewall_rules = [] + fwp_db.audited = False + return + # We will first check if the new list of rules is valid + filters = {'id': [r_id for r_id in rule_id_list]} + rules_in_db = self._get_collection_query(context, FirewallRule, + filters=filters) + rules_dict = dict((fwr_db['id'], fwr_db) for fwr_db in rules_in_db) + for fwrule_id in rule_id_list: + if fwrule_id not in rules_dict: + # If we find an invalid rule in the list we + # do not perform the update since this breaks + # the integrity of this list. + raise firewall.FirewallRuleNotFound(firewall_rule_id= + fwrule_id) + # New list of rules is valid so we will first reset the existing + # list and then add each rule in order. + # Note that the list could be empty in which case we interpret + # it as clearing existing rules. + fwp_db.firewall_rules = [] + for fwrule_id in rule_id_list: + fwp_db.firewall_rules.append(rules_dict[fwrule_id]) + fwp_db.audited = False + + def _process_rule_for_policy(self, context, firewall_policy_id, + firewall_rule_db, position): + with context.session.begin(subtransactions=True): + fwp_query = context.session.query( + FirewallPolicy).with_lockmode('update') + fwp_db = fwp_query.filter_by(id=firewall_policy_id).one() + if position: + # Note that although position numbering starts at 1, + # internal ordering of the list starts at 0, so we compensate. + fwp_db.firewall_rules.insert(position - 1, firewall_rule_db) + else: + fwp_db.firewall_rules.remove(firewall_rule_db) + fwp_db.firewall_rules.reorder() + fwp_db.audited = False + return self._make_firewall_policy_dict(fwp_db) + + def _get_min_max_ports_from_range(self, port_range): + if not port_range: + return [None, None] + ports = port_range.split(':') + ports[0] = int(ports[0]) + if len(ports) < 2: + ports.append(ports[0]) + else: + ports[1] = int(ports[1]) + return ports + + def _get_port_range_from_min_max_ports(self, min_port, max_port): + if not min_port: + return None + if min_port == max_port: + return str(min_port) + else: + return str(min_port) + ':' + str(max_port) + + def create_firewall(self, context, firewall): + LOG.debug(_("create_firewall() called")) + fw = firewall['firewall'] + tenant_id = self._get_tenant_id_for_create(context, fw) + with context.session.begin(subtransactions=True): + firewall_db = Firewall(id=uuidutils.generate_uuid(), + tenant_id=tenant_id, + name=fw['name'], + description=fw['description'], + firewall_policy_id= + fw['firewall_policy_id'], + admin_state_up=fw['admin_state_up'], + status=const.PENDING_CREATE) + context.session.add(firewall_db) + return self._make_firewall_dict(firewall_db) + + def update_firewall(self, context, id, firewall): + LOG.debug(_("update_firewall() called")) + fw = firewall['firewall'] + with context.session.begin(subtransactions=True): + fw_query = context.session.query( + Firewall).with_lockmode('update') + firewall_db = fw_query.filter_by(id=id).one() + firewall_db.update(fw) + return self._make_firewall_dict(firewall_db) + + def delete_firewall(self, context, id): + LOG.debug(_("delete_firewall() called")) + with context.session.begin(subtransactions=True): + fw_query = context.session.query( + Firewall).with_lockmode('update') + firewall_db = fw_query.filter_by(id=id).one() + # Note: Plugin should ensure that it's okay to delete if the + # firewall is active + context.session.delete(firewall_db) + + def get_firewall(self, context, id, fields=None): + LOG.debug(_("get_firewall() called")) + fw = self._get_firewall(context, id) + return self._make_firewall_dict(fw, fields) + + def get_firewalls(self, context, filters=None, fields=None): + LOG.debug(_("get_firewalls() called")) + return self._get_collection(context, Firewall, + self._make_firewall_dict, + filters=filters, fields=fields) + + def get_firewalls_count(self, context, filters=None): + LOG.debug(_("get_firewalls_count() called")) + return self._get_collection_count(context, Firewall, + filters=filters) + + def create_firewall_policy(self, context, firewall_policy): + LOG.debug(_("create_firewall_policy() called")) + fwp = firewall_policy['firewall_policy'] + tenant_id = self._get_tenant_id_for_create(context, fwp) + with context.session.begin(subtransactions=True): + fwp_db = FirewallPolicy(id=uuidutils.generate_uuid(), + tenant_id=tenant_id, + name=fwp['name'], + description=fwp['description'], + shared=fwp['shared']) + context.session.add(fwp_db) + self._set_rules_for_policy(context, fwp_db, + fwp['firewall_rules']) + fwp_db.audited = fwp['audited'] + return self._make_firewall_policy_dict(fwp_db) + + def update_firewall_policy(self, context, id, firewall_policy): + LOG.debug(_("update_firewall_policy() called")) + fwp = firewall_policy['firewall_policy'] + with context.session.begin(subtransactions=True): + fwp_db = self._get_firewall_policy(context, id) + if 'firewall_rules' in fwp: + self._set_rules_for_policy(context, fwp_db, + fwp['firewall_rules']) + del fwp['firewall_rules'] + fwp_db.update(fwp) + return self._make_firewall_policy_dict(fwp_db) + + def delete_firewall_policy(self, context, id): + LOG.debug(_("delete_firewall_policy() called")) + with context.session.begin(subtransactions=True): + fwp = self._get_firewall_policy(context, id) + # Ensure that the firewall_policy is not + # being used + qry = context.session.query(Firewall) + if qry.filter_by(firewall_policy_id=id).first(): + raise firewall.FirewallPolicyInUse(firewall_policy_id=id) + else: + context.session.delete(fwp) + + def get_firewall_policy(self, context, id, fields=None): + LOG.debug(_("get_firewall_policy() called")) + fwp = self._get_firewall_policy(context, id) + return self._make_firewall_policy_dict(fwp, fields) + + def get_firewall_policies(self, context, filters=None, fields=None): + LOG.debug(_("get_firewall_policies() called")) + return self._get_collection(context, FirewallPolicy, + self._make_firewall_policy_dict, + filters=filters, fields=fields) + + def get_firewalls_policies_count(self, context, filters=None): + LOG.debug(_("get_firewall_policies_count() called")) + return self._get_collection_count(context, FirewallPolicy, + filters=filters) + + def create_firewall_rule(self, context, firewall_rule): + LOG.debug(_("create_firewall_rule() called")) + fwr = firewall_rule['firewall_rule'] + tenant_id = self._get_tenant_id_for_create(context, fwr) + src_port_min, src_port_max = self._get_min_max_ports_from_range( + fwr['source_port']) + dst_port_min, dst_port_max = self._get_min_max_ports_from_range( + fwr['destination_port']) + with context.session.begin(subtransactions=True): + fwr_db = FirewallRule(id=uuidutils.generate_uuid(), + tenant_id=tenant_id, + name=fwr['name'], + description=fwr['description'], + shared=fwr['shared'], + protocol=fwr['protocol'], + ip_version=fwr['ip_version'], + source_ip_address=fwr['source_ip_address'], + destination_ip_address= + fwr['destination_ip_address'], + source_port_range_min=src_port_min, + source_port_range_max=src_port_max, + destination_port_range_min=dst_port_min, + destination_port_range_max=dst_port_max, + action=fwr['action'], + enabled=fwr['enabled']) + context.session.add(fwr_db) + return self._make_firewall_rule_dict(fwr_db) + + def update_firewall_rule(self, context, id, firewall_rule): + LOG.debug(_("update_firewall_rule() called")) + fwr = firewall_rule['firewall_rule'] + if 'source_port' in fwr: + src_port_min, src_port_max = self._get_min_max_ports_from_range( + fwr['source_port']) + fwr['source_port_range_min'] = src_port_min + fwr['source_port_range_max'] = src_port_max + del fwr['source_port'] + if 'destination_port' in fwr: + dst_port_min, dst_port_max = self._get_min_max_ports_from_range( + fwr['destination_port']) + fwr['destination_port_range_min'] = dst_port_min + fwr['destination_port_range_max'] = dst_port_max + del fwr['destination_port'] + with context.session.begin(subtransactions=True): + fwr_db = self._get_firewall_rule(context, id) + fwr_db.update(fwr) + if fwr_db.firewall_policy_id: + fwp_db = self._get_firewall_policy(context, + fwr_db.firewall_policy_id) + fwp_db.audited = False + return self._make_firewall_rule_dict(fwr_db) + + def delete_firewall_rule(self, context, id): + LOG.debug(_("delete_firewall_rule() called")) + with context.session.begin(subtransactions=True): + fwr = self._get_firewall_rule(context, id) + if fwr.firewall_policy_id: + raise firewall.FirewallRuleInUse(firewall_rule_id=id) + context.session.delete(fwr) + + def get_firewall_rule(self, context, id, fields=None): + LOG.debug(_("get_firewall_rule() called")) + fwr = self._get_firewall_rule(context, id) + return self._make_firewall_rule_dict(fwr, fields) + + def get_firewall_rules(self, context, filters=None, fields=None): + LOG.debug(_("get_firewall_rules() called")) + return self._get_collection(context, FirewallRule, + self._make_firewall_rule_dict, + filters=filters, fields=fields) + + def get_firewalls_rules_count(self, context, filters=None): + LOG.debug(_("get_firewall_rules_count() called")) + return self._get_collection_count(context, FirewallRule, + filters=filters) + + def _validate_insert_remove_rule_request(self, id, rule_info): + if not rule_info or 'firewall_rule_id' not in rule_info: + raise firewall.FirewallRuleInfoMissing() + + def insert_rule(self, context, id, rule_info): + LOG.debug(_("insert_rule() called")) + self._validate_insert_remove_rule_request(id, rule_info) + firewall_rule_id = rule_info['firewall_rule_id'] + insert_before = True + ref_firewall_rule_id = None + if not firewall_rule_id: + raise firewall.FirewallRuleNotFound(firewall_rule_id=None) + if 'insert_before' in rule_info: + ref_firewall_rule_id = rule_info['insert_before'] + if not ref_firewall_rule_id and 'insert_after' in rule_info: + # If insert_before is set, we will ignore insert_after. + ref_firewall_rule_id = rule_info['insert_after'] + insert_before = False + with context.session.begin(subtransactions=True): + fwr_db = self._get_firewall_rule(context, firewall_rule_id) + if fwr_db.firewall_policy_id: + raise firewall.FirewallRuleInUse(firewall_rule_id=fwr_db['id']) + if ref_firewall_rule_id: + # If reference_firewall_rule_id is set, the new rule + # is inserted depending on the value of insert_before. + # If insert_before is set, the new rule is inserted before + # reference_firewall_rule_id, and if it is not set the new + # rule is inserted after reference_firewall_rule_id. + ref_fwr_db = self._get_firewall_rule( + context, ref_firewall_rule_id) + if insert_before: + position = ref_fwr_db.position + else: + position = ref_fwr_db.position + 1 + else: + # If reference_firewall_rule_id is not set, it is assumed + # that the new rule needs to be inserted at the top. + # insert_before field is ignored. + # So default insertion is always at the top. + # Also note that position numbering starts at 1. + position = 1 + return self._process_rule_for_policy(context, id, fwr_db, + position) + + def remove_rule(self, context, id, rule_info): + LOG.debug(_("remove_rule() called")) + self._validate_insert_remove_rule_request(id, rule_info) + firewall_rule_id = rule_info['firewall_rule_id'] + if not firewall_rule_id: + raise firewall.FirewallRuleNotFound(firewall_rule_id=None) + with context.session.begin(subtransactions=True): + fwr_db = self._get_firewall_rule(context, firewall_rule_id) + if fwr_db.firewall_policy_id != id: + raise firewall.FirewallRuleNotAssociatedWithPolicy( + firewall_rule_id=fwr_db['id'], + firewall_policy_id=id) + return self._process_rule_for_policy(context, id, fwr_db, None) diff --git a/neutron/db/migration/alembic_migrations/versions/39cf3f799352_fwaas_havana_2_model.py b/neutron/db/migration/alembic_migrations/versions/39cf3f799352_fwaas_havana_2_model.py new file mode 100644 index 00000000000..59736ec541b --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/39cf3f799352_fwaas_havana_2_model.py @@ -0,0 +1,105 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 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. +# + +"""FWaaS Havana-2 model + +Revision ID: 39cf3f799352 +Revises: e6b16a30d97 +Create Date: 2013-07-10 16:16:51.302943 + +""" + +# revision identifiers, used by Alembic. +revision = '39cf3f799352' +down_revision = 'e6b16a30d97' + +# Change to ['*'] if this migration applies to all plugins + +migration_for_plugins = ['*'] + +from alembic import op +import sqlalchemy as sa + +from neutron.db import migration + + +def downgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + op.drop_table('firewall_rules') + op.drop_table('firewalls') + op.drop_table('firewall_policies') + + +def upgrade(active_plugin=None, options=None): + if not migration.should_run(active_plugin, migration_for_plugins): + return + + op.create_table( + 'firewall_policies', + sa.Column('tenant_id', sa.String(length=255), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('description', sa.String(length=1024), nullable=True), + sa.Column('shared', sa.Boolean(), autoincrement=False, nullable=True), + sa.Column('audited', sa.Boolean(), autoincrement=False, + nullable=True), + sa.PrimaryKeyConstraint('id')) + op.create_table( + 'firewalls', sa.Column('tenant_id', sa.String(length=255), + nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('description', sa.String(length=1024), nullable=True), + sa.Column('shared', sa.Boolean(), autoincrement=False, nullable=True), + sa.Column('admin_state_up', sa.Boolean(), autoincrement=False, + nullable=True), + sa.Column('status', sa.String(length=16), nullable=True), + sa.Column('firewall_policy_id', sa.String(length=36), nullable=True), + sa.ForeignKeyConstraint(['firewall_policy_id'], + ['firewall_policies.id'], + name='firewalls_ibfk_1'), + sa.PrimaryKeyConstraint('id')) + op.create_table( + 'firewall_rules', + sa.Column('tenant_id', sa.String(length=255), nullable=True), + sa.Column('id', sa.String(length=36), nullable=False), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('description', sa.String(length=1024), nullable=True), + sa.Column('firewall_policy_id', sa.String(length=36), nullable=True), + sa.Column('shared', sa.Boolean(), autoincrement=False, + nullable=True), + sa.Column('protocol', sa.String(length=24), nullable=True), + sa.Column('ip_version', sa.Integer(), autoincrement=False, + nullable=False), + sa.Column('source_ip_address', sa.String(length=46), nullable=True), + sa.Column('destination_ip_address', sa.String(length=46), + nullable=True), + sa.Column('source_port_range_min', sa.Integer(), nullable=True), + sa.Column('source_port_range_max', sa.Integer(), nullable=True), + sa.Column('destination_port_range_min', sa.Integer(), nullable=True), + sa.Column('destination_port_range_max', sa.Integer(), nullable=True), + sa.Column('action', sa.Enum(), nullable=True), + sa.Column('enabled', sa.Boolean(), autoincrement=False, + nullable=True), + sa.Column('position', sa.Integer(), autoincrement=False, + nullable=True), + sa.ForeignKeyConstraint(['firewall_policy_id'], + ['firewall_policies.id'], + name='firewall_rules_ibfk_1'), + sa.PrimaryKeyConstraint('id')) diff --git a/neutron/extensions/firewall.py b/neutron/extensions/firewall.py new file mode 100644 index 00000000000..ca7530e5f0a --- /dev/null +++ b/neutron/extensions/firewall.py @@ -0,0 +1,448 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Big Switch Networks, 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. +# +# @author: Sumit Naiksatam, sumitnaiksatam@gmail.com, Big Switch Networks, Inc. + +import abc + +from oslo.config import cfg + +from neutron.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.api.v2 import base +from neutron.common import exceptions as qexception +from neutron import manager +from neutron.openstack.common import log as logging +from neutron.plugins.common import constants +from neutron.services.service_base import ServicePluginBase + + +LOG = logging.getLogger(__name__) + + +# Firewall Exceptions +class FirewallNotFound(qexception.NotFound): + message = _("Firewall %(firewall_id)s could not be found.") + + +class FirewallInUse(qexception.InUse): + message = _("Firewall %(firewall_id)s is still active.") + + +class FirewallInPendingState(qexception.Conflict): + message = _("Operation cannot be performed since associated Firewall " + "%(firewall_id)s is in %(pending_state)s.") + + +class FirewallPolicyNotFound(qexception.NotFound): + message = _("Firewall Policy %(firewall_policy_id)s could not be found.") + + +class FirewallPolicyInUse(qexception.InUse): + message = _("Firewall Policy %(firewall_policy_id)s is being used.") + + +class FirewallRuleNotFound(qexception.NotFound): + message = _("Firewall Rule %(firewall_rule_id)s could not be found.") + + +class FirewallRuleInUse(qexception.InUse): + message = _("Firewall Rule %(firewall_rule_id)s is being used.") + + +class FirewallRuleNotAssociatedWithPolicy(qexception.InvalidInput): + message = _("Firewall Rule %(firewall_rule_id)s is not associated " + " with Firewall Policy %(firewall_policy_id)s.") + + +class FirewallRuleInvalidProtocol(qexception.InvalidInput): + message = _("Firewall Rule protocol %(protocol)s is not supported. " + "Only protocol values %(values)s and their integer " + "representation (0 to 255) are supported.") + + +class FirewallRuleInvalidAction(qexception.InvalidInput): + message = _("Firewall rule action %(action)s is not supported. " + "Only action values %(values)s are supported.") + + +class FirewallInvalidPortValue(qexception.InvalidInput): + message = _("Invalid value for port %(port)s.") + + +class FirewallRuleInfoMissing(qexception.InvalidInput): + message = _("Missing rule info argument for insert/remove " + "rule opertaion.") + + +fw_valid_protocol_values = [None, constants.TCP, constants.UDP, constants.ICMP] +fw_valid_action_values = [constants.FWAAS_ALLOW, constants.FWAAS_DENY] + + +def convert_protocol(value): + if value is None: + return + if value.isdigit(): + val = int(value) + if 0 <= val <= 255: + return val + else: + raise FirewallRuleInvalidProtocol(protocol=value, + values= + fw_valid_protocol_values) + elif value.lower() in fw_valid_protocol_values: + return value.lower() + else: + raise FirewallRuleInvalidProtocol(protocol=value, + values= + fw_valid_protocol_values) + + +def convert_action_to_case_insensitive(value): + if value is None: + return + else: + return value.lower() + + +def convert_port_to_string(value): + if value is None: + return + else: + return str(value) + + +def _validate_port_range(data, key_specs=None): + if data is None: + return + data = str(data) + ports = data.split(':') + for p in ports: + try: + val = int(p) + except (ValueError, TypeError): + msg = _("Port '%s' is not a valid number") % p + LOG.debug(msg) + return msg + if val <= 0 or val > 65535: + msg = _("Invalid port '%s'") % p + LOG.debug(msg) + return msg + + +def _validate_ip_or_subnet_or_none(data, valid_values=None): + if data is None: + return None + msg_ip = attr._validate_ip_address(data, valid_values) + if not msg_ip: + return + msg_subnet = attr._validate_subnet(data, valid_values) + if not msg_subnet: + return + return _("%(msg_ip)s and %(msg_subnet)s") % {'msg_ip': msg_ip, + 'msg_subnet': msg_subnet} + + +attr.validators['type:port_range'] = _validate_port_range +attr.validators['type:ip_or_subnet_or_none'] = _validate_ip_or_subnet_or_none + + +RESOURCE_ATTRIBUTE_MAP = { + 'firewall_rules': { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, 'primary_key': True}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'is_visible': True}, + 'name': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, 'default': ''}, + 'description': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, 'default': ''}, + 'firewall_policy_id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid_or_none': None}, + 'is_visible': True}, + 'shared': {'allow_post': True, 'allow_put': True, + 'default': False, 'convert_to': attr.convert_to_boolean, + 'is_visible': True, 'required_by_policy': True, + 'enforce_policy': True}, + 'protocol': {'allow_post': True, 'allow_put': True, + 'is_visible': True, 'default': None, + 'convert_to': convert_protocol, + 'validate': {'type:values': fw_valid_protocol_values}}, + 'ip_version': {'allow_post': True, 'allow_put': True, + 'default': 4, 'convert_to': attr.convert_to_int, + 'validate': {'type:values': [4, 6]}, + 'is_visible': True}, + 'source_ip_address': {'allow_post': True, 'allow_put': True, + 'validate': {'type:ip_or_subnet_or_none': None}, + 'is_visible': True, 'default': None}, + 'destination_ip_address': {'allow_post': True, 'allow_put': True, + 'validate': {'type:ip_or_subnet_or_none': + None}, + 'is_visible': True, 'default': None}, + 'source_port': {'allow_post': True, 'allow_put': True, + 'validate': {'type:port_range': None}, + 'convert_to': convert_port_to_string, + 'default': None, 'is_visible': True}, + 'destination_port': {'allow_post': True, 'allow_put': True, + 'validate': {'type:port_range': None}, + 'convert_to': convert_port_to_string, + 'default': None, 'is_visible': True}, + 'position': {'allow_post': False, 'allow_put': False, + 'default': None, 'is_visible': True}, + 'action': {'allow_post': True, 'allow_put': True, + 'convert_to': convert_action_to_case_insensitive, + 'validate': {'type:values': fw_valid_action_values}, + 'is_visible': True, 'default': 'deny'}, + 'enabled': {'allow_post': True, 'allow_put': True, + 'default': True, 'convert_to': attr.convert_to_boolean, + 'is_visible': True}, + }, + 'firewall_policies': { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'is_visible': True}, + 'name': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, 'default': ''}, + 'description': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, 'default': ''}, + 'shared': {'allow_post': True, 'allow_put': True, + 'default': False, 'convert_to': attr.convert_to_boolean, + 'is_visible': True, 'required_by_policy': True, + 'enforce_policy': True}, + 'firewall_rules': {'allow_post': True, 'allow_put': True, + 'convert_to': attr.convert_none_to_empty_list, + 'default': None, 'is_visible': True}, + 'audited': {'allow_post': True, 'allow_put': True, + 'default': False, 'convert_to': attr.convert_to_boolean, + 'is_visible': True}, + }, + 'firewalls': { + 'id': {'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True}, + 'tenant_id': {'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'is_visible': True}, + 'name': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, 'default': ''}, + 'description': {'allow_post': True, 'allow_put': True, + 'validate': {'type:string': None}, + 'is_visible': True, 'default': ''}, + 'admin_state_up': {'allow_post': True, 'allow_put': True, + 'default': True, + 'convert_to': attr.convert_to_boolean, + 'is_visible': True}, + 'status': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + 'shared': {'allow_post': True, 'allow_put': True, + 'default': False, 'convert_to': attr.convert_to_boolean, + 'is_visible': False, 'required_by_policy': True, + 'enforce_policy': True}, + 'firewall_policy_id': {'allow_post': True, 'allow_put': True, + 'validate': {'type:uuid_or_none': None}, + 'is_visible': True}, + }, +} + +firewall_quota_opts = [ + cfg.IntOpt('quota_firewall', + default=1, + help=_('Number of firewalls allowed per tenant, -1 for ' + 'unlimited')), + cfg.IntOpt('quota_firewall_policy', + default=1, + help=_('Number of firewall policies allowed per tenant, -1 ' + 'for unlimited')), + cfg.IntOpt('quota_firewall_rule', + default=-1, + help=_('Number of firewall rules allowed per tenant, -1 ' + 'for unlimited')), +] +cfg.CONF.register_opts(firewall_quota_opts, 'QUOTAS') + + +class Firewall(extensions.ExtensionDescriptor): + + @classmethod + def get_name(cls): + return "Firewall service" + + @classmethod + def get_alias(cls): + return "fwaas" + + @classmethod + def get_description(cls): + return "Extension for Firewall service" + + @classmethod + def get_namespace(cls): + return "http://wiki.openstack.org/Neutron/FWaaS/API_1.0" + + @classmethod + def get_updated(cls): + return "2013-02-25T10:00:00-00:00" + + @classmethod + def get_resources(cls): + my_plurals = [] + for plural in RESOURCE_ATTRIBUTE_MAP: + if plural == 'firewall_policies': + singular = 'firewall_policy' + else: + singular = plural[:-1] + my_plurals.append((plural, singular)) + attr.PLURALS.update(dict(my_plurals)) + resources = [] + plugin = manager.NeutronManager.get_service_plugins()[ + constants.FIREWALL] + for collection_name in RESOURCE_ATTRIBUTE_MAP: + # Special handling needed for resources with 'y' ending + if collection_name == 'firewall_policies': + resource_name = 'firewall_policy' + else: + resource_name = collection_name[:-1] + + params = RESOURCE_ATTRIBUTE_MAP[collection_name] + + member_actions = {} + if resource_name == 'firewall_policy': + member_actions = {'insert_rule': 'PUT', + 'remove_rule': 'PUT'} + + controller = base.create_resource( + collection_name, resource_name, plugin, params, + member_actions=member_actions, + allow_pagination=cfg.CONF.allow_pagination, + allow_sorting=cfg.CONF.allow_sorting) + + resource = extensions.ResourceExtension( + collection_name, + controller, + path_prefix=constants.COMMON_PREFIXES[constants.FIREWALL], + member_actions=member_actions, + attr_map=params) + resources.append(resource) + + return resources + + @classmethod + def get_plugin_interface(cls): + return FirewallPluginBase + + def update_attributes_map(self, attributes): + super(Firewall, self).update_attributes_map( + attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP) + + def get_extended_resources(self, version): + if version == "2.0": + return RESOURCE_ATTRIBUTE_MAP + else: + return {} + + +class FirewallPluginBase(ServicePluginBase): + __metaclass__ = abc.ABCMeta + + def get_plugin_name(self): + return constants.FIREWALL + + def get_plugin_type(self): + return constants.FIREWALL + + def get_plugin_description(self): + return 'Firewall service plugin' + + @abc.abstractmethod + def get_firewalls(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def get_firewall(self, context, id, fields=None): + pass + + @abc.abstractmethod + def create_firewall(self, context, firewall): + pass + + @abc.abstractmethod + def update_firewall(self, context, id, firewall): + pass + + @abc.abstractmethod + def delete_firewall(self, context, id): + pass + + @abc.abstractmethod + def get_firewall_rules(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def get_firewall_rule(self, context, id, fields=None): + pass + + @abc.abstractmethod + def create_firewall_rule(self, context, firewall_rule): + pass + + @abc.abstractmethod + def update_firewall_rule(self, context, id, firewall_rule): + pass + + @abc.abstractmethod + def delete_firewall_rule(self, context, id): + pass + + @abc.abstractmethod + def get_firewall_policy(self, context, id, fields=None): + pass + + @abc.abstractmethod + def get_firewall_policies(self, context, filters=None, fields=None): + pass + + @abc.abstractmethod + def create_firewall_policy(self, context, firewall_policy): + pass + + @abc.abstractmethod + def update_firewall_policy(self, context, id, firewall_policy): + pass + + @abc.abstractmethod + def delete_firewall_policy(self, context, id): + pass + + @abc.abstractmethod + def insert_rule(self, context, id, rule_info): + pass + + @abc.abstractmethod + def remove_rule(self, context, id, rule_info): + pass diff --git a/neutron/plugins/common/constants.py b/neutron/plugins/common/constants.py index 53b273e2f93..2e47fb0d0c8 100644 --- a/neutron/plugins/common/constants.py +++ b/neutron/plugins/common/constants.py @@ -19,20 +19,23 @@ CORE = "CORE" DUMMY = "DUMMY" LOADBALANCER = "LOADBALANCER" +FIREWALL = "FIREWALL" #maps extension alias to service type EXT_TO_SERVICE_MAPPING = { 'dummy': DUMMY, - 'lbaas': LOADBALANCER + 'lbaas': LOADBALANCER, + 'fwaas': FIREWALL } # TODO(salvatore-orlando): Move these (or derive them) from conf file -ALLOWED_SERVICES = [CORE, DUMMY, LOADBALANCER] +ALLOWED_SERVICES = [CORE, DUMMY, LOADBALANCER, FIREWALL] COMMON_PREFIXES = { CORE: "", DUMMY: "/dummy_svc", LOADBALANCER: "/lb", + FIREWALL: "/fw", } # Service operation status constants @@ -42,3 +45,12 @@ PENDING_UPDATE = "PENDING_UPDATE" PENDING_DELETE = "PENDING_DELETE" INACTIVE = "INACTIVE" ERROR = "ERROR" + +# FWaaS firewall rule action +FWAAS_ALLOW = "allow" +FWAAS_DENY = "deny" + +# L3 Protocol name constants +TCP = "tcp" +UDP = "udp" +ICMP = "icmp" diff --git a/neutron/services/firewall/__init__.py b/neutron/services/firewall/__init__.py new file mode 100644 index 00000000000..5e8da711fb1 --- /dev/null +++ b/neutron/services/firewall/__init__.py @@ -0,0 +1,16 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/neutron/services/firewall/fwaas_plugin.py b/neutron/services/firewall/fwaas_plugin.py new file mode 100644 index 00000000000..4e655e599e4 --- /dev/null +++ b/neutron/services/firewall/fwaas_plugin.py @@ -0,0 +1,285 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Big Switch Networks, 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. +# +# @author: Sumit Naiksatam, sumitnaiksatam@gmail.com, Big Switch Networks, Inc. + +from oslo.config import cfg + +from neutron.common import rpc as q_rpc +from neutron.common import topics +from neutron.db import api as qdbapi +from neutron.db.firewall import firewall_db +from neutron.extensions import firewall as fw_ext +from neutron.openstack.common import log as logging +from neutron.openstack.common import rpc +from neutron.openstack.common.rpc import proxy +from neutron.plugins.common import constants as const + + +LOG = logging.getLogger(__name__) + + +class FirewallCallbacks(object): + RPC_API_VERSION = '1.0' + + def __init__(self, plugin): + self.plugin = plugin + + def create_rpc_dispatcher(self): + return q_rpc.PluginRpcDispatcher([self]) + + def set_firewall_status(self, context, firewall_id, status, **kwargs): + """Agent uses this to set a firewall's status.""" + LOG.debug(_("set_firewall_status() called")) + with context.session.begin(subtransactions=True): + fw_db = self.plugin._get_firewall(context, firewall_id) + if status in (const.ACTIVE, const.INACTIVE): + fw_db.status = status + return True + else: + fw_db.status = const.ERROR + return False + + def firewall_deleted(self, context, firewall_id, **kwargs): + """Agent uses this to indicate firewall is deleted.""" + LOG.debug(_("firewall_deleted() called")) + with context.session.begin(subtransactions=True): + fw_db = self.plugin._get_firewall(context, firewall_id) + if fw_db.status == const.PENDING_DELETE: + self.plugin.delete_db_firewall_object(context, firewall_id) + return True + else: + fw_db.status = const.ERROR + LOG.warn(_('Firewall %s unexpectedly deleted by agent.'), + firewall_id) + return False + + def get_firewalls_for_tenant(self, context, **kwargs): + """Agent uses this to get all firewalls and rules for a tenant.""" + LOG.debug(_("get_firewalls_for_tenant() called")) + fw_list = [ + self.plugin._make_firewall_dict_with_rules(context, fw['id']) + for fw in self.plugin.get_firewalls(context) + ] + return fw_list + + def get_firewalls_for_tenant_without_rules(self, context, **kwargs): + """Agent uses this to get all firewalls for a tenant.""" + LOG.debug(_("get_firewalls_for_tenant_without_rules() called")) + fw_list = [fw for fw in self.plugin.get_firewalls(context)] + return fw_list + + +class FirewallAgentApi(proxy.RpcProxy): + """Plugin side of plugin to agent RPC API.""" + + API_VERSION = '1.0' + + def __init__(self, topic, host): + super(FirewallAgentApi, self).__init__(topic, self.API_VERSION) + self.host = host + + def create_firewall(self, context, firewall): + return self.fanout_cast( + context, + self.make_msg('create_firewall', firewall=firewall, + host=self.host), + topic=self.topic + ) + + def update_firewall(self, context, firewall): + return self.fanout_cast( + context, + self.make_msg('update_firewall', firewall=firewall, + host=self.host), + topic=self.topic + ) + + def delete_firewall(self, context, firewall): + return self.fanout_cast( + context, + self.make_msg('delete_firewall', firewall=firewall, + host=self.host), + topic=self.topic + ) + + +class FirewallPlugin(firewall_db.Firewall_db_mixin): + + """Implementation of the Neutron Firewall Service Plugin. + + This class manages the workflow of FWaaS request/response. + Most DB related works are implemented in class + firewall_db.Firewall_db_mixin. + """ + supported_extension_aliases = ["fwaas"] + + def __init__(self): + """Do the initialization for the firewall service plugin here.""" + qdbapi.register_models() + + self.callbacks = FirewallCallbacks(self) + + self.conn = rpc.create_connection(new=True) + self.conn.create_consumer( + topics.FIREWALL_PLUGIN, + self.callbacks.create_rpc_dispatcher(), + fanout=False) + self.conn.consume_in_thread() + + self.agent_rpc = FirewallAgentApi( + topics.L3_AGENT, + cfg.CONF.host + ) + + def _make_firewall_dict_with_rules(self, context, firewall_id): + firewall = self.get_firewall(context, firewall_id) + fw_policy_id = firewall['firewall_policy_id'] + if fw_policy_id: + fw_policy = self.get_firewall_policy(context, fw_policy_id) + fw_rules_list = [self.get_firewall_rule( + context, rule_id) for rule_id in fw_policy['firewall_rules']] + firewall['firewall_rule_list'] = fw_rules_list + else: + firewall['firewall_rule_list'] = [] + # FIXME(Sumit): If the size of the firewall object we are creating + # here exceeds the largest message size supported by rabbit/qpid + # then we will have a problem. + return firewall + + def _rpc_update_firewall(self, context, firewall_id): + status_update = {"firewall": {"status": const.PENDING_UPDATE}} + fw = super(FirewallPlugin, self).update_firewall(context, firewall_id, + status_update) + if fw: + fw_with_rules = ( + self._make_firewall_dict_with_rules(context, + firewall_id)) + self.agent_rpc.update_firewall(context, fw_with_rules) + + def _rpc_update_firewall_policy(self, context, firewall_policy_id): + firewall_policy = self.get_firewall_policy(context, firewall_policy_id) + if firewall_policy: + for firewall_id in firewall_policy['firewall_list']: + self._rpc_update_firewall(context, firewall_id) + + def _ensure_update_firewall(self, context, firewall_id): + fwall = self.get_firewall(context, firewall_id) + if fwall['status'] in [const.PENDING_CREATE, + const.PENDING_UPDATE, + const.PENDING_DELETE]: + raise fw_ext.FirewallInPendingState(firewall_id=firewall_id, + pending_state=fwall['status']) + + def _ensure_update_firewall_policy(self, context, firewall_policy_id): + firewall_policy = self.get_firewall_policy(context, firewall_policy_id) + if firewall_policy and 'firewall_list' in firewall_policy: + for firewall_id in firewall_policy['firewall_list']: + self._ensure_update_firewall(context, firewall_id) + + def _ensure_update_or_delete_firewall_rule(self, context, + firewall_rule_id): + fw_rule = self.get_firewall_rule(context, firewall_rule_id) + if 'firewall_policy_id' in fw_rule and fw_rule['firewall_policy_id']: + self._ensure_update_firewall_policy(context, + fw_rule['firewall_policy_id']) + + def create_firewall(self, context, firewall): + LOG.debug(_("create_firewall() called")) + firewall['firewall']['status'] = const.PENDING_CREATE + fw = super(FirewallPlugin, self).create_firewall(context, firewall) + fw_with_rules = ( + self._make_firewall_dict_with_rules(context, fw['id'])) + self.agent_rpc.create_firewall(context, fw_with_rules) + return fw + + def update_firewall(self, context, id, firewall): + LOG.debug(_("update_firewall() called")) + self._ensure_update_firewall(context, id) + firewall['firewall']['status'] = const.PENDING_UPDATE + fw = super(FirewallPlugin, self).update_firewall(context, id, firewall) + fw_with_rules = ( + self._make_firewall_dict_with_rules(context, fw['id'])) + self.agent_rpc.update_firewall(context, fw_with_rules) + return fw + + def delete_db_firewall_object(self, context, id): + firewall = self.get_firewall(context, id) + if firewall['status'] in [const.PENDING_DELETE]: + super(FirewallPlugin, self).delete_firewall(context, id) + + def delete_firewall(self, context, id): + LOG.debug(_("delete_firewall() called")) + status_update = {"firewall": {"status": const.PENDING_DELETE}} + fw = super(FirewallPlugin, self).update_firewall(context, id, + status_update) + fw_with_rules = ( + self._make_firewall_dict_with_rules(context, fw['id'])) + self.agent_rpc.delete_firewall(context, fw_with_rules) + + def update_firewall_policy(self, context, id, firewall_policy): + LOG.debug(_("update_firewall_policy() called")) + self._ensure_update_firewall_policy(context, id) + fwp = super(FirewallPlugin, + self).update_firewall_policy(context, id, firewall_policy) + self._rpc_update_firewall_policy(context, id) + return fwp + + def update_firewall_rule(self, context, id, firewall_rule): + LOG.debug(_("update_firewall_rule() called")) + self._ensure_update_or_delete_firewall_rule(context, id) + fwr = super(FirewallPlugin, + self).update_firewall_rule(context, id, firewall_rule) + firewall_policy_id = fwr['firewall_policy_id'] + if firewall_policy_id: + self._rpc_update_firewall_policy(context, firewall_policy_id) + return fwr + + def delete_firewall_rule(self, context, id): + LOG.debug(_("delete_firewall_rule() called")) + self._ensure_update_or_delete_firewall_rule(context, id) + fwr = self.get_firewall_rule(context, id) + firewall_policy_id = fwr['firewall_policy_id'] + super(FirewallPlugin, self).delete_firewall_rule(context, id) + # At this point we have already deleted the rule in the DB, + # however it's still not deleted on the backend firewall. + # Until it gets deleted on the backend we will be setting + # the firewall in PENDING_UPDATE state. The backend firewall + # implementation is responsible for setting the appropriate + # configuration (e.g. do not allow any traffic) until the rule + # is deleted. Once the rule is deleted, the backend should put + # the firewall back in ACTIVE state. While the firewall is in + # PENDING_UPDATE state, the firewall behavior might differ based + # on the backend implementation. + if firewall_policy_id: + self._rpc_update_firewall_policy(context, firewall_policy_id) + + def insert_rule(self, context, id, rule_info): + LOG.debug(_("insert_rule() called")) + self._ensure_update_firewall_policy(context, id) + fwp = super(FirewallPlugin, + self).insert_rule(context, id, rule_info) + self._rpc_update_firewall_policy(context, id) + return fwp + + def remove_rule(self, context, id, rule_info): + LOG.debug(_("remove_rule() called")) + self._ensure_update_firewall_policy(context, id) + fwp = super(FirewallPlugin, + self).remove_rule(context, id, rule_info) + self._rpc_update_firewall_policy(context, id) + return fwp diff --git a/neutron/tests/unit/db/firewall/__init__.py b/neutron/tests/unit/db/firewall/__init__.py new file mode 100644 index 00000000000..cae279d0ad6 --- /dev/null +++ b/neutron/tests/unit/db/firewall/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/neutron/tests/unit/db/firewall/test_db_firewall.py b/neutron/tests/unit/db/firewall/test_db_firewall.py new file mode 100644 index 00000000000..e5b8b3e2ebe --- /dev/null +++ b/neutron/tests/unit/db/firewall/test_db_firewall.py @@ -0,0 +1,965 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Big Switch Networks, 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 spec +# +# @author: Sumit Naiksatam, sumitnaiksatam@gmail.com, Big Switch Networks, Inc. + +import contextlib +import logging + +import webob.exc + +from neutron.api import extensions as api_ext +from neutron.common import config +from neutron import context +from neutron.db.firewall import firewall_db as fdb +import neutron.extensions +from neutron.extensions import firewall +from neutron.openstack.common import importutils +from neutron.plugins.common import constants +from neutron.tests.unit import test_db_plugin + + +LOG = logging.getLogger(__name__) +DB_FW_PLUGIN_KLASS = ( + "neutron.db.firewall.firewall_db.Firewall_db_mixin" +) +extensions_path = ':'.join(neutron.extensions.__path__) +DESCRIPTION = 'default description' +SHARED = True +PROTOCOL = 'tcp' +IP_VERSION = 4 +SOURCE_IP_ADDRESS_RAW = '1.1.1.1' +DESTINATION_IP_ADDRESS_RAW = '2.2.2.2' +SOURCE_PORT = '55000:56000' +DESTINATION_PORT = '56000:57000' +ACTION = 'allow' +AUDITED = True +ENABLED = True +ADMIN_STATE_UP = True + + +class FirewallPluginDbTestCase(test_db_plugin.NeutronDbPluginV2TestCase): + resource_prefix_map = dict( + (k, constants.COMMON_PREFIXES[constants.FIREWALL]) + for k in firewall.RESOURCE_ATTRIBUTE_MAP.keys() + ) + + def setUp(self, core_plugin=None, fw_plugin=None): + if not fw_plugin: + fw_plugin = DB_FW_PLUGIN_KLASS + service_plugins = {'fw_plugin_name': fw_plugin} + + fdb.Firewall_db_mixin.supported_extension_aliases = ["fwaas"] + super(FirewallPluginDbTestCase, self).setUp( + service_plugins=service_plugins + ) + + self.plugin = importutils.import_object(fw_plugin) + ext_mgr = api_ext.PluginAwareExtensionManager( + extensions_path, + {constants.FIREWALL: self.plugin} + ) + app = config.load_paste_app('extensions_test_app') + self.ext_api = api_ext.ExtensionMiddleware(app, ext_mgr=ext_mgr) + + def _test_list_resources(self, resource, items, + neutron_context=None, + query_params=None): + if resource.endswith('y'): + resource_plural = resource.replace('y', 'ies') + else: + resource_plural = resource + 's' + + res = self._list(resource_plural, + neutron_context=neutron_context, + query_params=query_params) + resource = resource.replace('-', '_') + self.assertEqual(sorted([i['id'] for i in res[resource_plural]]), + sorted([i[resource]['id'] for i in items])) + + def _get_test_firewall_rule_attrs(self, name='firewall_rule1'): + attrs = {'name': name, + 'tenant_id': self._tenant_id, + 'shared': SHARED, + 'protocol': PROTOCOL, + 'ip_version': IP_VERSION, + 'source_ip_address': SOURCE_IP_ADDRESS_RAW, + 'destination_ip_address': DESTINATION_IP_ADDRESS_RAW, + 'source_port': SOURCE_PORT, + 'destination_port': DESTINATION_PORT, + 'action': ACTION, + 'enabled': ENABLED} + return attrs + + def _get_test_firewall_policy_attrs(self, name='firewall_policy1'): + attrs = {'name': name, + 'description': DESCRIPTION, + 'tenant_id': self._tenant_id, + 'shared': SHARED, + 'firewall_rules': [], + 'audited': AUDITED} + return attrs + + def _get_test_firewall_attrs(self, name='firewall_1'): + attrs = {'name': name, + 'tenant_id': self._tenant_id, + 'admin_state_up': ADMIN_STATE_UP, + 'status': 'PENDING_CREATE'} + + return attrs + + def _create_firewall_policy(self, fmt, name, description, shared, + firewall_rules, audited, + expected_res_status=None, **kwargs): + data = {'firewall_policy': {'name': name, + 'description': description, + 'tenant_id': self._tenant_id, + 'shared': shared, + 'firewall_rules': firewall_rules, + 'audited': audited}} + + fw_policy_req = self.new_create_request('firewall_policies', data, fmt) + fw_policy_res = fw_policy_req.get_response(self.ext_api) + if expected_res_status: + self.assertEqual(fw_policy_res.status_int, expected_res_status) + + return fw_policy_res + + def _replace_firewall_status(self, attrs, old_status, new_status): + if attrs['status'] is old_status: + attrs['status'] = new_status + return attrs + + @contextlib.contextmanager + def firewall_policy(self, fmt=None, name='firewall_policy1', + description=DESCRIPTION, shared=True, + firewall_rules=None, audited=True, + no_delete=False, **kwargs): + if firewall_rules is None: + firewall_rules = [] + if not fmt: + fmt = self.fmt + res = self._create_firewall_policy(fmt, name, description, shared, + firewall_rules, audited, + **kwargs) + if res.status_int >= 400: + raise webob.exc.HTTPClientError(code=res.status_int) + firewall_policy = self.deserialize(fmt or self.fmt, res) + try: + yield firewall_policy + finally: + if not no_delete: + self._delete('firewall_policies', + firewall_policy['firewall_policy']['id']) + + def _create_firewall_rule(self, fmt, name, shared, protocol, + ip_version, source_ip_address, + destination_ip_address, source_port, + destination_port, action, enabled, + expected_res_status=None, **kwargs): + data = {'firewall_rule': {'name': name, + 'tenant_id': self._tenant_id, + 'shared': shared, + 'protocol': protocol, + 'ip_version': ip_version, + 'source_ip_address': source_ip_address, + 'destination_ip_address': + destination_ip_address, + 'source_port': source_port, + 'destination_port': destination_port, + 'action': action, + 'enabled': enabled}} + + fw_rule_req = self.new_create_request('firewall_rules', data, fmt) + fw_rule_res = fw_rule_req.get_response(self.ext_api) + if expected_res_status: + self.assertEqual(fw_rule_res.status_int, expected_res_status) + + return fw_rule_res + + @contextlib.contextmanager + def firewall_rule(self, fmt=None, name='firewall_rule1', + shared=SHARED, protocol=PROTOCOL, ip_version=IP_VERSION, + source_ip_address=SOURCE_IP_ADDRESS_RAW, + destination_ip_address=DESTINATION_IP_ADDRESS_RAW, + source_port=SOURCE_PORT, + destination_port=DESTINATION_PORT, + action=ACTION, enabled=ENABLED, + no_delete=False, **kwargs): + if not fmt: + fmt = self.fmt + res = self._create_firewall_rule(fmt, name, shared, protocol, + ip_version, source_ip_address, + destination_ip_address, + source_port, destination_port, + action, enabled, **kwargs) + if res.status_int >= 400: + raise webob.exc.HTTPClientError(code=res.status_int) + firewall_rule = self.deserialize(fmt or self.fmt, res) + try: + yield firewall_rule + finally: + if not no_delete: + self._delete('firewall_rules', + firewall_rule['firewall_rule']['id']) + + def _create_firewall(self, fmt, name, description, firewall_policy_id, + admin_state_up=True, expected_res_status=None, + **kwargs): + data = {'firewall': {'name': name, + 'description': description, + 'firewall_policy_id': firewall_policy_id, + 'admin_state_up': admin_state_up, + 'tenant_id': self._tenant_id}} + + firewall_req = self.new_create_request('firewalls', data, fmt) + firewall_res = firewall_req.get_response(self.ext_api) + if expected_res_status: + self.assertEqual(firewall_res.status_int, expected_res_status) + + return firewall_res + + @contextlib.contextmanager + def firewall(self, fmt=None, name='firewall_1', description=DESCRIPTION, + firewall_policy_id=None, admin_state_up=True, + no_delete=False, **kwargs): + if not fmt: + fmt = self.fmt + res = self._create_firewall(fmt, name, description, firewall_policy_id, + admin_state_up, **kwargs) + if res.status_int >= 400: + raise webob.exc.HTTPClientError(code=res.status_int) + firewall = self.deserialize(fmt or self.fmt, res) + try: + yield firewall + finally: + if not no_delete: + self._delete('firewalls', firewall['firewall']['id']) + + def _rule_action(self, action, id, firewall_rule_id, insert_before=None, + insert_after=None, expected_code=webob.exc.HTTPOk.code, + expected_body=None, body_data=None): + # We intentionally do this check for None since we want to distinguish + # from empty dictionary + if body_data is None: + if action == 'insert': + body_data = {'firewall_rule_id': firewall_rule_id, + 'insert_before': insert_before, + 'insert_after': insert_after} + else: + body_data = {'firewall_rule_id': firewall_rule_id} + + req = self.new_action_request('firewall_policies', + body_data, id, + "%s_rule" % action) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, expected_code) + response = self.deserialize(self.fmt, res) + if expected_body: + self.assertEqual(response, expected_body) + return response + + def _compare_firewall_rule_lists(self, firewall_policy_id, + list1, list2): + position = 0 + for r1, r2 in zip(list1, list2): + rule = r1['firewall_rule'] + rule['firewall_policy_id'] = firewall_policy_id + position += 1 + rule['position'] = position + for k in rule: + self.assertEqual(rule[k], r2[k]) + + +class TestFirewallDBPlugin(FirewallPluginDbTestCase): + + def test_create_firewall_policy(self): + name = "firewall_policy1" + attrs = self._get_test_firewall_policy_attrs(name) + + with self.firewall_policy(name=name, shared=SHARED, + firewall_rules=None, + audited=AUDITED) as firewall_policy: + for k, v in attrs.iteritems(): + self.assertEqual(firewall_policy['firewall_policy'][k], v) + + def test_create_firewall_policy_with_rules(self): + name = "firewall_policy1" + attrs = self._get_test_firewall_policy_attrs(name) + + with contextlib.nested(self.firewall_rule(name='fwr1', + no_delete=True), + self.firewall_rule(name='fwr2', + no_delete=True), + self.firewall_rule(name='fwr3', + no_delete=True)) as fr: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr] + attrs['firewall_rules'] = fw_rule_ids + with self.firewall_policy(name=name, shared=SHARED, + firewall_rules=fw_rule_ids, + audited=AUDITED, + no_delete=True) as fwp: + for k, v in attrs.iteritems(): + self.assertEqual(fwp['firewall_policy'][k], v) + + def test_show_firewall_policy(self): + name = "firewall_policy1" + attrs = self._get_test_firewall_policy_attrs(name) + + with self.firewall_policy(name=name, shared=SHARED, + firewall_rules=None, + audited=AUDITED) as fwp: + req = self.new_show_request('firewall_policies', + fwp['firewall_policy']['id'], + fmt=self.fmt) + res = self.deserialize(self.fmt, req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_policy'][k], v) + + def test_list_firewall_policies(self): + with contextlib.nested(self.firewall_policy(name='fwp1', + description='fwp'), + self.firewall_policy(name='fwp2', + description='fwp'), + self.firewall_policy(name='fwp3', + description='fwp') + ) as fw_policies: + self._test_list_resources('firewall_policy', + fw_policies, + query_params='description=fwp') + + def test_update_firewall_policy(self): + name = "new_firewall_policy1" + attrs = self._get_test_firewall_policy_attrs(name) + + with self.firewall_policy(shared=SHARED, + firewall_rules=None, + audited=AUDITED) as fwp: + data = {'firewall_policy': {'name': name}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = self.deserialize(self.fmt, req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_policy'][k], v) + + def test_update_firewall_policy_with_rules(self): + attrs = self._get_test_firewall_policy_attrs() + + with self.firewall_policy() as fwp: + with contextlib.nested(self.firewall_rule(name='fwr1', + no_delete=True), + self.firewall_rule(name='fwr2', + no_delete=True), + self.firewall_rule(name='fwr3', + no_delete=True)) as fr: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr] + attrs['firewall_rules'] = fw_rule_ids + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + attrs['audited'] = False + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_policy'][k], v) + + def test_update_firewall_policy_replace_rules(self): + attrs = self._get_test_firewall_policy_attrs() + + with self.firewall_policy() as fwp: + with contextlib.nested(self.firewall_rule(name='fwr1', + no_delete=True), + self.firewall_rule(name='fwr2', + no_delete=True)) as fr1: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr1] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + with contextlib.nested(self.firewall_rule(name='fwr3', + no_delete=True), + self.firewall_rule(name='fwr4', + no_delete=True)) as fr2: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr2] + attrs['firewall_rules'] = fw_rule_ids + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + attrs['audited'] = False + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_policy'][k], v) + + def test_update_firewall_policy_with_non_existing_rule(self): + attrs = self._get_test_firewall_policy_attrs() + + with self.firewall_policy() as fwp: + with contextlib.nested(self.firewall_rule(name='fwr1', + no_delete=True), + self.firewall_rule(name='fwr2', + no_delete=True)) as fr: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr] + fw_rule_ids.append('12345') # non-existent rule + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + res = req.get_response(self.ext_api) + #check that the firewall_rule was not found + self.assertEqual(res.status_int, 404) + #check if none of the rules got added to the policy + req = self.new_show_request('firewall_policies', + fwp['firewall_policy']['id'], + fmt=self.fmt) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_policy'][k], v) + + def test_delete_firewall_policy(self): + ctx = context.get_admin_context() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + req = self.new_delete_request('firewall_policies', fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 204) + self.assertRaises(firewall.FirewallPolicyNotFound, + self.plugin.get_firewall_policy, + ctx, fwp_id) + + def test_delete_firewall_policy_with_rule(self): + ctx = context.get_admin_context() + attrs = self._get_test_firewall_policy_attrs() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_rule(name='fwr1', no_delete=True) as fr: + fr_id = fr['firewall_rule']['id'] + fw_rule_ids = [fr_id] + attrs['firewall_rules'] = fw_rule_ids + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + fw_rule = self.plugin.get_firewall_rule(ctx, fr_id) + self.assertEqual(fw_rule['firewall_policy_id'], fwp_id) + req = self.new_delete_request('firewall_policies', fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 204) + self.assertRaises(firewall.FirewallPolicyNotFound, + self.plugin.get_firewall_policy, + ctx, fwp_id) + fw_rule = self.plugin.get_firewall_rule(ctx, fr_id) + self.assertEqual(fw_rule['firewall_policy_id'], None) + + def test_delete_firewall_policy_with_firewall_association(self): + attrs = self._get_test_firewall_attrs() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up= + ADMIN_STATE_UP): + req = self.new_delete_request('firewall_policies', fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 409) + + def test_create_firewall_rule(self): + attrs = self._get_test_firewall_rule_attrs() + + with self.firewall_rule() as firewall_rule: + for k, v in attrs.iteritems(): + self.assertEqual(firewall_rule['firewall_rule'][k], v) + + attrs['source_port'] = None + attrs['destination_port'] = None + with self.firewall_rule(source_port=None, + destination_port=None) as firewall_rule: + for k, v in attrs.iteritems(): + self.assertEqual(firewall_rule['firewall_rule'][k], v) + + attrs['source_port'] = '10000' + attrs['destination_port'] = '80' + with self.firewall_rule(source_port=10000, + destination_port=80) as firewall_rule: + for k, v in attrs.iteritems(): + self.assertEqual(firewall_rule['firewall_rule'][k], v) + + attrs['source_port'] = '10000' + attrs['destination_port'] = '80' + with self.firewall_rule(source_port='10000', + destination_port='80') as firewall_rule: + for k, v in attrs.iteritems(): + self.assertEqual(firewall_rule['firewall_rule'][k], v) + + def test_show_firewall_rule_with_fw_policy_not_associated(self): + attrs = self._get_test_firewall_rule_attrs() + with self.firewall_rule() as fw_rule: + req = self.new_show_request('firewall_rules', + fw_rule['firewall_rule']['id'], + fmt=self.fmt) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_rule'][k], v) + + def test_show_firewall_rule_with_fw_policy_associated(self): + attrs = self._get_test_firewall_rule_attrs() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall_rule(no_delete=True) as fw_rule: + data = {'firewall_policy': + {'firewall_rules': + [fw_rule['firewall_rule']['id']]}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + req = self.new_show_request('firewall_rules', + fw_rule['firewall_rule']['id'], + fmt=self.fmt) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_rule'][k], v) + + def test_list_firewall_rules(self): + with contextlib.nested(self.firewall_rule(name='fwr1', + no_delete=True), + self.firewall_rule(name='fwr2', + no_delete=True), + self.firewall_rule(name='fwr3', + no_delete=True)) as fr: + query_params = 'protocol=tcp' + self._test_list_resources('firewall_rule', fr, + query_params=query_params) + + def test_update_firewall_rule(self): + name = "new_firewall_rule1" + attrs = self._get_test_firewall_rule_attrs(name) + + attrs['source_port'] = '10:20' + attrs['destination_port'] = '30:40' + with self.firewall_rule(no_delete=True) as fwr: + data = {'firewall_rule': {'name': name, + 'source_port': '10:20', + 'destination_port': '30:40'}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_rule'][k], v) + + attrs['source_port'] = '10000' + attrs['destination_port'] = '80' + with self.firewall_rule(no_delete=True) as fwr: + data = {'firewall_rule': {'name': name, + 'source_port': 10000, + 'destination_port': 80}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_rule'][k], v) + + attrs['source_port'] = '10000' + attrs['destination_port'] = '80' + with self.firewall_rule(no_delete=True) as fwr: + data = {'firewall_rule': {'name': name, + 'source_port': '10000', + 'destination_port': '80'}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_rule'][k], v) + + attrs['source_port'] = None + attrs['destination_port'] = None + with self.firewall_rule(no_delete=True) as fwr: + data = {'firewall_rule': {'name': name, + 'source_port': None, + 'destination_port': None}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_rule'][k], v) + + def test_update_firewall_rule_with_policy_associated(self): + name = "new_firewall_rule1" + attrs = self._get_test_firewall_rule_attrs(name) + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall_rule(no_delete=True) as fwr: + fwr_id = fwr['firewall_rule']['id'] + data = {'firewall_policy': {'firewall_rules': [fwr_id]}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + data = {'firewall_rule': {'name': name}} + req = self.new_update_request('firewall_rules', data, + fwr['firewall_rule']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + attrs['firewall_policy_id'] = fwp_id + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall_rule'][k], v) + req = self.new_show_request('firewall_policies', + fwp['firewall_policy']['id'], + fmt=self.fmt) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + self.assertEqual(res['firewall_policy']['firewall_rules'], + [fwr_id]) + self.assertEqual(res['firewall_policy']['audited'], False) + + def test_delete_firewall_rule(self): + ctx = context.get_admin_context() + with self.firewall_rule(no_delete=True) as fwr: + fwr_id = fwr['firewall_rule']['id'] + req = self.new_delete_request('firewall_rules', fwr_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 204) + self.assertRaises(firewall.FirewallRuleNotFound, + self.plugin.get_firewall_rule, + ctx, fwr_id) + + def test_delete_firewall_rule_with_policy_associated(self): + attrs = self._get_test_firewall_rule_attrs() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall_rule(no_delete=True) as fwr: + fwr_id = fwr['firewall_rule']['id'] + data = {'firewall_policy': {'firewall_rules': [fwr_id]}} + req = self.new_update_request('firewall_policies', data, + fwp['firewall_policy']['id']) + req.get_response(self.ext_api) + req = self.new_delete_request('firewall_rules', fwr_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 409) + + def test_create_firewall(self): + name = "firewall1" + attrs = self._get_test_firewall_attrs(name) + + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall(name=name, + firewall_policy_id=fwp_id, + admin_state_up= + ADMIN_STATE_UP) as firewall: + for k, v in attrs.iteritems(): + self.assertEqual(firewall['firewall'][k], v) + + def test_show_firewall(self): + name = "firewall1" + attrs = self._get_test_firewall_attrs(name) + + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall(name=name, + firewall_policy_id=fwp_id, + admin_state_up= + ADMIN_STATE_UP) as firewall: + req = self.new_show_request('firewalls', + firewall['firewall']['id'], + fmt=self.fmt) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall'][k], v) + + def test_list_firewalls(self): + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with contextlib.nested(self.firewall(name='fw1', + firewall_policy_id=fwp_id, + description='fw'), + self.firewall(name='fw2', + firewall_policy_id=fwp_id, + description='fw'), + self.firewall(name='fw3', + firewall_policy_id=fwp_id, + description='fw')) as fwalls: + self._test_list_resources('firewall', fwalls, + query_params='description=fw') + + def test_update_firewall(self): + name = "new_firewall1" + attrs = self._get_test_firewall_attrs(name) + + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up= + ADMIN_STATE_UP) as firewall: + data = {'firewall': {'name': name}} + req = self.new_update_request('firewalls', data, + firewall['firewall']['id']) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall'][k], v) + + def test_delete_firewall(self): + ctx = context.get_admin_context() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall(firewall_policy_id=fwp_id, + no_delete=True) as fw: + fw_id = fw['firewall']['id'] + req = self.new_delete_request('firewalls', fw_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 204) + self.assertRaises(firewall.FirewallNotFound, + self.plugin.get_firewall, + ctx, fw_id) + + def test_insert_rule_in_policy_with_prior_rules_added_via_update(self): + attrs = self._get_test_firewall_policy_attrs() + attrs['audited'] = False + attrs['firewall_list'] = [] + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['id'] = fwp_id + with contextlib.nested(self.firewall_rule(name='fwr1', + no_delete=True), + self.firewall_rule(name='fwr2', + no_delete=True)) as fr1: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr1] + attrs['firewall_rules'] = fw_rule_ids[:] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + self._rule_action('insert', fwp_id, fw_rule_ids[0], + insert_before=fw_rule_ids[0], + insert_after=None, + expected_code=webob.exc.HTTPConflict.code, + expected_body=None) + with self.firewall_rule(name='fwr3', no_delete=True) as fwr3: + fwr3_id = fwr3['firewall_rule']['id'] + attrs['firewall_rules'].insert(0, fwr3_id) + self._rule_action('insert', fwp_id, fwr3_id, + insert_before=fw_rule_ids[0], + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + + def test_insert_rule_in_policy_failures(self): + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_rule(name='fwr1', no_delete=True) as fr1: + fr1_id = fr1['firewall_rule']['id'] + fw_rule_ids = [fr1_id] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + # test inserting with empty request body + self._rule_action('insert', fwp_id, '123', + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None, body_data={}) + # test inserting when firewall_rule_id is missing in + # request body + insert_data = {'insert_before': '123', + 'insert_after': '456'} + self._rule_action('insert', fwp_id, '123', + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None, + body_data=insert_data) + # test inserting when firewall_rule_id is None + insert_data = {'firewall_rule_id': None, + 'insert_before': '123', + 'insert_after': '456'} + self._rule_action('insert', fwp_id, '123', + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None, + body_data=insert_data) + # test inserting when firewall_policy_id is incorrect + self._rule_action('insert', '123', fr1_id, + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None) + # test inserting when firewall_policy_id is None + self._rule_action('insert', None, fr1_id, + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None) + + def test_insert_rule_in_policy(self): + attrs = self._get_test_firewall_policy_attrs() + attrs['audited'] = False + attrs['firewall_list'] = [] + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['id'] = fwp_id + with contextlib.nested(self.firewall_rule(name='fwr0', + no_delete=True), + self.firewall_rule(name='fwr1', + no_delete=True), + self.firewall_rule(name='fwr2', + no_delete=True), + self.firewall_rule(name='fwr3', + no_delete=True), + self.firewall_rule(name='fwr4', + no_delete=True), + self.firewall_rule(name='fwr5', + no_delete=True), + self.firewall_rule(name='fwr6', + no_delete=True)) as fwr: + # test insert when rule list is empty + fwr0_id = fwr[0]['firewall_rule']['id'] + attrs['firewall_rules'].insert(0, fwr0_id) + self._rule_action('insert', fwp_id, fwr0_id, + insert_before=None, + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert at top of rule list, insert_before and + # insert_after not provided + fwr1_id = fwr[1]['firewall_rule']['id'] + attrs['firewall_rules'].insert(0, fwr1_id) + insert_data = {'firewall_rule_id': fwr1_id} + self._rule_action('insert', fwp_id, fwr0_id, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs, body_data=insert_data) + # test insert at top of list above existing rule + fwr2_id = fwr[2]['firewall_rule']['id'] + attrs['firewall_rules'].insert(0, fwr2_id) + self._rule_action('insert', fwp_id, fwr2_id, + insert_before=fwr1_id, + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert at bottom of list + fwr3_id = fwr[3]['firewall_rule']['id'] + attrs['firewall_rules'].append(fwr3_id) + self._rule_action('insert', fwp_id, fwr3_id, + insert_before=None, + insert_after=fwr0_id, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert in the middle of the list using + # insert_before + fwr4_id = fwr[4]['firewall_rule']['id'] + attrs['firewall_rules'].insert(1, fwr4_id) + self._rule_action('insert', fwp_id, fwr4_id, + insert_before=fwr1_id, + insert_after=None, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert in the middle of the list using + # insert_after + fwr5_id = fwr[5]['firewall_rule']['id'] + attrs['firewall_rules'].insert(1, fwr5_id) + self._rule_action('insert', fwp_id, fwr5_id, + insert_before=None, + insert_after=fwr2_id, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + # test insert when both insert_before and + # insert_after are set + fwr6_id = fwr[6]['firewall_rule']['id'] + attrs['firewall_rules'].insert(1, fwr6_id) + self._rule_action('insert', fwp_id, fwr6_id, + insert_before=fwr5_id, + insert_after=fwr5_id, + expected_code=webob.exc.HTTPOk.code, + expected_body=attrs) + + def test_remove_rule_from_policy(self): + attrs = self._get_test_firewall_policy_attrs() + attrs['audited'] = False + attrs['firewall_list'] = [] + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['id'] = fwp_id + with contextlib.nested(self.firewall_rule(name='fwr1', + no_delete=True), + self.firewall_rule(name='fwr2', + no_delete=True), + self.firewall_rule(name='fwr3', + no_delete=True)) as fr1: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr1] + attrs['firewall_rules'] = fw_rule_ids[:] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + # test removing a rule from a policy that does not exist + self._rule_action('remove', '123', fw_rule_ids[1], + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None) + # test removing a rule in the middle of the list + attrs['firewall_rules'].remove(fw_rule_ids[1]) + self._rule_action('remove', fwp_id, fw_rule_ids[1], + expected_body=attrs) + # test removing a rule at the top of the list + attrs['firewall_rules'].remove(fw_rule_ids[0]) + self._rule_action('remove', fwp_id, fw_rule_ids[0], + expected_body=attrs) + # test removing remaining rule in the list + attrs['firewall_rules'].remove(fw_rule_ids[2]) + self._rule_action('remove', fwp_id, fw_rule_ids[2], + expected_body=attrs) + # test removing rule that is not associated with the policy + self._rule_action('remove', fwp_id, fw_rule_ids[2], + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None) + + def test_remove_rule_from_policy_failures(self): + with self.firewall_policy() as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_rule(name='fwr1', no_delete=True) as fr1: + fw_rule_ids = [fr1['firewall_rule']['id']] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + # test removing rule that does not exist + self._rule_action('remove', fwp_id, '123', + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None) + # test removing rule with bad request + self._rule_action('remove', fwp_id, '123', + expected_code=webob.exc.HTTPBadRequest.code, + expected_body=None, body_data={}) + # test removing rule with firewall_rule_id set to None + self._rule_action('remove', fwp_id, '123', + expected_code=webob.exc.HTTPNotFound.code, + expected_body=None, + body_data={'firewall_rule_id': None}) + + +class TestFirewallDBPluginXML(TestFirewallDBPlugin): + fmt = 'xml' diff --git a/neutron/tests/unit/services/firewall/__init__.py b/neutron/tests/unit/services/firewall/__init__.py new file mode 100644 index 00000000000..cae279d0ad6 --- /dev/null +++ b/neutron/tests/unit/services/firewall/__init__.py @@ -0,0 +1,15 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 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. diff --git a/neutron/tests/unit/services/firewall/test_fwaas_plugin.py b/neutron/tests/unit/services/firewall/test_fwaas_plugin.py new file mode 100644 index 00000000000..5b484f1e191 --- /dev/null +++ b/neutron/tests/unit/services/firewall/test_fwaas_plugin.py @@ -0,0 +1,353 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Big Switch Networks, 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 spec +# +# @author: Sumit Naiksatam, sumitnaiksatam@gmail.com, Big Switch Networks, Inc. + + +import contextlib + +import mock + +from neutron import context +from neutron.extensions import firewall +from neutron.plugins.common import constants as const +from neutron.services.firewall import fwaas_plugin +from neutron.tests import base +from neutron.tests.unit.db.firewall import test_db_firewall + + +FW_PLUGIN_KLASS = ( + "neutron.services.firewall.fwaas_plugin.FirewallPlugin" +) + + +class TestFirewallCallbacks(test_db_firewall.FirewallPluginDbTestCase): + + def setUp(self): + super(TestFirewallCallbacks, + self).setUp(fw_plugin=FW_PLUGIN_KLASS) + self.callbacks = self.plugin.callbacks + + def test_set_firewall_status(self): + ctx = context.get_admin_context() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up= + test_db_firewall.ADMIN_STATE_UP) as fw: + fw_id = fw['firewall']['id'] + res = self.callbacks.set_firewall_status(ctx, fw_id, + const.ACTIVE, + host='dummy') + fw_db = self.plugin.get_firewall(ctx, fw_id) + self.assertEqual(fw_db['status'], const.ACTIVE) + self.assertTrue(res) + res = self.callbacks.set_firewall_status(ctx, fw_id, + const.ERROR) + fw_db = self.plugin.get_firewall(ctx, fw_id) + self.assertEqual(fw_db['status'], const.ERROR) + self.assertFalse(res) + + def test_firewall_deleted(self): + ctx = context.get_admin_context() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up=test_db_firewall.ADMIN_STATE_UP, + no_delete=True) as fw: + fw_id = fw['firewall']['id'] + with ctx.session.begin(subtransactions=True): + fw_db = self.plugin._get_firewall(ctx, fw_id) + fw_db['status'] = const.PENDING_DELETE + ctx.session.flush() + res = self.callbacks.firewall_deleted(ctx, fw_id, + host='dummy') + self.assertTrue(res) + self.assertRaises(firewall.FirewallNotFound, + self.plugin.get_firewall, + ctx, fw_id) + + def test_firewall_deleted_error(self): + ctx = context.get_admin_context() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up=test_db_firewall.ADMIN_STATE_UP, + no_delete=True) as fw: + fw_id = fw['firewall']['id'] + res = self.callbacks.firewall_deleted(ctx, fw_id, + host='dummy') + self.assertFalse(res) + fw_db = self.plugin._get_firewall(ctx, fw_id) + self.assertEqual(fw_db['status'], const.ERROR) + + def test_get_firewall_for_tenant(self): + tenant_id = 'test-tenant' + ctx = context.Context('', tenant_id) + with self.firewall_policy(tenant_id=tenant_id, no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with contextlib.nested(self.firewall_rule(name='fwr1', + tenant_id=tenant_id, + no_delete=True), + self.firewall_rule(name='fwr2', + tenant_id=tenant_id, + no_delete=True), + self.firewall_rule(name='fwr3', + tenant_id=tenant_id, + no_delete=True)) as fr: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + res = req.get_response(self.ext_api) + attrs = self._get_test_firewall_attrs() + attrs['firewall_policy_id'] = fwp_id + with self.firewall(firewall_policy_id=fwp_id, + tenant_id=tenant_id, + admin_state_up= + test_db_firewall.ADMIN_STATE_UP, + no_delete=True) as fw: + fw_id = fw['firewall']['id'] + res = self.callbacks.get_firewalls_for_tenant(ctx, + host='dummy') + fw_rules = ( + self.plugin._make_firewall_dict_with_rules(ctx, + fw_id) + ) + self.assertEqual(res[0], fw_rules) + self._compare_firewall_rule_lists( + fwp_id, fr, res[0]['firewall_rule_list']) + + def test_get_firewall_for_tenant_without_rules(self): + tenant_id = 'test-tenant' + ctx = context.Context('', tenant_id) + with self.firewall_policy(tenant_id=tenant_id, no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs = self._get_test_firewall_attrs() + attrs['firewall_policy_id'] = fwp_id + with contextlib.nested(self.firewall( + firewall_policy_id=fwp_id, + tenant_id=tenant_id, + admin_state_up=test_db_firewall.ADMIN_STATE_UP, + no_delete=True), self.firewall( + firewall_policy_id=fwp_id, + tenant_id=tenant_id, + admin_state_up=test_db_firewall.ADMIN_STATE_UP, + no_delete=True)) as fws: + fw_list = [fw['firewall'] for fw in fws] + f = self.callbacks.get_firewalls_for_tenant_without_rules + res = f(ctx, host='dummy') + for fw in res: + del fw['shared'] + self.assertEqual(res, fw_list) + + +class TestFirewallAgentApi(base.BaseTestCase): + def setUp(self): + super(TestFirewallAgentApi, self).setUp() + self.addCleanup(mock.patch.stopall) + + self.api = fwaas_plugin.FirewallAgentApi('topic', 'host') + self.mock_fanoutcast = mock.patch.object(self.api, + 'fanout_cast').start() + self.mock_msg = mock.patch.object(self.api, 'make_msg').start() + + def test_init(self): + self.assertEqual(self.api.topic, 'topic') + self.assertEqual(self.api.host, 'host') + + def _call_test_helper(self, method_name): + rv = getattr(self.api, method_name)(mock.sentinel.context, 'test') + self.assertEqual(rv, self.mock_fanoutcast.return_value) + self.mock_fanoutcast.assert_called_once_with( + mock.sentinel.context, + self.mock_msg.return_value, + topic='topic' + ) + + self.mock_msg.assert_called_once_with( + method_name, + firewall='test', + host='host' + ) + + def test_create_firewall(self): + self._call_test_helper('create_firewall') + + def test_update_firewall(self): + self._call_test_helper('update_firewall') + + def test_delete_firewall(self): + self._call_test_helper('delete_firewall') + + +class TestFirewallPluginBase(test_db_firewall.TestFirewallDBPlugin): + + def setUp(self): + super(TestFirewallPluginBase, self).setUp(fw_plugin=FW_PLUGIN_KLASS) + self.callbacks = self.plugin.callbacks + + def test_update_firewall(self): + ctx = context.get_admin_context() + name = "new_firewall1" + attrs = self._get_test_firewall_attrs(name) + + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up= + test_db_firewall.ADMIN_STATE_UP) as firewall: + fw_id = firewall['firewall']['id'] + res = self.callbacks.set_firewall_status(ctx, fw_id, + const.ACTIVE) + data = {'firewall': {'name': name}} + req = self.new_update_request('firewalls', data, fw_id) + res = self.deserialize(self.fmt, + req.get_response(self.ext_api)) + attrs = self._replace_firewall_status(attrs, + const.PENDING_CREATE, + const.PENDING_UPDATE) + for k, v in attrs.iteritems(): + self.assertEqual(res['firewall'][k], v) + + def test_update_firewall_fails_when_firewall_pending(self): + name = "new_firewall1" + attrs = self._get_test_firewall_attrs(name) + + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up= + test_db_firewall.ADMIN_STATE_UP) as firewall: + fw_id = firewall['firewall']['id'] + data = {'firewall': {'name': name}} + req = self.new_update_request('firewalls', data, fw_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 409) + + def test_update_firewall_policy_fails_when_firewall_pending(self): + name = "new_firewall1" + attrs = self._get_test_firewall_attrs(name) + + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up= + test_db_firewall.ADMIN_STATE_UP): + data = {'firewall_policy': {'name': name}} + req = self.new_update_request('firewall_policies', + data, fwp_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 409) + + def test_update_firewall_rule_fails_when_firewall_pending(self): + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall_rule(name='fwr1', no_delete=True) as fr: + fr_id = fr['firewall_rule']['id'] + fw_rule_ids = [fr_id] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up= + test_db_firewall.ADMIN_STATE_UP, + no_delete=True): + data = {'firewall_rule': {'protocol': 'udp'}} + req = self.new_update_request('firewall_rules', + data, fr_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 409) + + def test_delete_firewall(self): + ctx = context.get_admin_context() + attrs = self._get_test_firewall_attrs() + + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + attrs['firewall_policy_id'] = fwp_id + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up= + test_db_firewall.ADMIN_STATE_UP) as firewall: + fw_id = firewall['firewall']['id'] + attrs = self._replace_firewall_status(attrs, + const.PENDING_CREATE, + const.PENDING_DELETE) + req = self.new_delete_request('firewalls', fw_id) + req.get_response(self.ext_api) + fw_db = self.plugin._get_firewall(ctx, fw_id) + for k, v in attrs.iteritems(): + self.assertEqual(fw_db[k], v) + + def test_delete_firewall_after_agent_delete(self): + ctx = context.get_admin_context() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with self.firewall(firewall_policy_id=fwp_id, + no_delete=True) as fw: + fw_id = fw['firewall']['id'] + with ctx.session.begin(subtransactions=True): + req = self.new_delete_request('firewalls', fw_id) + res = req.get_response(self.ext_api) + self.assertEqual(res.status_int, 204) + self.plugin.callbacks.firewall_deleted(ctx, fw_id) + self.assertRaises(firewall.FirewallNotFound, + self.plugin.get_firewall, + ctx, fw_id) + + def test_make_firewall_dict_with_in_place_rules(self): + ctx = context.get_admin_context() + with self.firewall_policy(no_delete=True) as fwp: + fwp_id = fwp['firewall_policy']['id'] + with contextlib.nested(self.firewall_rule(name='fwr1', + no_delete=True), + self.firewall_rule(name='fwr2', + no_delete=True), + self.firewall_rule(name='fwr3', + no_delete=True)) as fr: + fw_rule_ids = [r['firewall_rule']['id'] for r in fr] + data = {'firewall_policy': + {'firewall_rules': fw_rule_ids}} + req = self.new_update_request('firewall_policies', data, + fwp_id) + req.get_response(self.ext_api) + attrs = self._get_test_firewall_attrs() + attrs['firewall_policy_id'] = fwp_id + with self.firewall(firewall_policy_id=fwp_id, + admin_state_up= + test_db_firewall.ADMIN_STATE_UP, + no_delete=True) as fw: + fw_id = fw['firewall']['id'] + fw_rules = ( + self.plugin._make_firewall_dict_with_rules(ctx, + fw_id) + ) + self.assertEqual(fw_rules['id'], fw_id) + self._compare_firewall_rule_lists( + fwp_id, fr, fw_rules['firewall_rule_list']) + + def test_make_firewall_dict_with_in_place_rules_no_policy(self): + ctx = context.get_admin_context() + with self.firewall(no_delete=True) as fw: + fw_id = fw['firewall']['id'] + fw_rules = self.plugin._make_firewall_dict_with_rules(ctx, fw_id) + self.assertEquals(fw_rules['firewall_rule_list'], []) diff --git a/neutron/tests/unit/test_extension_firewall.py b/neutron/tests/unit/test_extension_firewall.py new file mode 100644 index 00000000000..d3d086995df --- /dev/null +++ b/neutron/tests/unit/test_extension_firewall.py @@ -0,0 +1,552 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright 2013 Big Switch Networks, 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 spec +# +# @author: Sumit Naiksatam, sumitnaiksatam@gmail.com, Big Switch Networks, Inc. + +import copy + +import mock +from oslo.config import cfg +from webob import exc +import webtest + +from neutron.api import extensions +from neutron.api.v2 import attributes +from neutron.common import config +from neutron.extensions import firewall +from neutron import manager +from neutron.openstack.common import uuidutils +from neutron.plugins.common import constants +from neutron.tests import base +from neutron.tests.unit import test_api_v2 +from neutron.tests.unit import test_extensions +from neutron.tests.unit import testlib_api + + +_uuid = uuidutils.generate_uuid +_get_path = test_api_v2._get_path + + +class FirewallTestExtensionManager(object): + + def get_resources(self): + # Add the resources to the global attribute map + # This is done here as the setup process won't + # initialize the main API router which extends + # the global attribute map + attributes.RESOURCE_ATTRIBUTE_MAP.update( + firewall.RESOURCE_ATTRIBUTE_MAP) + return firewall.Firewall.get_resources() + + def get_actions(self): + return [] + + def get_request_extensions(self): + return [] + + +class FirewallExtensionTestCase(testlib_api.WebTestCase): + fmt = 'json' + + def setUp(self): + super(FirewallExtensionTestCase, self).setUp() + plugin = 'neutron.extensions.firewall.FirewallPluginBase' + # Ensure 'stale' patched copies of the plugin are never returned + manager.NeutronManager._instance = None + + # Ensure existing ExtensionManager is not used + extensions.PluginAwareExtensionManager._instance = None + + # Create the default configurations + args = ['--config-file', test_api_v2.etcdir('neutron.conf.test')] + config.parse(args) + + # Stubbing core plugin with Firewall plugin + cfg.CONF.set_override('core_plugin', plugin) + cfg.CONF.set_override('service_plugins', [plugin]) + + self._plugin_patcher = mock.patch(plugin, autospec=True) + self.plugin = self._plugin_patcher.start() + instance = self.plugin.return_value + instance.get_plugin_type.return_value = constants.FIREWALL + + ext_mgr = FirewallTestExtensionManager() + self.ext_mdw = test_extensions.setup_extensions_middleware(ext_mgr) + self.api = webtest.TestApp(self.ext_mdw) + super(FirewallExtensionTestCase, self).setUp() + + def tearDown(self): + self._plugin_patcher.stop() + self.api = None + self.plugin = None + cfg.CONF.reset() + super(FirewallExtensionTestCase, self).tearDown() + + def _test_entity_delete(self, entity): + """Does the entity deletion based on naming convention.""" + entity_id = _uuid() + path_prefix = 'fw/' + + if entity == 'firewall_policy': + entity_plural = 'firewall_policies' + else: + entity_plural = entity + 's' + + res = self.api.delete(_get_path(path_prefix + entity_plural, + id=entity_id, fmt=self.fmt)) + delete_entity = getattr(self.plugin.return_value, "delete_" + entity) + delete_entity.assert_called_with(mock.ANY, entity_id) + self.assertEqual(res.status_int, exc.HTTPNoContent.code) + + def test_create_firewall(self): + fw_id = _uuid() + data = {'firewall': {'description': 'descr_firewall1', + 'name': 'firewall1', + 'admin_state_up': True, + 'firewall_policy_id': _uuid(), + 'shared': False, + 'tenant_id': _uuid()}} + return_value = copy.copy(data['firewall']) + return_value.update({'id': fw_id}) + # since 'shared' is hidden + del return_value['shared'] + + instance = self.plugin.return_value + instance.create_firewall.return_value = return_value + res = self.api.post(_get_path('fw/firewalls', fmt=self.fmt), + self.serialize(data), + content_type='application/%s' % self.fmt) + instance.create_firewall.assert_called_with(mock.ANY, + firewall=data) + self.assertEqual(res.status_int, exc.HTTPCreated.code) + res = self.deserialize(res) + self.assertIn('firewall', res) + self.assertEqual(res['firewall'], return_value) + + def test_firewall_list(self): + fw_id = _uuid() + return_value = [{'tenant_id': _uuid(), + 'id': fw_id}] + + instance = self.plugin.return_value + instance.get_firewalls.return_value = return_value + + res = self.api.get(_get_path('fw/firewalls', fmt=self.fmt)) + + instance.get_firewalls.assert_called_with(mock.ANY, + fields=mock.ANY, + filters=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + + def test_firewall_get(self): + fw_id = _uuid() + return_value = {'tenant_id': _uuid(), + 'id': fw_id} + + instance = self.plugin.return_value + instance.get_firewall.return_value = return_value + + res = self.api.get(_get_path('fw/firewalls', + id=fw_id, fmt=self.fmt)) + + instance.get_firewall.assert_called_with(mock.ANY, + fw_id, + fields=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertIn('firewall', res) + self.assertEqual(res['firewall'], return_value) + + def test_firewall_update(self): + fw_id = _uuid() + update_data = {'firewall': {'name': 'new_name'}} + return_value = {'tenant_id': _uuid(), + 'id': fw_id} + + instance = self.plugin.return_value + instance.update_firewall.return_value = return_value + + res = self.api.put(_get_path('fw/firewalls', id=fw_id, + fmt=self.fmt), + self.serialize(update_data)) + + instance.update_firewall.assert_called_with(mock.ANY, fw_id, + firewall=update_data) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertIn('firewall', res) + self.assertEqual(res['firewall'], return_value) + + def test_firewall_delete(self): + self._test_entity_delete('firewall') + + def _test_create_firewall_rule(self, src_port, dst_port): + rule_id = _uuid() + data = {'firewall_rule': {'description': 'descr_firewall_rule1', + 'name': 'rule1', + 'shared': False, + 'protocol': 'tcp', + 'ip_version': 4, + 'source_ip_address': '192.168.0.1', + 'destination_ip_address': '127.0.0.1', + 'source_port': src_port, + 'destination_port': dst_port, + 'action': 'allow', + 'enabled': True, + 'tenant_id': _uuid()}} + expected_ret_val = copy.copy(data['firewall_rule']) + expected_ret_val['source_port'] = str(src_port) + expected_ret_val['destination_port'] = str(dst_port) + expected_call_args = copy.copy(expected_ret_val) + expected_ret_val['id'] = rule_id + instance = self.plugin.return_value + instance.create_firewall_rule.return_value = expected_ret_val + res = self.api.post(_get_path('fw/firewall_rules', fmt=self.fmt), + self.serialize(data), + content_type='application/%s' % self.fmt) + instance.create_firewall_rule.assert_called_with(mock.ANY, + firewall_rule= + {'firewall_rule': + expected_call_args}) + self.assertEqual(res.status_int, exc.HTTPCreated.code) + res = self.deserialize(res) + self.assertIn('firewall_rule', res) + self.assertEqual(res['firewall_rule'], expected_ret_val) + + def test_create_firewall_rule_with_integer_ports(self): + self._test_create_firewall_rule(1, 10) + + def test_create_firewall_rule_with_string_ports(self): + self._test_create_firewall_rule('1', '10') + + def test_create_firewall_rule_with_port_range(self): + self._test_create_firewall_rule('1:20', '30:40') + + def test_firewall_rule_list(self): + rule_id = _uuid() + return_value = [{'tenant_id': _uuid(), + 'id': rule_id}] + + instance = self.plugin.return_value + instance.get_firewall_rules.return_value = return_value + + res = self.api.get(_get_path('fw/firewall_rules', fmt=self.fmt)) + + instance.get_firewall_rules.assert_called_with(mock.ANY, + fields=mock.ANY, + filters=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + + def test_firewall_rule_get(self): + rule_id = _uuid() + return_value = {'tenant_id': _uuid(), + 'id': rule_id} + + instance = self.plugin.return_value + instance.get_firewall_rule.return_value = return_value + + res = self.api.get(_get_path('fw/firewall_rules', + id=rule_id, fmt=self.fmt)) + + instance.get_firewall_rule.assert_called_with(mock.ANY, + rule_id, + fields=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertIn('firewall_rule', res) + self.assertEqual(res['firewall_rule'], return_value) + + def test_firewall_rule_update(self): + rule_id = _uuid() + update_data = {'firewall_rule': {'action': 'deny'}} + return_value = {'tenant_id': _uuid(), + 'id': rule_id} + + instance = self.plugin.return_value + instance.update_firewall_rule.return_value = return_value + + res = self.api.put(_get_path('fw/firewall_rules', id=rule_id, + fmt=self.fmt), + self.serialize(update_data)) + + instance.update_firewall_rule.assert_called_with(mock.ANY, + rule_id, + firewall_rule= + update_data) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertIn('firewall_rule', res) + self.assertEqual(res['firewall_rule'], return_value) + + def test_firewall_rule_delete(self): + self._test_entity_delete('firewall_rule') + + def test_create_firewall_policy(self): + policy_id = _uuid() + data = {'firewall_policy': {'description': 'descr_firewall_policy1', + 'name': 'new_fw_policy1', + 'shared': False, + 'firewall_rules': [_uuid(), _uuid()], + 'audited': False, + 'tenant_id': _uuid()}} + return_value = copy.copy(data['firewall_policy']) + return_value.update({'id': policy_id}) + + instance = self.plugin.return_value + instance.create_firewall_policy.return_value = return_value + res = self.api.post(_get_path('fw/firewall_policies', + fmt=self.fmt), + self.serialize(data), + content_type='application/%s' % self.fmt) + instance.create_firewall_policy.assert_called_with(mock.ANY, + firewall_policy= + data) + self.assertEqual(res.status_int, exc.HTTPCreated.code) + res = self.deserialize(res) + self.assertIn('firewall_policy', res) + self.assertEqual(res['firewall_policy'], return_value) + + def test_firewall_policy_list(self): + policy_id = _uuid() + return_value = [{'tenant_id': _uuid(), + 'id': policy_id}] + + instance = self.plugin.return_value + instance.get_firewall_policies.return_value = return_value + + res = self.api.get(_get_path('fw/firewall_policies', + fmt=self.fmt)) + + instance.get_firewall_policies.assert_called_with(mock.ANY, + fields=mock.ANY, + filters=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + + def test_firewall_policy_get(self): + policy_id = _uuid() + return_value = {'tenant_id': _uuid(), + 'id': policy_id} + + instance = self.plugin.return_value + instance.get_firewall_policy.return_value = return_value + + res = self.api.get(_get_path('fw/firewall_policies', + id=policy_id, fmt=self.fmt)) + + instance.get_firewall_policy.assert_called_with(mock.ANY, + policy_id, + fields=mock.ANY) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertIn('firewall_policy', res) + self.assertEqual(res['firewall_policy'], return_value) + + def test_firewall_policy_update(self): + policy_id = _uuid() + update_data = {'firewall_policy': {'audited': True}} + return_value = {'tenant_id': _uuid(), + 'id': policy_id} + + instance = self.plugin.return_value + instance.update_firewall_policy.return_value = return_value + + res = self.api.put(_get_path('fw/firewall_policies', + id=policy_id, + fmt=self.fmt), + self.serialize(update_data)) + + instance.update_firewall_policy.assert_called_with(mock.ANY, + policy_id, + firewall_policy= + update_data) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertIn('firewall_policy', res) + self.assertEqual(res['firewall_policy'], return_value) + + def test_firewall_policy_delete(self): + self._test_entity_delete('firewall_policy') + + def test_firewall_policy_insert_rule(self): + firewall_policy_id = _uuid() + firewall_rule_id = _uuid() + ref_firewall_rule_id = _uuid() + + insert_data = {'firewall_rule_id': firewall_rule_id, + 'insert_before': ref_firewall_rule_id, + 'insert_after': None} + return_value = {'firewall_policy': + {'tenant_id': _uuid(), + 'id': firewall_policy_id, + 'firewall_rules': [ref_firewall_rule_id, + firewall_rule_id]}} + + instance = self.plugin.return_value + instance.insert_rule.return_value = return_value + + path = _get_path('fw/firewall_policies', id=firewall_policy_id, + action="insert_rule", + fmt=self.fmt) + res = self.api.put(path, self.serialize(insert_data)) + instance.insert_rule.assert_called_with(mock.ANY, firewall_policy_id, + insert_data) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertEqual(res, return_value) + + def test_firewall_policy_remove_rule(self): + firewall_policy_id = _uuid() + firewall_rule_id = _uuid() + + remove_data = {'firewall_rule_id': firewall_rule_id} + return_value = {'firewall_policy': + {'tenant_id': _uuid(), + 'id': firewall_policy_id, + 'firewall_rules': []}} + + instance = self.plugin.return_value + instance.remove_rule.return_value = return_value + + path = _get_path('fw/firewall_policies', id=firewall_policy_id, + action="remove_rule", + fmt=self.fmt) + res = self.api.put(path, self.serialize(remove_data)) + instance.remove_rule.assert_called_with(mock.ANY, firewall_policy_id, + remove_data) + self.assertEqual(res.status_int, exc.HTTPOk.code) + res = self.deserialize(res) + self.assertEqual(res, return_value) + + +class FirewallExtensionTestCaseXML(FirewallExtensionTestCase): + fmt = 'xml' + + +class TestFirewallAttributeValidators(base.BaseTestCase): + + def test_validate_port_range(self): + msg = firewall._validate_port_range(None) + self.assertIsNone(msg) + + msg = firewall._validate_port_range('10') + self.assertIsNone(msg) + + msg = firewall._validate_port_range(10) + self.assertIsNone(msg) + + msg = firewall._validate_port_range(-1) + self.assertEqual(msg, "Invalid port '-1'") + + msg = firewall._validate_port_range('66000') + self.assertEqual(msg, "Invalid port '66000'") + + msg = firewall._validate_port_range('10:20') + self.assertIsNone(msg) + + msg = firewall._validate_port_range('1:65535') + self.assertIsNone(msg) + + msg = firewall._validate_port_range('0:65535') + self.assertEqual(msg, "Invalid port '0'") + + msg = firewall._validate_port_range('1:65536') + self.assertEqual(msg, "Invalid port '65536'") + + msg = firewall._validate_port_range('abc:efg') + self.assertEqual(msg, "Port 'abc' is not a valid number") + + msg = firewall._validate_port_range('1:efg') + self.assertEqual(msg, "Port 'efg' is not a valid number") + + msg = firewall._validate_port_range('-1:10') + self.assertEqual(msg, "Invalid port '-1'") + + msg = firewall._validate_port_range('66000:10') + self.assertEqual(msg, "Invalid port '66000'") + + msg = firewall._validate_port_range('10:66000') + self.assertEqual(msg, "Invalid port '66000'") + + msg = firewall._validate_port_range('1:-10') + self.assertEqual(msg, "Invalid port '-10'") + + def test_validate_ip_or_subnet_or_none(self): + msg = firewall._validate_ip_or_subnet_or_none(None) + self.assertIsNone(msg) + + msg = firewall._validate_ip_or_subnet_or_none('1.1.1.1') + self.assertIsNone(msg) + + msg = firewall._validate_ip_or_subnet_or_none('1.1.1.0/24') + self.assertIsNone(msg) + + ip_addr = '1111.1.1.1' + msg = firewall._validate_ip_or_subnet_or_none(ip_addr) + self.assertEqual(msg, ("'%s' is not a valid IP address and " + "'%s' is not a valid IP subnet") % (ip_addr, + ip_addr)) + + ip_addr = '1.1.1.1 has whitespace' + msg = firewall._validate_ip_or_subnet_or_none(ip_addr) + self.assertEqual(msg, ("'%s' is not a valid IP address and " + "'%s' is not a valid IP subnet") % (ip_addr, + ip_addr)) + + ip_addr = '111.1.1.1\twhitespace' + msg = firewall._validate_ip_or_subnet_or_none(ip_addr) + self.assertEqual(msg, ("'%s' is not a valid IP address and " + "'%s' is not a valid IP subnet") % (ip_addr, + ip_addr)) + + ip_addr = '111.1.1.1\nwhitespace' + msg = firewall._validate_ip_or_subnet_or_none(ip_addr) + self.assertEqual(msg, ("'%s' is not a valid IP address and " + "'%s' is not a valid IP subnet") % (ip_addr, + ip_addr)) + + # Valid - IPv4 + cidr = "10.0.2.0/24" + msg = firewall._validate_ip_or_subnet_or_none(cidr, None) + self.assertIsNone(msg) + + # Valid - IPv6 without final octets + cidr = "fe80::/24" + msg = firewall._validate_ip_or_subnet_or_none(cidr, None) + self.assertIsNone(msg) + + # Valid - IPv6 with final octets + cidr = "fe80::0/24" + msg = firewall._validate_ip_or_subnet_or_none(cidr, None) + self.assertEqual(msg, ("'%s' is not a valid IP address and " + "'%s' isn't a recognized IP subnet cidr," + " 'fe80::/24' is recommended") % (cidr, + cidr)) + + cidr = "fe80::" + msg = firewall._validate_ip_or_subnet_or_none(cidr, None) + self.assertIsNone(msg) + + # Invalid - IPv6 with final octets, missing mask + cidr = "fe80::0" + msg = firewall._validate_ip_or_subnet_or_none(cidr, None) + self.assertIsNone(msg) + + # Invalid - Address format error + cidr = 'invalid' + msg = firewall._validate_ip_or_subnet_or_none(cidr, None) + self.assertEqual(msg, ("'%s' is not a valid IP address and " + "'%s' is not a valid IP subnet") % (cidr, + cidr))