[log]: implement logging plugin

This patch introduces the logging api definition and initial
implementation of LoggingApiPlugin. The api definition code will
be removed after [1] has been merged on neutron lib.

[1]https://review.openstack.org/#/c/415817/

Co-Authored-By: Yushiro FURUKAWA <y.furukawa_2@jp.fujitsu.com>

Partially-implements: blueprint security-group-logging
Related-Bug: #1468366
Change-Id: Iace31506502de25da9dce5fcfdbfe2c726bea27f
This commit is contained in:
Nguyen Phuong An 2016-11-09 17:02:48 +07:00
parent 4a316960f2
commit 913c9e78b9
14 changed files with 525 additions and 15 deletions

View File

@ -225,5 +225,12 @@
"get_security_group_rules": "rule:admin_or_owner",
"get_security_group_rule": "rule:admin_or_owner",
"create_security_group_rule": "rule:admin_or_owner",
"delete_security_group_rule": "rule:admin_or_owner"
"delete_security_group_rule": "rule:admin_or_owner",
"get_loggable_resources": "rule:admin_only",
"create_log": "rule:admin_only",
"update_log": "rule:admin_only",
"delete_log": "rule:admin_only",
"get_logs": "rule:admin_only",
"get_log": "rule:admin_only"
}

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
from neutron.objects.logapi import logging_resource as log_object
from neutron.objects import network
from neutron.objects import ports
from neutron.objects.qos import policy
@ -19,6 +20,7 @@ from neutron.objects import trunk
# Supported types
LOGGING_RESOURCE = log_object.Log.obj_name()
TRUNK = trunk.Trunk.obj_name()
QOS_POLICY = policy.QosPolicy.obj_name()
SUBPORT = trunk.SubPort.obj_name()
@ -38,6 +40,7 @@ _VALID_CLS = (
network.Network,
securitygroup.SecurityGroup,
securitygroup.SecurityGroupRule,
log_object.Log,
)
_TYPE_TO_CLS_MAP = {cls.obj_name(): cls for cls in _VALID_CLS}

View File

