From dd9aec1745838c1b9a5ce252d21e0fa5e02b01bb Mon Sep 17 00:00:00 2001 From: Cuong Nguyen Date: Thu, 11 Jan 2018 17:08:48 +0700 Subject: [PATCH] Add log validator for FWaaS v2 logging This patch adds a method to validate logging request for FWaaS. It also introduces get_fwg_attached_to_port method in db_v2 as helper function for logging feature. Co-Authored-By: Nguyen Phuong An Co-Authored-By: Kim Bao Long Partial-Bug: #1720727 Change-Id: I723b48a75ba3f19476785bec0c5d5295d04aac95 --- .../db/firewall/v2/firewall_db_v2.py | 8 + neutron_fwaas/services/logapi/__init__.py | 0 neutron_fwaas/services/logapi/constants.py | 21 +++ neutron_fwaas/services/logapi/exceptions.py | 34 ++++ neutron_fwaas/services/logapi/fwg_validate.py | 125 ++++++++++++++ .../tests/unit/services/logapi/__init__.py | 0 .../unit/services/logapi/test_fwg_validate.py | 157 ++++++++++++++++++ 7 files changed, 345 insertions(+) create mode 100644 neutron_fwaas/services/logapi/__init__.py create mode 100644 neutron_fwaas/services/logapi/constants.py create mode 100644 neutron_fwaas/services/logapi/exceptions.py create mode 100644 neutron_fwaas/services/logapi/fwg_validate.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/__init__.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/test_fwg_validate.py diff --git a/neutron_fwaas/db/firewall/v2/firewall_db_v2.py b/neutron_fwaas/db/firewall/v2/firewall_db_v2.py index c44905c7c..6f809ff24 100644 --- a/neutron_fwaas/db/firewall/v2/firewall_db_v2.py +++ b/neutron_fwaas/db/firewall/v2/firewall_db_v2.py @@ -844,6 +844,14 @@ class FirewallPluginDb(common_db_mixin.CommonDbMixin): return default_fwg.id return None + def get_fwg_attached_to_port(self, context, port_id): + """Return a firewall group ID is associated to a port""" + fwg_port = self._model_query(context, FirewallGroupPortAssociation).\ + filter_by(port_id=port_id).first() + if fwg_port: + return fwg_port.firewall_group_id + return None + def _ensure_default_firewall_group(self, context, tenant_id): """Create a default firewall group if one doesn't exist for a tenant diff --git a/neutron_fwaas/services/logapi/__init__.py b/neutron_fwaas/services/logapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/services/logapi/constants.py b/neutron_fwaas/services/logapi/constants.py new file mode 100644 index 000000000..fc82767b0 --- /dev/null +++ b/neutron_fwaas/services/logapi/constants.py @@ -0,0 +1,21 @@ +# Copyright (c) 2018 Fujitsu Limited +# 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. + + +# Firewall group logging resource type +FIREWALL_GROUP = 'firewall_group' + +# Target logging resource type +TARGET_RESOURCE = 'port which is associated with the firewall group' diff --git a/neutron_fwaas/services/logapi/exceptions.py b/neutron_fwaas/services/logapi/exceptions.py new file mode 100644 index 000000000..c7af753c0 --- /dev/null +++ b/neutron_fwaas/services/logapi/exceptions.py @@ -0,0 +1,34 @@ +# Copyright (c) 2018 Fujitsu Limited +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron._i18n import _ +from neutron_lib import exceptions as n_exc + +# TODO(annp or longkb): move to neutron-lib + + +class FWGIsNotReadyForLogging(n_exc.InvalidInput): + message = _("Firewall group %(fwg_id)s is not ready for logging " + "because of %(fwg_status)s status.") + + +class TargetResourceNotAssociated(n_exc.InvalidInput): + message = _("Target resource %(target_id)s is not associated with " + "any firewall group.") + + +class PortIsNotReadyForLogging(n_exc.InvalidInput): + message = _("Target resource %(target_id)s is not ready for logging " + "because of %(port_status)s status.") diff --git a/neutron_fwaas/services/logapi/fwg_validate.py b/neutron_fwaas/services/logapi/fwg_validate.py new file mode 100644 index 000000000..ea3f3b0e0 --- /dev/null +++ b/neutron_fwaas/services/logapi/fwg_validate.py @@ -0,0 +1,125 @@ +# Copyright (c) 2018 Fujitsu Limited +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron.objects import ports +from neutron.services.logapi.common import exceptions as log_exc +from neutron.services.logapi.common import validators +from neutron_lib import constants as nl_const +from neutron_lib.plugins import directory +from sqlalchemy.orm import exc as orm_exc + +from neutron_fwaas.common import fwaas_constants +from neutron_fwaas.services.logapi import constants as log_const +from neutron_fwaas.services.logapi import exceptions as fwg_log_exc + +fwg_plugin = None + + +def _check_fwg(context, fwg_id): + try: + fwg = fwg_plugin.get_firewall_group(context, id=fwg_id) + except orm_exc.NoResultFound: + raise log_exc.ResourceNotFound(resource_id=fwg_id) + + if fwg['status'] != nl_const.ACTIVE: + raise fwg_log_exc.FWGIsNotReadyForLogging( + fwg_id=fwg_id, fwg_status=fwg['status']) + + +def _check_fwg_port(context, port_id): + + # Checking port exists + port = ports.Port.get_object(context, id=port_id) + if not port: + raise log_exc.TargetResourceNotFound(target_id=port_id) + + device_owner = port.get('device_owner', '') + # Checking supported firewall group logging for vm port + if device_owner.startswith(nl_const.DEVICE_OWNER_COMPUTE_PREFIX): + if not validators.validate_log_type_for_port( + log_const.FIREWALL_GROUP, port): + raise log_exc.LoggingTypeNotSupported( + log_type=log_const.FIREWALL_GROUP, + port_id=port_id) + # Checking supported firewall group for router interface, DVR interface, + # and HA replicated interface + elif device_owner not in nl_const.ROUTER_INTERFACE_OWNERS: + raise log_exc.LoggingTypeNotSupported( + log_type=log_const.FIREWALL_GROUP, port_id=port_id) + + # Checking port status + port_status = port.get('status') + if port_status != nl_const.PORT_STATUS_ACTIVE: + raise fwg_log_exc.PortIsNotReadyForLogging(target_id=port_id, + port_status=port_status) + + # Checking whether router port or vm port binding with any firewall group + fwg_id = fwg_plugin.driver.firewall_db.get_fwg_attached_to_port( + context, port_id=port_id) + + if not fwg_id: + raise fwg_log_exc.TargetResourceNotAssociated(target_id=port_id) + + fwg = fwg_plugin.get_firewall_group(context, id=fwg_id) + + if fwg['status'] != nl_const.ACTIVE: + raise fwg_log_exc.FWGIsNotReadyForLogging(fwg_id=fwg_id, + fwg_status=fwg['status']) + + +def _check_target_resource_bound_fwg(context, fwg_id, target_id): + ports = fwg_plugin.driver.firewall_db.get_ports_in_firewall_group( + context=context, firewall_group_id=fwg_id) + if target_id not in ports: + raise log_exc.InvalidResourceConstraint( + resource=log_const.FIREWALL_GROUP, + resource_id=fwg_id, + target_resource=log_const.TARGET_RESOURCE, + target_id=target_id) + + +@validators.ResourceValidateRequest.register(log_const.FIREWALL_GROUP) +def validate_firewall_group_request(context, log_data): + """Validate a log request + + This method validates log request is satisfied or not. + + A ResourceNotFound will be raised if resource_id in log_data not exists or + a TargetResourceNotFound will be raised if target_id in log_data not + exists. Beside, FWGIsNotReadyForLogging will be raised in the case of + queried firewall group is not in ACTIVE state. PortIsNotReadyForLogging + exception will be raised if port is not in ACTIVE status. Besides, + TargetResourceNotAssociated exception will be raised if a given port does + not have any firewall group attach to. This method will also raise a + LoggingTypeNotSupported, if there is no log_driver supporting for + resource_type in log_data. + + In addition, if log_data specify both resource_id and target_id. A + InvalidResourceConstraint will be raised if there is no constraint between + resource_id and target_id. + + """ + + global fwg_plugin + if not fwg_plugin: + fwg_plugin = directory.get_plugin(fwaas_constants.FIREWALL_V2) + resource_id = log_data.get('resource_id') + target_id = log_data.get('target_id') + if resource_id and target_id: + _check_target_resource_bound_fwg(context, resource_id, target_id) + if resource_id: + _check_fwg(context, resource_id) + if target_id: + _check_fwg_port(context, target_id) diff --git a/neutron_fwaas/tests/unit/services/logapi/__init__.py b/neutron_fwaas/tests/unit/services/logapi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/neutron_fwaas/tests/unit/services/logapi/test_fwg_validate.py b/neutron_fwaas/tests/unit/services/logapi/test_fwg_validate.py new file mode 100644 index 000000000..2ee9876fa --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/test_fwg_validate.py @@ -0,0 +1,157 @@ +# Copyright (c) 2018 Fujitsu Limited +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +from neutron.objects import ports +from neutron.services.logapi.common import exceptions as log_exc +from neutron.services.logapi.common import validators +from neutron.tests import base +from neutron_lib import constants as nl_const +from sqlalchemy.orm import exc as orm_exc + +from neutron_fwaas.services.logapi import exceptions as fwg_log_exc +from neutron_fwaas.services.logapi import fwg_validate + + +class TestFWGLogRequestValidations(base.BaseTestCase): + """Test validator for a log creation request""" + + def setUp(self): + super(TestFWGLogRequestValidations, self).setUp() + fwg_validate.fwg_plugin = mock.Mock() + fwg_validate.fwg_plugin.driver = mock.Mock() + fwg_validate.fwg_plugin.driver.firewall_db = mock.Mock() + + def test_validate_fwg_request(self): + m_context = mock.Mock() + fake_data = { + 'resource_type': 'firewall_group', + 'resource_id': 'fake_fwg_id' + } + with mock.patch.object(fwg_validate, '_check_fwg'): + fwg_validate.validate_firewall_group_request(m_context, fake_data) + fwg_validate._check_fwg.\ + assert_called_with(m_context, fake_data['resource_id']) + fake_data = { + 'resource_type': 'firewall_group', + 'resource_id': 'fake_fwg_id', + 'target_id': 'fake_port_id' + } + with mock.patch.object(fwg_validate, + '_check_target_resource_bound_fwg'): + with mock.patch.object(fwg_validate, '_check_fwg'): + with mock.patch.object(fwg_validate, '_check_fwg_port'): + fwg_validate.validate_firewall_group_request(m_context, + fake_data) + fwg_validate._check_target_resource_bound_fwg.\ + assert_called_with(m_context, + fake_data['resource_id'], + fake_data['target_id']) + fwg_validate._check_fwg. \ + assert_called_with(m_context, + fake_data['resource_id']) + fwg_validate._check_fwg_port. \ + assert_called_with(m_context, + fake_data['target_id']) + + def test_validate_request_fwg_id_not_exists(self): + + with mock.patch.object(fwg_validate.fwg_plugin, 'get_firewall_group', + side_effect=orm_exc.NoResultFound): + self.assertRaises( + log_exc.ResourceNotFound, + fwg_validate._check_fwg, + mock.ANY, + 'fake_fwg_id') + + def test_validate_request_fwg_not_active(self): + fake_fwg = {'id': '1234', 'status': 'PENDING'} + with mock.patch.object(fwg_validate.fwg_plugin, 'get_firewall_group', + return_value=fake_fwg): + self.assertRaises( + fwg_log_exc.FWGIsNotReadyForLogging, + fwg_validate._check_fwg, + mock.ANY, + 'fake_fwg_id') + + def test_validate_request_router_or_port_id_not_exists(self): + with mock.patch.object(ports.Port, 'get_object', return_value=None): + self.assertRaises( + log_exc.TargetResourceNotFound, + fwg_validate._check_fwg_port, + mock.ANY, + 'fake_port_id') + + def test_validate_request_unsupported_fwg_log_on_vm_port(self): + + fake_port = {'device_owner': "compute:"} + with mock.patch.object(ports.Port, 'get_object', + return_value=fake_port): + with mock.patch.object(validators, 'validate_log_type_for_port', + return_value=False): + self.assertRaises( + log_exc.LoggingTypeNotSupported, + fwg_validate._check_fwg_port, + mock.ANY, + 'fake_port_id') + + def test_validate_request_router_port_is_not_active(self): + + non_active_status = [nl_const.PORT_STATUS_DOWN, + nl_const.PORT_STATUS_ERROR, + nl_const.PORT_STATUS_NOTAPPLICABLE, + nl_const.PORT_STATUS_BUILD] + fake_port = [{'device_owner': nl_const.DEVICE_OWNER_ROUTER_INTF, + 'status': status} + for status in non_active_status] + with mock.patch.object(ports.Port, 'get_object', + side_effect=fake_port): + for status in non_active_status: + self.assertRaises( + fwg_log_exc.PortIsNotReadyForLogging, + fwg_validate._check_fwg_port, + mock.ANY, + 'fake_port_id') + + def test_validate_request_router_port_was_not_associated_fwg(self): + + fake_port = {'device_owner': nl_const.DEVICE_OWNER_ROUTER_INTF, + 'status': nl_const.PORT_STATUS_ACTIVE} + + with mock.patch.object(ports.Port, 'get_object', + return_value=fake_port): + with mock.patch.object(fwg_validate.fwg_plugin.driver.firewall_db, + 'get_fwg_attached_to_port', + return_value=None): + self.assertRaises( + fwg_log_exc.TargetResourceNotAssociated, + fwg_validate._check_fwg_port, + mock.ANY, + 'fake_port_id') + + def test_validate_request_target_resource_not_bound_fwg(self): + + fake_ports_in_fwg = ['fake_port_id1, fake_port_id2'] + with mock.patch.object( + fwg_validate.fwg_plugin.driver.firewall_db, + 'get_ports_in_firewall_group', + return_value=fake_ports_in_fwg): + + self.assertRaises( + log_exc.InvalidResourceConstraint, + fwg_validate._check_target_resource_bound_fwg, + mock.ANY, + mock.ANY, + 'fake_target_id')