From 48b82f7c925d1fdbe0a5b69f99999ecd6ca731c5 Mon Sep 17 00:00:00 2001 From: Kim Bao Long Date: Fri, 7 Sep 2018 12:30:25 +0700 Subject: [PATCH] Subscribe Neutron port update events for FWG Logging handling Currently, FWaaS L3 logging only sync with firewall group related events, it does not care about Neutron port status update as reported in [1]. This patch aims to subscribe Neutron callback events that will trigger FWG Logging driver for further handling. [1] https://bugs.launchpad.net/neutron/+bug/1788759 Change-Id: If2754040dad0bae6c224ceaec8b7e66436a2195d Co-Authored-By: Nguyen Phuong An Closes-Bug: #1788759 --- .../logapi/agents/drivers/iptables/driver.py | 4 + .../services/logapi/common/log_db_api.py | 19 +- .../services/logapi/common/port_callback.py | 40 ++++ .../services/logapi/common/test_log_db_api.py | 38 +++- .../logapi/common/test_port_callback.py | 203 ++++++++++++++++++ 5 files changed, 296 insertions(+), 8 deletions(-) create mode 100644 neutron_fwaas/services/logapi/common/port_callback.py create mode 100644 neutron_fwaas/tests/unit/services/logapi/common/test_port_callback.py diff --git a/neutron_fwaas/services/logapi/agents/drivers/iptables/driver.py b/neutron_fwaas/services/logapi/agents/drivers/iptables/driver.py index 61a28c9a4..b694228e5 100644 --- a/neutron_fwaas/services/logapi/agents/drivers/iptables/driver.py +++ b/neutron_fwaas/services/logapi/agents/drivers/iptables/driver.py @@ -22,6 +22,7 @@ from oslo_utils import importutils from neutron_fwaas.common import fwaas_constants from neutron_fwaas.services.logapi.common import fwg_callback +from neutron_fwaas.services.logapi.common import port_callback from neutron_fwaas.services.logapi import constants as fw_const from neutron_fwaas.services.logapi.rpc import log_server as rpc_server @@ -64,4 +65,7 @@ def register(): # Register resource callback handler manager.register( fwaas_constants.FIREWALL_GROUP, fwg_callback.FirewallGroupCallBack) + # Register resource callback handler for Neutron ports + manager.register(resources.PORT, port_callback.NeutronPortCallBack) + LOG.debug('FWaaS L3 Logging driver based iptables registered') diff --git a/neutron_fwaas/services/logapi/common/log_db_api.py b/neutron_fwaas/services/logapi/common/log_db_api.py index 5df87a274..d188413c3 100644 --- a/neutron_fwaas/services/logapi/common/log_db_api.py +++ b/neutron_fwaas/services/logapi/common/log_db_api.py @@ -55,10 +55,12 @@ def _get_ports_being_logged(context, log_obj): # TODO(longkb): L2 ports will be supported in the future # Check whether a port is router port or not. if device_owner in nl_const.ROUTER_INTERFACE_OWNERS: - # Check whether a port is attached to firewall group or not - fwg = fw_plugin_db.get_fwg_attached_to_port(context, port_id) - if fwg: - filtered_port_ids.append(port_id) + # Checking port status + if port.get('status') == nl_const.PORT_STATUS_ACTIVE: + # Check whether a port is attached to firewall group or not + fwg = fw_plugin_db.get_fwg_attached_to_port(context, port_id) + if fwg: + filtered_port_ids.append(port_id) return filtered_port_ids @@ -75,6 +77,15 @@ def _make_log_info_dict(log_obj, port_ids): def get_logs_for_port(context, port_id): """Return a list of log_resources bound to a given port_id""" + global fw_plugin_db + if not fw_plugin_db: + fw_plugin = directory.get_plugin(fwaas_constants.FIREWALL_V2) + + # NOTE(longkb): check whether fw plugin was loaded or not. + if not fw_plugin: + return [] + fw_plugin_db = fw_plugin.driver.firewall_db + logs_bounded = [] port = port_objects.Port.get_object(context, id=port_id) diff --git a/neutron_fwaas/services/logapi/common/port_callback.py b/neutron_fwaas/services/logapi/common/port_callback.py new file mode 100644 index 000000000..e508ad07f --- /dev/null +++ b/neutron_fwaas/services/logapi/common/port_callback.py @@ -0,0 +1,40 @@ +# 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.services.logapi.common import constants as log_const +from neutron.services.logapi.drivers import manager +from neutron_lib.callbacks import events +from neutron_lib import constants as nl_const + +from neutron_fwaas.services.logapi.common import log_db_api + + +class NeutronPortCallBack(manager.ResourceCallBackBase): + + def handle_event(self, resource, event, trigger, **kwargs): + if event == events.AFTER_UPDATE: + context = kwargs.get('context') + original_port = kwargs.get('original_port') + port = kwargs.get('port') + + if port['device_owner'] in nl_const.ROUTER_INTERFACE_OWNERS: + if original_port['status'] != port['status']: + self.trigger_logging(context, port) + + def trigger_logging(self, context, port): + log_resources = log_db_api.get_logs_for_port(context, port['id']) + if log_resources: + self.resource_push_api( + log_const.RESOURCE_UPDATE, context, log_resources) diff --git a/neutron_fwaas/tests/unit/services/logapi/common/test_log_db_api.py b/neutron_fwaas/tests/unit/services/logapi/common/test_log_db_api.py index 3f42fcdda..33b193f9b 100644 --- a/neutron_fwaas/tests/unit/services/logapi/common/test_log_db_api.py +++ b/neutron_fwaas/tests/unit/services/logapi/common/test_log_db_api.py @@ -54,13 +54,15 @@ def _fake_log_info(id, project_id, ports_id, event='ALL'): return expected -def _fake_port_object(port_id, device_owner, +def _fake_port_object(port_id, device_owner, status, project_id=uuidutils.generate_uuid()): port_data = { 'id': port_id, 'device_owner': device_owner, 'project_id': project_id } + if status: + port_data['status'] = status return port_data @@ -77,13 +79,16 @@ class LoggingRpcCallbackTestCase(base.BaseTestCase): self.router_port = uuidutils.generate_uuid() self.fake_vm_port = \ _fake_port_object(self.vm_port, - nl_const.DEVICE_OWNER_COMPUTE_PREFIX) + nl_const.DEVICE_OWNER_COMPUTE_PREFIX, + nl_const.PORT_STATUS_ACTIVE) self.fake_router_port = \ _fake_port_object(self.router_port, - nl_const.DEVICE_OWNER_ROUTER_INTF) + nl_const.DEVICE_OWNER_ROUTER_INTF, + nl_const.PORT_STATUS_ACTIVE) self.fake_router_ports = \ - [_fake_port_object(self.router_port, device) + [_fake_port_object(self.router_port, device, + nl_const.PORT_STATUS_ACTIVE) for device in nl_const.ROUTER_INTERFACE_OWNERS] def test_get_fwg_log_info_for_log_resources(self): @@ -159,6 +164,19 @@ class LoggingRpcCallbackTestCase(base.BaseTestCase): log_db_api._get_ports_being_logged(self.context, log_obj) self.assertEqual([self.router_port], logged_port_ids) + # Test with inactive router port + self.fake_router_port['status'] = nl_const.PORT_STATUS_DOWN + log_obj = _create_log_object(tenant_id, resource_id=fwg_id, + target_id=self.router_port) + + log_db_api.fw_plugin_db. \ + get_fwg_attached_to_port = mock.Mock(return_value='fwg_id') + with mock.patch.object(port_objects.Port, 'get_object', + return_value=self.fake_router_port): + logged_port_ids = \ + log_db_api._get_ports_being_logged(self.context, log_obj) + self.assertEqual([], logged_port_ids) + def test_get_ports_being_logged_with_resource_id(self): tenant_id = uuidutils.generate_uuid() fwg_id = uuidutils.generate_uuid() @@ -199,6 +217,18 @@ class LoggingRpcCallbackTestCase(base.BaseTestCase): log_db_api._get_ports_being_logged(self.context, log_obj) self.assertEqual([self.router_port], logged_port_ids) + # Test with inactive router port + log_db_api.fw_plugin_db.get_ports_in_firewall_group = \ + mock.Mock(return_value=[self.router_port]) + log_db_api.fw_plugin_db. \ + get_fwg_attached_to_port = mock.Mock(return_value='fwg_id') + + with mock.patch.object(port_objects.Port, 'get_object', + return_value=self.fake_router_port): + logged_port_ids = \ + log_db_api._get_ports_being_logged(self.context, log_obj) + self.assertEqual([self.router_port], logged_port_ids) + def test_get_ports_being_logged_with_ports_in_tenant(self): tenant_id = uuidutils.generate_uuid() log_obj = _create_log_object(tenant_id) diff --git a/neutron_fwaas/tests/unit/services/logapi/common/test_port_callback.py b/neutron_fwaas/tests/unit/services/logapi/common/test_port_callback.py new file mode 100644 index 000000000..a7eba97f6 --- /dev/null +++ b/neutron_fwaas/tests/unit/services/logapi/common/test_port_callback.py @@ -0,0 +1,203 @@ +# 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 as port_objects +from neutron.services.logapi.drivers import base as log_driver_base +from neutron.services.logapi.drivers import manager as driver_mgr +from neutron.tests import base +from neutron_lib.callbacks import events +from neutron_lib.callbacks import registry +from neutron_lib.callbacks import resources +from neutron_lib import constants as nl_const +from oslo_utils import uuidutils + +from neutron_fwaas.services.logapi.common import log_db_api +from neutron_fwaas.services.logapi.common import port_callback + +FAKE_DRIVER = None + + +class FakeDriver(log_driver_base.DriverBase): + + @staticmethod + def create(): + return FakeDriver( + name='fake_driver', + vif_types=[], + vnic_types=[], + supported_logging_types=['firewall_group'], + requires_rpc=True + ) + + +def fake_register(): + global FAKE_DRIVER + if not FAKE_DRIVER: + FAKE_DRIVER = FakeDriver.create() + driver_mgr.register(resources.PORT, port_callback.NeutronPortCallBack) + + +class TestFirewallGroupRuleCallback(base.BaseTestCase): + + def setUp(self): + super(TestFirewallGroupRuleCallback, self).setUp() + self.driver_manager = driver_mgr.LoggingServiceDriverManager() + self.port_callback = port_callback.NeutronPortCallBack(mock.Mock(), + mock.Mock()) + self.m_context = mock.Mock() + + def _create_port_object(self, name=None, device_owner=None, + status=nl_const.PORT_STATUS_ACTIVE): + port_data = { + 'id': uuidutils.generate_uuid(), + 'project_id': 'fake_tenant_id', + 'status': status + } + if name: + port_data['name'] = name + if device_owner: + port_data['device_owner'] = device_owner + return port_objects.Port(**port_data) + + @mock.patch.object(port_callback.NeutronPortCallBack, 'handle_event') + def test_handle_event(self, m_port_cb_handler): + fake_register() + self.driver_manager.register_driver(FAKE_DRIVER) + + registry.notify(resources.PORT, events.AFTER_CREATE, mock.ANY) + m_port_cb_handler.assert_called_once_with( + resources.PORT, events.AFTER_CREATE, mock.ANY) + + m_port_cb_handler.reset_mock() + registry.notify( + resources.PORT, events.AFTER_UPDATE, mock.ANY) + m_port_cb_handler.assert_called_once_with( + resources.PORT, events.AFTER_UPDATE, mock.ANY) + + m_port_cb_handler.reset_mock() + registry.notify( + 'non_registered_resource', events.AFTER_CREATE, mock.ANY) + m_port_cb_handler.assert_not_called() + + m_port_cb_handler.reset_mock() + registry.notify( + 'non_registered_resource', events.AFTER_UPDATE, mock.ANY) + m_port_cb_handler.assert_not_called() + + def test_trigger_logging(self): + fake_log_obj = mock.Mock() + self.port_callback.resource_push_api = mock.Mock() + port = self._create_port_object(device_owner='fake_device_owner') + + # Test with log resource could be found from DB + with mock.patch.object(log_db_api, 'get_logs_for_port', + return_value=[fake_log_obj]): + self.port_callback.trigger_logging(self.m_context, port) + self.port_callback.resource_push_api.assert_called() + + # Test with log resource could not be found from DB + self.port_callback.resource_push_api.reset_mock() + with mock.patch.object(log_db_api, 'get_logs_for_port', + return_value=[]): + self.port_callback.trigger_logging(self.m_context, port) + self.port_callback.resource_push_api.assert_not_called() + + def test_handle_event_with_router_port(self): + with mock.patch.object(self.port_callback, 'trigger_logging'): + # Test for router port enabling + f_port_config = self._fake_port_config( + nl_const.DEVICE_OWNER_ROUTER_INTF, action='enable') + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + **f_port_config) + self.port_callback.trigger_logging.assert_called() + + # Test for router port disabling + self.port_callback.trigger_logging.reset_mock() + f_port_config = self._fake_port_config( + nl_const.DEVICE_OWNER_ROUTER_INTF, action='disable') + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + **f_port_config) + self.port_callback.trigger_logging.assert_called() + + # Test for router port status does not change + self.port_callback.trigger_logging.reset_mock() + f_port_config = \ + self._fake_port_config(nl_const.DEVICE_OWNER_ROUTER_INTF) + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + **f_port_config) + self.port_callback.trigger_logging.assert_not_called() + + def test_handle_event_with_non_router_port(self): + with mock.patch.object(self.port_callback, 'trigger_logging'): + # Test for port enabling + f_port_config = self._fake_port_config('fake_port_type', + action='enable') + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + **f_port_config) + self.port_callback.trigger_logging.assert_not_called() + + # Test for port disabling + self.port_callback.trigger_logging.reset_mock() + f_port_config = self._fake_port_config('fake_port_type', + action='disable') + self.port_callback.handle_event(mock.ANY, + events.AFTER_UPDATE, + mock.ANY, + **f_port_config) + self.port_callback.trigger_logging.assert_not_called() + + def _fake_port_config(self, device_owner, action=None): + f_kwargs = {} + f_kwargs['context'] = self.m_context + if action == 'enable': + # Create original port with DOWN status + original_port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_DOWN) + + # Create port with ACTIVE status + port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE) + f_kwargs['original_port'] = original_port + f_kwargs['port'] = port + elif action == 'disable': + # Create original port with ACTIVE status + original_port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE) + + # Create port with DOWN status + port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_DOWN) + f_kwargs['original_port'] = original_port + f_kwargs['port'] = port + else: + # Create original port with ACTIVE status + original_port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE) + + # Create port with ACTIVE status + port = self._create_port_object( + device_owner=device_owner, status=nl_const.PORT_STATUS_ACTIVE) + f_kwargs['original_port'] = original_port + f_kwargs['port'] = port + return f_kwargs