@ -0,0 +1,158 @@
# Copyright (c) 2017 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 abc
import itertools
from neutron_lib.api import converters
from neutron_lib.api import extensions as api_extensions
from neutron_lib.db import constants as db_const
from neutron_lib.services import base as service_base
import six
from neutron.api.v2 import resource_helper
from neutron.plugins.common import constants
from neutron.services.logapi.common import constants as log_const
LOG_PREFIX = "/log"
# Attribute Map
RESOURCE_ATTRIBUTE_MAP = {
'logs': {
'id': {'allow_post': False, 'allow_put': False,
'validate': {'type:uuid': None},
'is_visible': True,
'primary_key': True},
'project_id': {'allow_post': True, 'allow_put': False,
'required_by_policy': True,
'validate': {
'type:string':
db_const.PROJECT_ID_FIELD_SIZE},
'is_visible': True},
'name': {'allow_post': True, 'allow_put': True,
'validate': {'type:string': db_const.NAME_FIELD_SIZE},
'default': '', 'is_visible': True},
'resource_type': {'allow_post': True, 'allow_put': False,
'required_by_policy': True,
'validate':
{'type:string': db_const.RESOURCE_TYPE_FIELD_SIZE},
'is_visible': True},
'resource_id': {'allow_post': True, 'allow_put': False,
'validate': {'type:uuid_or_none': None},
'default': None, 'is_visible': True},
'event': {'allow_post': True, 'allow_put': False,
'validate': {'type:values': log_const.LOG_EVENTS},
'default': log_const.ALL_EVENT, 'is_visible': True},
'target_id': {'allow_post': True, 'allow_put': False,
'validate': {'type:uuid_or_none': None},
'default': None, 'is_visible': True},
'enabled': {'allow_post': True, 'allow_put': True,
'is_visible': True, 'default': True,
'convert_to': converters.convert_to_boolean},
},
'loggable_resources': {
'type': {'allow_post': False, 'allow_put': False,
'is_visible': True}},
}
class Logging(api_extensions.ExtensionDescriptor):
"""Neutron logging api extension."""
@classmethod
def get_name(cls):
return "Logging API Extension"
@classmethod
def get_alias(cls):
return "logging"
@classmethod
def get_description(cls):
return "Provides a logging API for resources such as security group"
@classmethod
def get_updated(cls):
return "2017-01-01T10:00:00-00:00"
@classmethod
def get_plugin_interface(cls):
return LoggingPluginBase
@classmethod
def get_resources(cls):
"""Returns Ext Resources."""
plural_mappings = resource_helper.build_plural_mappings(
{}, itertools.chain(RESOURCE_ATTRIBUTE_MAP))
resources = resource_helper.build_resource_info(
plural_mappings,
RESOURCE_ATTRIBUTE_MAP,
constants.LOG_API,
translate_name=True,
allow_bulk=True)
return resources
def update_attributes_map(self, attributes, extension_attrs_map=None):
super(Logging, self).update_attributes_map(
attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP)
def get_extended_resources(self, version):
if version == "2.0":
return dict(list(RESOURCE_ATTRIBUTE_MAP.items()))
else:
return {}
@six.add_metaclass(abc.ABCMeta)
class LoggingPluginBase(service_base.ServicePluginBase):
path_prefix = LOG_PREFIX
def get_plugin_description(self):
return "Logging API Service Plugin"
@classmethod
def get_plugin_type(cls):
return constants.LOG_API
@abc.abstractmethod
def get_logs(self, context, filters=None, fields=None, sorts=None,
limit=None, marker=None, page_reverse=False):
pass
@abc.abstractmethod
def get_log(self, context, log_id, fields=None):
pass
@abc.abstractmethod
def create_log(self, context, log):
pass
@abc.abstractmethod
def update_log(self, context, log_id, log):
pass
@abc.abstractmethod
def delete_log(self, context, log_id):
pass
@abc.abstractmethod
def get_loggable_resources(self, context, filters=None, fields=None,
sorts=None, limit=None,
marker=None, page_reverse=False):
pass

View File

@ -35,4 +35,4 @@ class SecurityEvent(obj_fields.String):
class SecurityEventField(obj_fields.AutoTypedField):
AUTO_TYPE = SecurityEvent(valid_values=log_const.VALID_EVENTS)
AUTO_TYPE = SecurityEvent(valid_values=log_const.LOG_EVENTS)

View File

@ -25,6 +25,7 @@ VPN = "VPN"
METERING = "METERING"
FLAVORS = "FLAVORS"
QOS = "QOS"
LOG_API = "LOGGING"
# Maps extension alias to service type that
# can be implemented by the core plugin.

View File

@ -16,5 +16,5 @@
ACCEPT_EVENT = 'ACCEPT'
DROP_EVENT = 'DROP'
ALL_EVENT = 'ALL'
VALID_EVENTS = [ACCEPT_EVENT, DROP_EVENT, ALL_EVENT]
LOG_EVENTS = [ACCEPT_EVENT, DROP_EVENT, ALL_EVENT]
LOGGING_PLUGIN = 'logging-plugin'

View File

@ -1,5 +1,4 @@
# Copyright 2016 Fujitsu Limited.
#
# Copyright 2017 Fujitsu Limited.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -18,13 +17,9 @@ from neutron._i18n import _
from neutron_lib import exceptions as n_exc
class ResourceLogNotFound(n_exc.NotFound):
message = _("Resource log %(id)s could not be found.")
class LogResourceNotFound(n_exc.NotFound):
message = _("Log resource %(log_id)s could not be found.")
class ParentResourceNotFound(n_exc.NotFound):
message = _("Parent resource %(parent_resource_id)s could not be found.")
class ResourceNotFound(n_exc.NotFound):
message = _("Resource %(resource_id)s could not be found.")
class InvalidLogReosurceType(n_exc.InvalidInput):
message = _("Invalid log resource_type: %(resource_type)s.")

View File

