From cdb973e77dc33dcccfb8c62eed69e4b2318acd8d Mon Sep 17 00:00:00 2001 From: Cuong Nguyen Date: Fri, 22 Dec 2017 17:09:20 +0700 Subject: [PATCH] Generic validate_request method for logging This patch adds a generic validate_request method for logging, So logging can be extended to other resources like firewall group. Co-Authored-By: Nguyen Phuong An Co-Authored-By: Kim Bao Long Partial-Bug: #1720727 Change-Id: I9d580645131a9f1b8233ea5f6e0378cd98f023f1 --- neutron/services/logapi/common/constants.py | 3 + neutron/services/logapi/common/exceptions.py | 6 + neutron/services/logapi/common/sg_validate.py | 84 +++++++++++++ neutron/services/logapi/common/validators.py | 104 ++++++++-------- .../logapi/drivers/openvswitch/driver.py | 5 + neutron/services/logapi/logging_plugin.py | 7 +- .../logapi/common/test_sg_validate.py | 115 ++++++++++++++++++ .../services/logapi/common/test_validators.py | 75 +++--------- .../services/logapi/test_logging_plugin.py | 7 ++ 9 files changed, 290 insertions(+), 116 deletions(-) create mode 100644 neutron/services/logapi/common/sg_validate.py create mode 100644 neutron/tests/unit/services/logapi/common/test_sg_validate.py diff --git a/neutron/services/logapi/common/constants.py b/neutron/services/logapi/common/constants.py index dc72c5dfcd1..1f9ae180bdb 100644 --- a/neutron/services/logapi/common/constants.py +++ b/neutron/services/logapi/common/constants.py @@ -22,6 +22,9 @@ LOGGING_PLUGIN = 'logging-plugin' # supported logging types SECURITY_GROUP = 'security_group' +# target resource types +PORT = 'port' + RPC_NAMESPACE_LOGGING = 'logging-plugin' # Define for rpc_method_key diff --git a/neutron/services/logapi/common/exceptions.py b/neutron/services/logapi/common/exceptions.py index 7147916c986..d52e7121535 100644 --- a/neutron/services/logapi/common/exceptions.py +++ b/neutron/services/logapi/common/exceptions.py @@ -51,3 +51,9 @@ class LogapiDriverException(n_exc.NeutronException): class CookieNotFound(n_exc.NotFound): message = _("Cookie %(cookie_id)s could not be found.") + + +class ValidatedMethodNotFound(n_exc.NeutronException): + """A validated method not found Exception""" + message = _('Validated method for %(resource_type)s log ' + 'could not be found.') diff --git a/neutron/services/logapi/common/sg_validate.py b/neutron/services/logapi/common/sg_validate.py new file mode 100644 index 00000000000..f1a2910fedc --- /dev/null +++ b/neutron/services/logapi/common/sg_validate.py @@ -0,0 +1,84 @@ +# 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 oslo_log import log as logging +from sqlalchemy.orm import exc as orm_exc + +from neutron.db import _utils as db_utils +from neutron.db.models import securitygroup as sg_db +from neutron.objects import ports +from neutron.objects import securitygroup as sg_object +from neutron.services.logapi.common import constants as log_const +from neutron.services.logapi.common import exceptions as log_exc +from neutron.services.logapi.common import validators + +LOG = logging.getLogger(__name__) + + +def _check_port_bound_sg(context, sg_id, port_id): + try: + db_utils.model_query(context, sg_db.SecurityGroupPortBinding)\ + .filter_by(security_group_id=sg_id, port_id=port_id).one() + except orm_exc.NoResultFound: + raise log_exc.InvalidResourceConstraint( + resource=log_const.SECURITY_GROUP, + resource_id=sg_id, + target_resource=log_const.PORT, + target_id=port_id + ) + + +def _check_sg_exists(context, sg_id): + if sg_object.SecurityGroup.count(context, id=sg_id) < 1: + raise log_exc.ResourceNotFound(resource_id=sg_id) + + +def _get_port(context, port_id): + port = ports.Port.get_object(context, id=port_id) + if not port: + raise log_exc.TargetResourceNotFound(target_id=port_id) + return port + + +@validators.ResourceValidateRequest.register(log_const.SECURITY_GROUP) +def validate_security_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. 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. + + """ + + resource_id = log_data.get('resource_id') + target_id = log_data.get('target_id') + if resource_id: + _check_sg_exists(context, resource_id) + if target_id: + port = _get_port(context, target_id) + if not validators.validate_log_type_for_port( + log_const.SECURITY_GROUP, port): + raise log_exc.LoggingTypeNotSupported( + log_type=log_const.SECURITY_GROUP, + port_id=target_id) + if resource_id and target_id: + _check_port_bound_sg(context, resource_id, target_id) diff --git a/neutron/services/logapi/common/validators.py b/neutron/services/logapi/common/validators.py index 3301ca601b2..44e3d6f4693 100644 --- a/neutron/services/logapi/common/validators.py +++ b/neutron/services/logapi/common/validators.py @@ -19,13 +19,7 @@ from neutron_lib.plugins import constants as plugin_const from neutron_lib.plugins import directory from neutron_lib.plugins import utils from oslo_log import log as logging -from sqlalchemy.orm import exc as orm_exc -from neutron.db import _utils as db_utils -from neutron.db.models import securitygroup as sg_db -from neutron.objects import ports -from neutron.objects import securitygroup as sg_object -from neutron.services.logapi.common import constants as log_const from neutron.services.logapi.common import exceptions as log_exc LOG = logging.getLogger(__name__) @@ -36,30 +30,6 @@ SKIPPED_VIF_TYPES = [ ] -def _check_port_bound_sg(context, sg_id, port_id): - try: - db_utils.model_query(context, sg_db.SecurityGroupPortBinding)\ - .filter_by(security_group_id=sg_id, port_id=port_id).one() - except orm_exc.NoResultFound: - raise log_exc.InvalidResourceConstraint(resource='security_group', - resource_id=sg_id, - target_resource='port', - target_id=port_id) - - -def _check_secgroup_exists(context, sg_id): - number_of_matching = sg_object.SecurityGroup.count(context, id=sg_id) - if number_of_matching < 1: - raise log_exc.ResourceNotFound(resource_id=sg_id) - - -def _get_port(context, port_id): - port = ports.Port.get_object(context, id=port_id) - if not port: - raise log_exc.TargetResourceNotFound(target_id=port_id) - return port - - def _validate_vnic_type(driver, vnic_type, port_id): if driver.is_vnic_compatible(vnic_type): return True @@ -113,30 +83,56 @@ def validate_log_type_for_port(log_type, port): return False -def validate_request(context, log_data): - """Validate a log request +class ResourceValidateRequest(object): - 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. - This method will also raise a LoggingTypeNotSupported, if there is no - log_driver supporting for resource_type in log_data. + _instance = None - 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. + def __init__(self): + self.validate_methods = {} - """ - resource_id = log_data.get('resource_id') - target_id = log_data.get('target_id') - resource_type = log_data.get('resource_type') - if resource_type == log_const.SECURITY_GROUP: - if resource_id: - _check_secgroup_exists(context, resource_id) - if target_id: - port = _get_port(context, target_id) - if not validate_log_type_for_port(resource_type, port): - raise log_exc.LoggingTypeNotSupported(log_type=resource_type, - port_id=target_id) - if resource_id and target_id: - _check_port_bound_sg(context, resource_id, target_id) + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + @property + def validate_methods_map(self): + return self.validate_methods + + def validate_request(self, context, log_data): + """ + This method will get validated method according to resource_type. An + InvalidLogResourceType exception will be raised if there is no logging + driver that supports resource_type as logging resource. In addition, + a ValidatedMethodNotFound exception will be raised if a validate method + was not registered for resource_type. + """ + + resource_type = log_data.get('resource_type') + log_plugin = directory.get_plugin(alias=plugin_const.LOG_API) + supported_logging_types = log_plugin.supported_logging_types + + if resource_type in supported_logging_types: + method = self.get_validated_method(resource_type) + method(context, log_data) + else: + raise log_exc.InvalidLogResourceType(resource_type=resource_type) + + def get_validated_method(self, resource_type): + """Get the validated method for resource_type""" + + method = self.validate_methods[resource_type] + if not method: + raise log_exc.ValidatedMethodNotFound(resource_type=resource_type) + return method + + @classmethod + def register(cls, resource_type): + """This is intended to be used as a decorator to register a validated + method for resource_type. + """ + def func_wrap(func): + cls.get_instance().validate_methods[resource_type] = func + return func + return func_wrap diff --git a/neutron/services/logapi/drivers/openvswitch/driver.py b/neutron/services/logapi/drivers/openvswitch/driver.py index ab7b2be9a2f..03ed6ee3d51 100644 --- a/neutron/services/logapi/drivers/openvswitch/driver.py +++ b/neutron/services/logapi/drivers/openvswitch/driver.py @@ -16,6 +16,7 @@ from neutron_lib.api.definitions import portbindings from neutron_lib.callbacks import resources from oslo_log import log as logging +from oslo_utils import importutils from neutron.services.logapi.common import constants as log_const from neutron.services.logapi.drivers import base @@ -55,4 +56,8 @@ def register(): server_rpc.get_sg_log_info_for_log_resources} ] DRIVER.register_rpc_methods(log_const.SECURITY_GROUP, rpc_methods) + # Trigger decorator + importutils.import_module( + 'neutron.services.logapi.common.sg_validate' + ) LOG.debug('Open vSwitch logging driver registered') diff --git a/neutron/services/logapi/logging_plugin.py b/neutron/services/logapi/logging_plugin.py index 19811655fcd..5e56e0428b7 100644 --- a/neutron/services/logapi/logging_plugin.py +++ b/neutron/services/logapi/logging_plugin.py @@ -36,6 +36,7 @@ class LoggingPlugin(log_ext.LoggingPluginBase): def __init__(self): super(LoggingPlugin, self).__init__() self.driver_manager = driver_mgr.LoggingServiceDriverManager() + self.validator_mgr = validators.ResourceValidateRequest.get_instance() @property def supported_logging_types(self): @@ -67,11 +68,7 @@ class LoggingPlugin(log_ext.LoggingPluginBase): def create_log(self, context, log): """Create a log object""" log_data = log['log'] - resource_type = log_data['resource_type'] - if resource_type not in self.supported_logging_types: - raise log_exc.InvalidLogResourceType( - resource_type=resource_type) - validators.validate_request(context, log_data) + self.validator_mgr.validate_request(context, log_data) with db_api.context_manager.writer.using(context): # body 'log' contains both tenant_id and project_id # but only latter needs to be used to create Log object. diff --git a/neutron/tests/unit/services/logapi/common/test_sg_validate.py b/neutron/tests/unit/services/logapi/common/test_sg_validate.py new file mode 100644 index 00000000000..48ee07abd07 --- /dev/null +++ b/neutron/tests/unit/services/logapi/common/test_sg_validate.py @@ -0,0 +1,115 @@ +# 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_lib.plugins import directory +from oslo_utils import importutils +from sqlalchemy.orm import exc as orm_exc + +from neutron.objects import ports +from neutron.objects import securitygroup as sg_object +from neutron.services.logapi.common import exceptions as log_exc +from neutron.services.logapi.common import validators +from neutron.tests import base + + +class FakePlugin(object): + + def __init__(self): + self.validator_mgr = validators.ResourceValidateRequest.get_instance() + self.supported_logging_types = ['security_group'] + + +class TestSGLogRequestValidations(base.BaseTestCase): + """Test validation for a request""" + def setUp(self): + self.log_plugin = FakePlugin() + importutils.import_module('neutron.services.logapi.common.sg_validate') + super(TestSGLogRequestValidations, self).setUp() + + def test_validate_request_resource_id_not_exists(self): + log_data = {'resource_type': 'security_group', + 'resource_id': 'fake_sg_id'} + + with mock.patch.object(directory, 'get_plugin', + return_value=self.log_plugin): + with mock.patch.object(sg_object.SecurityGroup, 'count', + return_value=0): + self.assertRaises( + log_exc.ResourceNotFound, + self.log_plugin.validator_mgr.validate_request, + mock.ANY, + log_data) + + def test_validate_request_target_id_not_exists(self): + log_data = {'resource_type': 'security_group', + 'target_id': 'fake_port_id'} + + with mock.patch.object(directory, 'get_plugin', + return_value=self.log_plugin): + with mock.patch.object(ports.Port, 'get_object', + return_value=None): + self.assertRaises( + log_exc.TargetResourceNotFound, + self.log_plugin.validator_mgr.validate_request, + mock.ANY, + log_data) + + def test_validate_request_unsupported_logging_type(self): + log_data = {'resource_type': 'security_group', + 'target_id': 'fake_port_id'} + + with mock.patch.object(directory, 'get_plugin', + return_value=self.log_plugin): + with mock.patch.object(ports.Port, 'get_object', + return_value=mock.ANY): + with mock.patch.object(validators, + 'validate_log_type_for_port', + return_value=False): + self.assertRaises( + log_exc.LoggingTypeNotSupported, + self.log_plugin.validator_mgr.validate_request, + mock.ANY, + log_data) + + def test_validate_request_invalid_resource_constraint(self): + log_data = {'resource_type': 'security_group', + 'resource_id': 'fake_sg_id', + 'target_id': 'fake_port_id'} + + class FakeFiltered(object): + def one(self): + raise orm_exc.NoResultFound + + class FakeSGPortBinding(object): + def filter_by(self, security_group_id, port_id): + return FakeFiltered() + + with mock.patch.object(directory, 'get_plugin', + return_value=self.log_plugin): + with mock.patch.object( + sg_object.SecurityGroup, 'count', return_value=1): + with mock.patch.object( + ports.Port, 'get_object', return_value=mock.ANY): + with mock.patch.object(validators, + 'validate_log_type_for_port', + return_value=True): + with mock.patch('neutron.db._utils.model_query', + return_value=FakeSGPortBinding()): + self.assertRaises( + log_exc.InvalidResourceConstraint, + self.log_plugin.validator_mgr.validate_request, + mock.ANY, + log_data) diff --git a/neutron/tests/unit/services/logapi/common/test_validators.py b/neutron/tests/unit/services/logapi/common/test_validators.py index 31222537094..a8d8a791c33 100644 --- a/neutron/tests/unit/services/logapi/common/test_validators.py +++ b/neutron/tests/unit/services/logapi/common/test_validators.py @@ -19,78 +19,39 @@ from neutron_lib import context from neutron_lib.plugins import constants as plugin_const from neutron_lib.plugins import directory from oslo_utils import uuidutils -from sqlalchemy.orm import exc as orm_exc from neutron.objects import ports -from neutron.objects import securitygroup as sg_object -from neutron.services.logapi.common import exceptions as log_exc from neutron.services.logapi.common import validators from neutron.tests import base from neutron.tests.unit.services.logapi.drivers import ( test_manager as drv_mgr) -class TestRequestValidations(base.BaseTestCase): - """Test validation for a request""" +class TestRegisterValidateMethods(base.BaseTestCase): - def test_validate_request_resource_sg_not_exists(self): - log_data = {'resource_type': 'security_group', - 'resource_id': 'fake_sg_id'} - with mock.patch.object(sg_object.SecurityGroup, 'count', - return_value=0): - self.assertRaises(log_exc.ResourceNotFound, - validators.validate_request, - mock.ANY, - log_data) + def setUp(self): + self.validator_mgr = validators.ResourceValidateRequest.get_instance() + super(TestRegisterValidateMethods, self).setUp() - def test_validate_request_target_resource_port_not_exists(self): - log_data = {'resource_type': 'security_group', - 'target_id': 'fake_port_id'} - with mock.patch.object(ports.Port, 'get_object', return_value=None): - self.assertRaises(log_exc.TargetResourceNotFound, - validators.validate_request, - mock.ANY, - log_data) + def test_register_validate_method(self): + self.validator_mgr.validate_methods.clear() + resource_type = 'fake_resource' - def test_validate_request_log_type_not_supported_on_port(self): - log_data = {'resource_type': 'security_group', - 'target_id': 'fake_port_id'} - with mock.patch.object(ports.Port, 'get_object', - return_value=mock.ANY): - with mock.patch.object(validators, 'validate_log_type_for_port', - return_value=False): - self.assertRaises(log_exc.LoggingTypeNotSupported, - validators.validate_request, - mock.ANY, - log_data) + @validators.ResourceValidateRequest.register(resource_type) + def fake_method(): + pass - def test_validate_request_invalid_resource_constraint(self): - log_data = {'resource_type': 'security_group', - 'resource_id': 'fake_sg_id', - 'target_id': 'fake_port_id'} + self.assertEqual({'fake_resource': fake_method}, + self.validator_mgr.validate_methods_map) - class FakeFiltered(object): - def one(self): - raise orm_exc.NoResultFound + def test_get_validated_method(self): - class FakeSGPortBinding(object): - def filter_by(self, security_group_id, port_id): - return FakeFiltered() + @validators.ResourceValidateRequest.register('fake_resource') + def fake_method(): + pass - with mock.patch.object( - sg_object.SecurityGroup, 'count', return_value=1): - with mock.patch.object( - ports.Port, 'get_object', return_value=mock.ANY): - with mock.patch.object(validators, - 'validate_log_type_for_port', - return_value=True): - with mock.patch('neutron.db._utils.model_query', - return_value=FakeSGPortBinding()): - self.assertRaises( - log_exc.InvalidResourceConstraint, - validators.validate_request, - mock.ANY, - log_data) + actual = self.validator_mgr.get_validated_method('fake_resource') + self.assertEqual(fake_method, actual) class TestLogDriversLoggingTypeValidations(drv_mgr.TestLogDriversManagerBase): diff --git a/neutron/tests/unit/services/logapi/test_logging_plugin.py b/neutron/tests/unit/services/logapi/test_logging_plugin.py index b209257a983..c3f50c162a4 100644 --- a/neutron/tests/unit/services/logapi/test_logging_plugin.py +++ b/neutron/tests/unit/services/logapi/test_logging_plugin.py @@ -25,6 +25,7 @@ from neutron.objects.logapi import logging_resource as log_object from neutron.objects import ports from neutron.objects import securitygroup as sg_object from neutron.services.logapi.common import exceptions as log_exc +from neutron.services.logapi.common import sg_validate from neutron.tests.unit.services.logapi import base DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2' @@ -51,6 +52,12 @@ class TestLoggingPlugin(base.BaseLogTestCase): ["neutron.services.logapi.logging_plugin.LoggingPlugin"]) manager.init() + mock.patch( + 'neutron.services.logapi.common.validators.' + 'ResourceValidateRequest.get_validated_method', + return_value=sg_validate.validate_security_group_request + ).start() + self.log_plugin = directory.get_plugin(plugin_const.LOG_API) self.log_plugin.driver_manager = mock.Mock() log_types = mock.PropertyMock(return_value=SUPPORTED_LOGGING_TYPES)