diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 2a29d33e865..2a2816d0318 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -62c781cb6192 +c8c222d42aa9 diff --git a/neutron/db/migration/alembic_migrations/versions/pike/expand/c8c222d42aa9_logging_api.py b/neutron/db/migration/alembic_migrations/versions/pike/expand/c8c222d42aa9_logging_api.py new file mode 100644 index 00000000000..4c41a1fc585 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/pike/expand/c8c222d42aa9_logging_api.py @@ -0,0 +1,60 @@ +# Copyright 2017 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. +# + +"""logging api + +Revision ID: c8c222d42aa9 +Revises: 62c781cb6192 +Create Date: 2017-05-30 11:51:08.173604 + +""" + +# revision identifiers, used by Alembic. +revision = 'c8c222d42aa9' +down_revision = '62c781cb6192' + +from alembic import op +import sqlalchemy as sa + +from neutron_lib.db import constants as db_const + + +def upgrade(): + + op.create_table( + 'logs', + sa.Column('project_id', + sa.String(length=db_const.PROJECT_ID_FIELD_SIZE), + nullable=True, + index=True), + sa.Column('id', sa.String(length=db_const.UUID_FIELD_SIZE), + nullable=False), + sa.Column('standard_attr_id', sa.BigInteger(), nullable=False), + sa.Column('name', sa.String(length=db_const.NAME_FIELD_SIZE), + nullable=True), + sa.Column('resource_type', sa.String(length=36), nullable=False), + sa.Column('resource_id', sa.String(length=db_const.UUID_FIELD_SIZE), + nullable=True, + index=True), + sa.Column('target_id', sa.String(length=db_const.UUID_FIELD_SIZE), + nullable=True, + index=True), + sa.Column('event', sa.String(length=255), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.ForeignKeyConstraint(['standard_attr_id'], + ['standardattributes.id'], + ondelete='CASCADE'), + sa.UniqueConstraint('standard_attr_id')) diff --git a/neutron/db/models/loggingapi.py b/neutron/db/models/loggingapi.py new file mode 100644 index 00000000000..bd77fd3030e --- /dev/null +++ b/neutron/db/models/loggingapi.py @@ -0,0 +1,37 @@ +# 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_lib.db import constants as db_const +from neutron_lib.db import model_base +import sqlalchemy as sa + +from neutron.db import standard_attr + + +class Log(standard_attr.HasStandardAttributes, model_base.BASEV2, + model_base.HasId, model_base.HasProject): + """Represents neutron logging resource database""" + + __tablename__ = 'logs' + + name = sa.Column(sa.String(db_const.NAME_FIELD_SIZE)) + resource_type = sa.Column(sa.String(36), nullable=False) + resource_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE), + nullable=True, index=True) + event = sa.Column(sa.String(255), nullable=False) + target_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE), + nullable=True, index=True) + enabled = sa.Column(sa.Boolean()) + api_collections = ['logs'] diff --git a/neutron/objects/logapi/__init__.py b/neutron/objects/logapi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/objects/logapi/event_types.py b/neutron/objects/logapi/event_types.py new file mode 100644 index 00000000000..7575a2e9334 --- /dev/null +++ b/neutron/objects/logapi/event_types.py @@ -0,0 +1,38 @@ +# 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_versionedobjects import fields as obj_fields + +from neutron._i18n import _ +from neutron.services.logapi.common import constants as log_const + + +class SecurityEvent(obj_fields.String): + def __init__(self, valid_values, **kwargs): + self._valid_values = valid_values + super(SecurityEvent, self).__init__(**kwargs) + + def coerce(self, obj, attr, value): + if value not in self._valid_values: + msg = ( + _("Field value %(value)s is not in the list " + "of valid values: %(values)s") % + {'value': value, 'values': self._valid_values} + ) + raise ValueError(msg) + return super(SecurityEvent, self).coerce(obj, attr, value) + + +class SecurityEventField(obj_fields.AutoTypedField): + AUTO_TYPE = SecurityEvent(valid_values=log_const.VALID_EVENTS) diff --git a/neutron/objects/logapi/logging_resource.py b/neutron/objects/logapi/logging_resource.py new file mode 100644 index 00000000000..a2df707bbfb --- /dev/null +++ b/neutron/objects/logapi/logging_resource.py @@ -0,0 +1,45 @@ +# 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 oslo_versionedobjects import base as obj_base +from oslo_versionedobjects import fields as obj_fields + +from neutron.db.models import loggingapi as log_db +from neutron.objects import base +from neutron.objects import common_types +from neutron.objects.logapi import event_types +from neutron.services.logapi.common import constants as log_const + + +@obj_base.VersionedObjectRegistry.register +class Log(base.NeutronDbObject): + # Version 1.0: Initial version + VERSION = '1.0' + + db_model = log_db.Log + + fields = { + 'id': common_types.UUIDField(), + 'project_id': obj_fields.StringField(nullable=True), + 'name': obj_fields.StringField(nullable=True), + 'resource_type': obj_fields.StringField(), + 'resource_id': common_types.UUIDField(nullable=True, default=None), + 'target_id': common_types.UUIDField(nullable=True, default=None), + 'event': event_types.SecurityEventField(default=log_const.ALL_EVENT), + 'enabled': obj_fields.BooleanField(default=True), + } + + fields_no_update = ['project_id', 'resource_type', 'resource_id', + 'target_id', 'event'] diff --git a/neutron/services/logapi/__init__.py b/neutron/services/logapi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/services/logapi/common/__init__.py b/neutron/services/logapi/common/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/services/logapi/common/constants.py b/neutron/services/logapi/common/constants.py new file mode 100644 index 00000000000..acda2a632da --- /dev/null +++ b/neutron/services/logapi/common/constants.py @@ -0,0 +1,20 @@ +# Copyright 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. + +ACCEPT_EVENT = 'ACCEPT' +DROP_EVENT = 'DROP' +ALL_EVENT = 'ALL' +VALID_EVENTS = [ACCEPT_EVENT, DROP_EVENT, ALL_EVENT] +LOGGING_PLUGIN = 'logging-plugin' diff --git a/neutron/services/logapi/common/exceptions.py b/neutron/services/logapi/common/exceptions.py new file mode 100644 index 00000000000..2192370fcf9 --- /dev/null +++ b/neutron/services/logapi/common/exceptions.py @@ -0,0 +1,30 @@ +# Copyright 2016 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 + + +class ResourceLogNotFound(n_exc.NotFound): + message = _("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.") diff --git a/neutron/tests/tools.py b/neutron/tests/tools.py index 60a88161f32..fc105f04e83 100644 --- a/neutron/tests/tools.py +++ b/neutron/tests/tools.py @@ -34,6 +34,7 @@ import unittest2 from neutron.api.v2 import attributes from neutron.common import constants as n_const from neutron.plugins.common import constants as p_const +from neutron.services.logapi.common import constants as log_const class AttributeMapMemento(fixtures.Fixture): @@ -319,3 +320,7 @@ def reset_random_seed(): def get_random_ipv6_mode(): return random.choice(constants.IPV6_MODES) + + +def get_random_security_event(): + return random.choice(log_const.VALID_EVENTS) diff --git a/neutron/tests/unit/db/test_standard_attr.py b/neutron/tests/unit/db/test_standard_attr.py index dc71be234f5..8ac39e854d4 100644 --- a/neutron/tests/unit/db/test_standard_attr.py +++ b/neutron/tests/unit/db/test_standard_attr.py @@ -69,7 +69,8 @@ class StandardAttrAPIImapctTestCase(testlib_api.SqlTestCase): # doc/source/devref/api_extensions.rst expected = ['subnets', 'trunks', 'routers', 'segments', 'security_group_rules', 'networks', 'policies', - 'subnetpools', 'ports', 'security_groups', 'floatingips'] + 'subnetpools', 'ports', 'security_groups', 'floatingips', + 'logs'] self.assertEqual( set(expected), set(standard_attr.get_standard_attr_resource_model_map().keys()) diff --git a/neutron/tests/unit/objects/logapi/__init__.py b/neutron/tests/unit/objects/logapi/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/objects/logapi/test_logging_resource.py b/neutron/tests/unit/objects/logapi/test_logging_resource.py new file mode 100644 index 00000000000..3a2c7b93b2a --- /dev/null +++ b/neutron/tests/unit/objects/logapi/test_logging_resource.py @@ -0,0 +1,86 @@ +# 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_utils import uuidutils + +from neutron.objects.logapi import logging_resource as log_res +from neutron.objects import securitygroup +from neutron.tests.unit.objects import test_base +from neutron.tests.unit import testlib_api + + +class LogObjectTestCase(test_base.BaseObjectIfaceTestCase): + + _test_class = log_res.Log + + +class LogDBObjectTestCase(test_base.BaseDbObjectTestCase, + testlib_api.SqlTestCase): + + _test_class = log_res.Log + + def setUp(self): + super(LogDBObjectTestCase, self).setUp() + self._network_id = self._create_test_network_id() + self._port_id = self._create_test_port_id(network_id=self._network_id) + self._security_group = self._create_test_security_group() + self.update_obj_fields({'resource_id': self._security_group['id'], + 'target_id': self._port_id}) + + def _create_test_security_group(self): + sg_fields = self.get_random_object_fields(securitygroup.SecurityGroup) + sg_obj = securitygroup.SecurityGroup(self.context, **sg_fields) + return sg_obj + + def test_create_sg_log_with_secgroup(self): + sg = self._create_test_security_group() + sg_log = log_res.Log(context=self.context, + id=uuidutils.generate_uuid(), + name='test-create', + resource_type='security_group', + resource_id=sg.id, + enabled=False) + sg_log.create() + self.assertEqual(sg.id, sg_log.resource_id) + + def test_create_sg_log_with_port(self): + port_id = self._create_test_port_id(network_id=self._network_id) + sg_log = log_res.Log(context=self.context, + id=uuidutils.generate_uuid(), + name='test-create', + resource_type='security_group', + target_id=port_id, + enabled=False) + sg_log.create() + self.assertEqual(port_id, sg_log.target_id) + + def _test_update_multiple_fields(self): + sg_log = log_res.Log(context=self.context, + id=uuidutils.generate_uuid(), + name='test-create', + description='test-description', + resource_type='security_group', + enabled=False) + sg_log.create() + fields = {'name': 'test-update', 'description': 'test-update-descr', + 'enabled': True} + sg_log.update_fields(**fields) + sg_log.update() + + new_sg_log = log_res.Log.get_object(self.context, id=sg_log.id) + self._assert_attrs(new_sg_log, **fields) + + def _assert_attrs(self, sg_log, **kwargs): + """Check the values passed in kwargs match the values of the sg log""" + for k in sg_log.fields: + if k in kwargs: + self.assertEqual(kwargs[k], sg_log[k]) diff --git a/neutron/tests/unit/objects/test_base.py b/neutron/tests/unit/objects/test_base.py index b2eb71664c6..d55b34ea3b2 100644 --- a/neutron/tests/unit/objects/test_base.py +++ b/neutron/tests/unit/objects/test_base.py @@ -40,6 +40,7 @@ from neutron.objects import common_types from neutron.objects.db import api as obj_db_api from neutron.objects import exceptions as o_exc from neutron.objects import flavor +from neutron.objects.logapi import event_types from neutron.objects import network as net_obj from neutron.objects import ports from neutron.objects import rbac_db @@ -445,6 +446,7 @@ FIELD_TYPE_VALUE_GENERATOR_MAP = { common_types.SetOfUUIDsField: get_set_of_random_uuids, common_types.UUIDField: uuidutils.generate_uuid, common_types.VlanIdRangeField: tools.get_random_vlan, + event_types.SecurityEventField: tools.get_random_security_event, obj_fields.BooleanField: tools.get_random_boolean, obj_fields.DateTimeField: tools.get_random_datetime, obj_fields.DictOfStringsField: get_random_dict_of_strings, diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index 17a19294e4f..1a407679ebb 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -50,6 +50,7 @@ object_data = { 'IpamSubnet': '1.0-713de401682a70f34891e13af645fa08', 'MeteringLabel': '1.0-cc4b620a3425222447cbe459f62de533', 'MeteringLabelRule': '1.0-b5c5717e7bab8d1af1623156012a5842', + 'Log': '1.0-6391351c0f34ed34375a19202f361d24', 'Network': '1.0-f2f6308f79731a767b92b26b0f4f3849', 'NetworkDNSDomain': '1.0-420db7910294608534c1e2e30d6d8319', 'NetworkPortSecurity': '1.0-b30802391a87945ee9c07582b4ff95e3',