@ -0,0 +1,93 @@
# Copyright (c) 2017 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.db import api as db_api
from neutron.db import db_base_plugin_common
from neutron.extensions import logging as log_ext
from neutron.objects import base as base_obj
from neutron.objects.logapi import logging_resource as log_object
from neutron.services.logapi.common import exceptions as log_exc
class LoggingPlugin(log_ext.LoggingPluginBase):
"""Implementation of the Neutron logging api plugin."""
supported_extension_aliases = ['logging']
__native_pagination_support = True
__native_sorting_support = True
@property
def supported_logging_types(self):
# Todo(annp): supported_logging_types will dynamic load from
# log_drivers. So return value for this function is a temporary.
return []
@db_base_plugin_common.filter_fields
@db_base_plugin_common.convert_result_to_dict
def get_logs(self, context, filters=None, fields=None, sorts=None,
limit=None, marker=None, page_reverse=False):
"""Return information for available log objects"""
filters = filters or {}
pager = base_obj.Pager(sorts, limit, page_reverse, marker)
return log_object.Log.get_objects(context, _pager=pager, **filters)
def _get_log(self, context, log_id):
"""Return the log object or raise if not found"""
log_obj = log_object.Log.get_object(context, id=log_id)
if not log_obj:
raise log_exc.LogResourceNotFound(log_id=log_id)
return log_obj
@db_base_plugin_common.filter_fields
@db_base_plugin_common.convert_result_to_dict
def get_log(self, context, log_id, fields=None):
return self._get_log(context, log_id)
@db_base_plugin_common.convert_result_to_dict
def create_log(self, context, log):
"""Create a log object"""
log_data = log['log']
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.
# We need to remove redundant keyword.
log_data.pop('tenant_id', None)
log_obj = log_object.Log(context=context, **log_data)
log_obj.create()
return log_obj
@db_base_plugin_common.convert_result_to_dict
def update_log(self, context, log_id, log):
"""Update information for the specified log object"""
log_data = log['log']
with db_api.context_manager.writer.using(context):
log_obj = log_object.Log(context, id=log_id)
log_obj.update_fields(log_data, reset_changes=True)
log_obj.update()
return log_obj
def delete_log(self, context, log_id):
"""Delete the specified log object"""
with db_api.context_manager.writer.using(context):
log_obj = self._get_log(context, log_id)
log_obj.delete()
def get_loggable_resources(self, context, filters=None, fields=None,
sorts=None, limit=None,
marker=None, page_reverse=False):
"""Get supported logging types"""
return [{'type': type_}
for type_ in self.supported_logging_types]

View File

@ -225,5 +225,12 @@
"get_security_group_rules": "rule:admin_or_owner",
"get_security_group_rule": "rule:admin_or_owner",
"create_security_group_rule": "rule:admin_or_owner",
"delete_security_group_rule": "rule:admin_or_owner"
"delete_security_group_rule": "rule:admin_or_owner",
"get_loggable_resources": "rule:admin_only",
"create_log": "rule:admin_only",
"update_log": "rule:admin_only",
"delete_log": "rule:admin_only",
"get_logs": "rule:admin_only",
"get_log": "rule:admin_only"
}

View File

@ -323,4 +323,4 @@ def get_random_ipv6_mode():
def get_random_security_event():
return random.choice(log_const.VALID_EVENTS)
return random.choice(log_const.LOG_EVENTS)

View File

@ -0,0 +1,38 @@
# 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.api.rpc.callbacks.consumer import registry as cons_registry
from neutron.api.rpc.callbacks.producer import registry as prod_registry
from neutron.api.rpc.callbacks import resource_manager
from neutron.tests.unit import testlib_api
class BaseLogTestCase(testlib_api.SqlTestCase):
def setUp(self):
super(BaseLogTestCase, self).setUp()
with mock.patch.object(
resource_manager.ResourceCallbacksManager, '_singleton',
new_callable=mock.PropertyMock(return_value=False)):
self.cons_mgr = resource_manager.ConsumerResourceCallbacksManager()
self.prod_mgr = resource_manager.ProducerResourceCallbacksManager()
for mgr in (self.cons_mgr, self.prod_mgr):
mgr.clear()
mock.patch.object(
cons_registry, '_get_manager', return_value=self.cons_mgr).start()
mock.patch.object(
prod_registry, '_get_manager', return_value=self.prod_mgr).start()

View File

@ -0,0 +1,207 @@
# Copyright (C) 2017 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 import context
from neutron_lib.plugins import directory
from oslo_config import cfg
from oslo_utils import uuidutils
from neutron import manager
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.plugins.common import constants
from neutron.services.logapi.common import exceptions as log_exc
from neutron.tests.unit.services.logapi import base
DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'
class TestLoggingPlugin(base.BaseLogTestCase):
def setUp(self):
super(TestLoggingPlugin, self).setUp()
self.setup_coreplugin(load_plugins=False)
mock.patch('neutron.objects.db.api.create_object').start()
mock.patch('neutron.objects.db.api.update_object').start()
mock.patch('neutron.objects.db.api.delete_object').start()
mock.patch('neutron.objects.db.api.get_object').start()
# We don't use real models as per mocks above. We also need to mock-out
# methods that work with real data types
mock.patch(
'neutron.objects.base.NeutronDbObject.modify_fields_from_db'
).start()
cfg.CONF.set_override("core_plugin", DB_PLUGIN_KLASS)
cfg.CONF.set_override("service_plugins",
["neutron.services.logapi.logging_plugin.LoggingPlugin"])
manager.init()
self.log_plugin = directory.get_plugin(constants.LOG_API)
self.ctxt = context.Context('fake_user', 'fake_tenant')
mock.patch.object(self.ctxt.session, 'refresh').start()
mock.patch.object(self.ctxt.session, 'expunge').start()
def test_get_logs(self):
with mock.patch.object(log_object.Log, 'get_objects')\
as get_objects_mock:
filters = {'filter': 'filter_id'}
self.log_plugin.get_logs(self.ctxt, filters=filters)
get_objects_mock.assert_called_once_with(self.ctxt,
_pager=mock.ANY, filter='filter_id')
def test_get_log_without_return_value(self):
with mock.patch.object(log_object.Log, 'get_object',
return_value=None):
self.assertRaises(
log_exc.LogResourceNotFound,
self.log_plugin.get_log,
self.ctxt,
mock.ANY,
)
def test_get_log_with_return_value(self):
log_id = uuidutils.generate_uuid()
with mock.patch.object(log_object.Log, 'get_object')\
as get_object_mock:
self.log_plugin.get_log(self.ctxt, log_id)
get_object_mock.assert_called_once_with(self.ctxt,
id=log_id)
@mock.patch('neutron.db._utils.model_query')
def test_create_log_full_options(self, query_mock):
log = {'log': {'resource_type': 'security_group',
'enabled': True,
'resource_id': uuidutils.generate_uuid(),
'target_id': uuidutils.generate_uuid()}}
sg = mock.Mock()
port = mock.Mock()
new_log = mock.Mock()
with mock.patch.object(sg_object.SecurityGroup, 'get_object',
return_value=sg):
with mock.patch.object(ports.Port, 'get_object',
return_value=port):
with mock.patch('neutron.objects.logapi.'
'logging_resource.Log',
return_value=new_log) as init_log_mock:
self.log_plugin.create_log(self.ctxt, log)
init_log_mock.assert_called_once_with(context=self.ctxt,
**log['log'])
self.assertTrue(new_log.create.called)
def test_create_log_without_sg_resource(self):
log = {'log': {'resource_type': 'security_group',
'enabled': True,
'target_id': uuidutils.generate_uuid()}}
new_log = mock.Mock()
new_log.enabled = True
port = mock.Mock()
with mock.patch.object(ports.Port, 'get_object', return_value=port):
with mock.patch('neutron.objects.logapi.logging_resource.Log',
return_value=new_log) as init_log_mock:
self.log_plugin.create_log(self.ctxt, log)
init_log_mock.assert_called_once_with(
context=self.ctxt, **log['log'])
self.assertTrue(new_log.create.called)
def test_create_log_without_parent_resource(self):
log = {'log': {'resource_type': 'security_group',
'enabled': True,
'resource_id': uuidutils.generate_uuid()}}
new_log = mock.Mock()
new_log.enabled = True
sg = mock.Mock()
with mock.patch.object(sg_object.SecurityGroup, 'get_object',
return_value=sg):
with mock.patch('neutron.objects.logapi.logging_resource.Log',
return_value=new_log) as init_log_mock:
self.log_plugin.create_log(self.ctxt, log)
init_log_mock.assert_called_once_with(context=self.ctxt,
**log['log'])
self.assertTrue(new_log.create.called)
def test_create_log_without_target(self):
log = {'log': {'resource_type': 'security_group',
'enabled': True, }}
new_log = mock.Mock()
new_log.enabled = True
with mock.patch('neutron.objects.logapi.'
'logging_resource.Log',
return_value=new_log) as init_log_mock:
self.log_plugin.create_log(self.ctxt, log)
init_log_mock.assert_called_once_with(context=self.ctxt,
**log['log'])
self.assertTrue(new_log.create.called)
def test_create_log_disabled(self):
log_data = {'log': {'resource_type': 'security_group',
'enabled': False}}
new_log = mock.Mock()
new_log.enabled = False
with mock.patch('neutron.objects.logapi.'
'logging_resource.Log',
return_value=new_log) as init_log_mock:
self.log_plugin.create_log(self.ctxt, log_data)
init_log_mock.assert_called_once_with(
context=self.ctxt, **log_data['log'])
self.assertTrue(new_log.create.called)
def test_update_log(self):
log_data = {'log': {'enabled': True}}
new_log = mock.Mock()
new_log.id = uuidutils.generate_uuid()
with mock.patch('neutron.objects.logapi.'
'logging_resource.Log',
return_value=new_log) as update_log_mock:
self.log_plugin.update_log(self.ctxt, new_log.id, log_data)
update_log_mock.assert_called_once_with(self.ctxt,
id=new_log.id)
new_log.update_fields.assert_called_once_with(log_data['log'],
reset_changes=True)
self.assertTrue(new_log.update.called)
def test_update_log_none_enabled(self):
log_data = {'log': {}}
new_log = mock.Mock()
new_log.id = uuidutils.generate_uuid()
with mock.patch('neutron.objects.logapi.'
'logging_resource.Log',
return_value=new_log) as update_log_mock:
self.log_plugin.update_log(self.ctxt, new_log.id, log_data)
update_log_mock.assert_called_once_with(self.ctxt,
id=new_log.id)
new_log.update_fields.assert_called_once_with(log_data['log'],
reset_changes=True)
self.assertTrue(new_log.update.called)
def test_delete_log(self):
delete_log = mock.Mock()
delete_log.id = uuidutils.generate_uuid()
with mock.patch.object(log_object.Log, 'get_object',
return_value=delete_log) as delete_log_mock:
self.log_plugin.delete_log(self.ctxt, delete_log.id)
delete_log_mock.assert_called_once_with(self.ctxt,
id=delete_log.id)
self.assertTrue(delete_log.delete.called)
def test_delete_nonexistent_log(self):
with mock.patch.object(log_object.Log, 'get_object',
return_value=None):
self.assertRaises(
log_exc.LogResourceNotFound,
self.log_plugin.delete_log,
self.ctxt,
mock.ANY)

View File

@ -82,6 +82,7 @@ neutron.service_plugins =
timestamp = neutron.services.timestamp.timestamp_plugin:TimeStampPlugin
trunk = neutron.services.trunk.plugin:TrunkPlugin
loki = neutron.services.loki.loki_plugin:LokiPlugin
logapi = neutron.services.logapi.logging_plugin:LoggingPlugin
neutron.ml2.type_drivers =
flat = neutron.plugins.ml2.drivers.type_flat:FlatTypeDriver
local = neutron.plugins.ml2.drivers.type_local:LocalTypeDriver