From 4c2c983618ddb7a528c9005b0d7aaf5322bd198d Mon Sep 17 00:00:00 2001 From: ZhaoBo Date: Thu, 18 Feb 2016 13:28:58 +0800 Subject: [PATCH] Add timestamp for neutron core resources Currently, neutron core resources (like net, subnet, port and subnetpool) do not save time-stamps upon their creation and updation. This information can be critical for debugging purposes. This patch introduces a new extension called "timestamp" extending existing the neutron core resources to allow their creation and modification times to be record. Now this patch add this resource schema and the functions which listen db events to add timestamp fields. APIImpact DocImpact: Neutron core resources now contain 'timestamp' fields like 'created_at' and 'updated_at' Change-Id: I24114b464403435d9c1e1e123d2bc2f37c8fc6ea Partially-Implements: blueprint add-port-timestamp --- .../alembic_migrations/versions/EXPAND_HEAD | 2 +- ...ccad37f_add_timestamp_to_base_resources.py | 37 ++++ neutron/db/model_base.py | 2 +- neutron/extensions/timestamp_core.py | 65 +++++++ neutron/plugins/common/constants.py | 1 + neutron/services/timestamp/__init__.py | 0 neutron/services/timestamp/timestamp_db.py | 75 ++++++++ .../services/timestamp/timestamp_plugin.py | 39 ++++ neutron/tests/api/test_timestamp.py | 178 ++++++++++++++++++ ...add-timestamp-fields-f9ab949fc88f05f6.yaml | 6 + setup.cfg | 1 + 11 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/mitaka/expand/3894bccad37f_add_timestamp_to_base_resources.py create mode 100644 neutron/extensions/timestamp_core.py create mode 100644 neutron/services/timestamp/__init__.py create mode 100644 neutron/services/timestamp/timestamp_db.py create mode 100644 neutron/services/timestamp/timestamp_plugin.py create mode 100644 neutron/tests/api/test_timestamp.py create mode 100644 releasenotes/notes/add-timestamp-fields-f9ab949fc88f05f6.yaml diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index d7322a8c116..542e85d00a7 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -2f9e956e7532 +3894bccad37f diff --git a/neutron/db/migration/alembic_migrations/versions/mitaka/expand/3894bccad37f_add_timestamp_to_base_resources.py b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/3894bccad37f_add_timestamp_to_base_resources.py new file mode 100644 index 00000000000..ca52ba03f5f --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/mitaka/expand/3894bccad37f_add_timestamp_to_base_resources.py @@ -0,0 +1,37 @@ +# Copyright 2015 HuaWei Technologies. +# +# 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. +# + +"""add_timestamp_to_base_resources + +Revision ID: 3894bccad37f +Revises: 2f9e956e7532 +Create Date: 2016-03-01 04:19:58.852612 + +""" + +# revision identifiers, used by Alembic. +revision = '3894bccad37f' +down_revision = '2f9e956e7532' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + for column_name in ['created_at', 'updated_at']: + op.add_column( + 'standardattributes', + sa.Column(column_name, sa.DateTime(), nullable=True) + ) diff --git a/neutron/db/model_base.py b/neutron/db/model_base.py index c5a4f04576b..0db04fb7a99 100644 --- a/neutron/db/model_base.py +++ b/neutron/db/model_base.py @@ -79,7 +79,7 @@ class NeutronBaseV2(NeutronBase): BASEV2 = declarative.declarative_base(cls=NeutronBaseV2) -class StandardAttribute(BASEV2): +class StandardAttribute(BASEV2, models.TimestampMixin): """Common table to associate all Neutron API resources. By having Neutron objects related to this table, we can associate new diff --git a/neutron/extensions/timestamp_core.py b/neutron/extensions/timestamp_core.py new file mode 100644 index 00000000000..d1589cf6400 --- /dev/null +++ b/neutron/extensions/timestamp_core.py @@ -0,0 +1,65 @@ +# Copyright 2015 HuaWei Technologies. +# +# 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.api import extensions + +# Attribute Map +CREATED = 'created_at' +UPDATED = 'updated_at' +TIMESTAMP_BODY = { + CREATED: {'allow_post': False, 'allow_put': False, + 'is_visible': True, 'default': None + }, + UPDATED: {'allow_post': False, 'allow_put': False, + 'is_visible': True, 'default': None + }, +} +EXTENDED_ATTRIBUTES_2_0 = { + 'networks': TIMESTAMP_BODY, + 'subnets': TIMESTAMP_BODY, + 'ports': TIMESTAMP_BODY, + 'subnetpools': TIMESTAMP_BODY, +} + + +class Timestamp_core(extensions.ExtensionDescriptor): + """Extension class supporting timestamp. + + This class is used by neutron's extension framework for adding timestamp + to neutron core resources. + """ + + @classmethod + def get_name(cls): + return "Time Stamp Fields addition for core resources" + + @classmethod + def get_alias(cls): + return "timestamp_core" + + @classmethod + def get_description(cls): + return ("This extension can be used for recording " + "create/update timestamps for core resources " + "like port/subnet/network/subnetpools.") + + @classmethod + def get_updated(cls): + return "2016-03-01T10:00:00-00:00" + + def get_extended_resources(self, version): + if version == "2.0": + return EXTENDED_ATTRIBUTES_2_0 + else: + return {} diff --git a/neutron/plugins/common/constants.py b/neutron/plugins/common/constants.py index 270263602c3..ad717cbda88 100644 --- a/neutron/plugins/common/constants.py +++ b/neutron/plugins/common/constants.py @@ -42,6 +42,7 @@ EXT_TO_SERVICE_MAPPING = { DEFAULT_SERVICE_PLUGINS = { 'auto_allocate': 'auto-allocated-topology', 'tag': 'tag', + 'timestamp_core': 'timestamp_core', } # Service operation status constants diff --git a/neutron/services/timestamp/__init__.py b/neutron/services/timestamp/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/services/timestamp/timestamp_db.py b/neutron/services/timestamp/timestamp_db.py new file mode 100644 index 00000000000..b573283e1d3 --- /dev/null +++ b/neutron/services/timestamp/timestamp_db.py @@ -0,0 +1,75 @@ +# Copyright 2015 HuaWei Technologies. +# +# 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 +from oslo_utils import timeutils +from sqlalchemy import event +from sqlalchemy import exc as sql_exc +from sqlalchemy.orm import session as se + +from neutron._i18n import _LW +from neutron.db import model_base + +LOG = log.getLogger(__name__) + + +class TimeStamp_db_mixin(object): + """Mixin class to add Time Stamp methods.""" + + ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' + + def update_timestamp(self, session, context, instances): + objs_list = session.new.union(session.dirty) + + while objs_list: + obj = objs_list.pop() + if hasattr(obj, 'standard_attr') and obj.standard_attr_id: + obj.standard_attr.updated_at = timeutils.utcnow() + + def register_db_events(self): + event.listen(model_base.StandardAttribute, 'before_insert', + self._add_timestamp) + event.listen(se.Session, 'before_flush', self.update_timestamp) + + def unregister_db_events(self): + self._unregister_db_event(model_base.StandardAttribute, + 'before_insert', self._add_timestamp) + self._unregister_db_event(se.Session, 'before_flush', + self.update_timestamp) + + def _unregister_db_event(self, listen_obj, listened_event, listen_hander): + try: + event.remove(listen_obj, listened_event, listen_hander) + except sql_exc.InvalidRequestError: + LOG.warning(_LW("No sqlalchemy event for resource %s found"), + listen_obj) + + def _format_timestamp(self, resource_db, result): + result['created_at'] = (resource_db.standard_attr.created_at. + strftime(self.ISO8601_TIME_FORMAT)) + result['updated_at'] = (resource_db.standard_attr.updated_at. + strftime(self.ISO8601_TIME_FORMAT)) + + def extend_resource_dict_timestamp(self, plugin_obj, + resource_res, resource_db): + if (resource_db and resource_db.standard_attr.created_at and + resource_db.standard_attr.updated_at): + self._format_timestamp(resource_db, resource_res) + + def _add_timestamp(self, mapper, _conn, target): + if not target.created_at and not target.updated_at: + time = timeutils.utcnow() + for field in ['created_at', 'updated_at']: + setattr(target, field, time) + return target diff --git a/neutron/services/timestamp/timestamp_plugin.py b/neutron/services/timestamp/timestamp_plugin.py new file mode 100644 index 00000000000..3134cfa0228 --- /dev/null +++ b/neutron/services/timestamp/timestamp_plugin.py @@ -0,0 +1,39 @@ +# Copyright 2015 HuaWei Technologies. +# +# 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.api.v2 import attributes +from neutron.db import db_base_plugin_v2 +from neutron.services import service_base +from neutron.services.timestamp import timestamp_db as ts_db + + +class TimeStampPlugin(service_base.ServicePluginBase, + ts_db.TimeStamp_db_mixin): + """Implements Neutron Timestamp Service plugin.""" + + supported_extension_aliases = ['timestamp_core'] + + def __init__(self): + super(TimeStampPlugin, self).__init__() + self.register_db_events() + for resources in [attributes.NETWORKS, attributes.PORTS, + attributes.SUBNETS, attributes.SUBNETPOOLS]: + db_base_plugin_v2.NeutronDbPluginV2.register_dict_extend_funcs( + resources, [self.extend_resource_dict_timestamp]) + + def get_plugin_type(self): + return 'timestamp_core' + + def get_plugin_description(self): + return "Neutron core resources timestamp addition support" diff --git a/neutron/tests/api/test_timestamp.py b/neutron/tests/api/test_timestamp.py new file mode 100644 index 00000000000..4082ededb49 --- /dev/null +++ b/neutron/tests/api/test_timestamp.py @@ -0,0 +1,178 @@ +# 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 copy + +from tempest.lib.common.utils import data_utils +from tempest import test + +from neutron.tests.api import base + + +class TestTimeStamp(base.BaseAdminNetworkTest): + + ## attributes for subnetpool + min_prefixlen = '28' + max_prefixlen = '31' + _ip_version = 4 + subnet_cidr = '10.11.12.0/31' + new_prefix = '10.11.15.0/24' + larger_prefix = '10.11.0.0/16' + + @classmethod + def skip_checks(cls): + super(TestTimeStamp, cls).skip_checks() + + if not test.is_extension_enabled('timestamp_core', 'network'): + raise cls.skipException("timestamp_core extension not enabled") + + @classmethod + def resource_setup(cls): + super(TestTimeStamp, cls).resource_setup() + prefixes = ['10.11.12.0/24'] + cls._subnetpool_data = {'min_prefixlen': '29', 'prefixes': prefixes} + + def _create_subnetpool(self, is_admin=False, **kwargs): + name = data_utils.rand_name('subnetpool-') + subnetpool_data = copy.deepcopy(self._subnetpool_data) + for key in subnetpool_data.keys(): + kwargs[key] = subnetpool_data[key] + return self.create_subnetpool(name=name, is_admin=is_admin, **kwargs) + + @test.idempotent_id('462be770-b310-4df9-9c42-773217e4c8b1') + def test_create_network_with_timestamp(self): + network = self.create_network() + # Verifies body contains timestamp fields + self.assertIsNotNone(network['created_at']) + self.assertIsNotNone(network['updated_at']) + + @test.idempotent_id('4db5417a-e11c-474d-a361-af00ebef57c5') + def test_update_network_with_timestamp(self): + network = self.create_network() + origin_updated_at = network['updated_at'] + update_body = {'name': network['name'] + 'new'} + body = self.admin_client.update_network(network['id'], + **update_body) + updated_network = body['network'] + new_updated_at = updated_network['updated_at'] + self.assertEqual(network['created_at'], updated_network['created_at']) + # Verify that origin_updated_at is not same with new_updated_at + self.assertIsNot(origin_updated_at, new_updated_at) + + @test.idempotent_id('2ac50ab2-7ebd-4e27-b3ce-a9e399faaea2') + def test_show_networks_attribute_with_timestamp(self): + network = self.create_network() + body = self.client.show_network(network['id']) + show_net = body['network'] + # verify the timestamp from creation and showed is same + self.assertEqual(network['created_at'], + show_net['created_at']) + self.assertEqual(network['updated_at'], + show_net['updated_at']) + + @test.idempotent_id('8ee55186-454f-4b97-9f9f-eb2772ee891c') + def test_create_subnet_with_timestamp(self): + network = self.create_network() + subnet = self.create_subnet(network) + # Verifies body contains timestamp fields + self.assertIsNotNone(subnet['created_at']) + self.assertIsNotNone(subnet['updated_at']) + + @test.idempotent_id('a490215a-6f4c-4af9-9a4c-57c41f1c4fa1') + def test_update_subnet_with_timestamp(self): + network = self.create_network() + subnet = self.create_subnet(network) + origin_updated_at = subnet['updated_at'] + update_body = {'name': subnet['name'] + 'new'} + body = self.admin_client.update_subnet(subnet['id'], + **update_body) + updated_subnet = body['subnet'] + new_updated_at = updated_subnet['updated_at'] + self.assertEqual(subnet['created_at'], updated_subnet['created_at']) + # Verify that origin_updated_at is not same with new_updated_at + self.assertIsNot(origin_updated_at, new_updated_at) + + @test.idempotent_id('1836a086-e7cf-4141-bf57-0cfe79e8051e') + def test_show_subnet_attribute_with_timestamp(self): + network = self.create_network() + subnet = self.create_subnet(network) + body = self.client.show_subnet(subnet['id']) + show_subnet = body['subnet'] + # verify the timestamp from creation and showed is same + self.assertEqual(subnet['created_at'], + show_subnet['created_at']) + self.assertEqual(subnet['updated_at'], + show_subnet['updated_at']) + + @test.idempotent_id('e2450a7b-d84f-4600-a093-45e78597bbac') + def test_create_port_with_timestamp(self): + network = self.create_network() + port = self.create_port(network) + # Verifies body contains timestamp fields + self.assertIsNotNone(port['created_at']) + self.assertIsNotNone(port['updated_at']) + + @test.idempotent_id('4241e0d3-54b4-46ce-a9a7-093fc764161b') + def test_update_port_with_timestamp(self): + network = self.create_network() + port = self.create_port(network) + origin_updated_at = port['updated_at'] + update_body = {'name': port['name'] + 'new'} + body = self.admin_client.update_port(port['id'], + **update_body) + updated_port = body['port'] + new_updated_at = updated_port['updated_at'] + self.assertEqual(port['created_at'], updated_port['created_at']) + # Verify that origin_updated_at is not same with new_updated_at + self.assertIsNot(origin_updated_at, new_updated_at) + + @test.idempotent_id('584c6723-40b6-4f26-81dd-f508f9d9fb51') + def test_show_port_attribute_with_timestamp(self): + network = self.create_network() + port = self.create_port(network) + body = self.client.show_port(port['id']) + show_port = body['port'] + # verify the timestamp from creation and showed is same + self.assertEqual(port['created_at'], + show_port['created_at']) + self.assertEqual(port['updated_at'], + show_port['updated_at']) + + @test.idempotent_id('87a8b196-4b90-44f0-b7f3-d2057d7d658e') + def test_create_subnetpool_with_timestamp(self): + sp = self._create_subnetpool() + # Verifies body contains timestamp fields + self.assertIsNotNone(sp['created_at']) + self.assertIsNotNone(sp['updated_at']) + + @test.idempotent_id('d48c7578-c3d2-4f9b-a7a1-be2008c770a0') + def test_update_subnetpool_with_timestamp(self): + sp = self._create_subnetpool() + origin_updated_at = sp['updated_at'] + update_body = {'name': sp['name'] + 'new', + 'min_prefixlen': self.min_prefixlen, + 'max_prefixlen': self.max_prefixlen} + body = self.client.update_subnetpool(sp['id'], **update_body) + updated_sp = body['subnetpool'] + new_updated_at = updated_sp['updated_at'] + self.assertEqual(sp['created_at'], updated_sp['created_at']) + # Verify that origin_updated_at is not same with new_updated_at + self.assertIsNot(origin_updated_at, new_updated_at) + + @test.idempotent_id('1d3970e6-bcf7-46cd-b7d7-0807759c73b4') + def test_show_subnetpool_attribute_with_timestamp(self): + sp = self._create_subnetpool() + body = self.client.show_subnetpool(sp['id']) + show_sp = body['subnetpool'] + # verify the timestamp from creation and showed is same + self.assertEqual(sp['created_at'], show_sp['created_at']) + self.assertEqual(sp['updated_at'], show_sp['updated_at']) diff --git a/releasenotes/notes/add-timestamp-fields-f9ab949fc88f05f6.yaml b/releasenotes/notes/add-timestamp-fields-f9ab949fc88f05f6.yaml new file mode 100644 index 00000000000..833210fa87d --- /dev/null +++ b/releasenotes/notes/add-timestamp-fields-f9ab949fc88f05f6.yaml @@ -0,0 +1,6 @@ +--- +prelude: > + Timestamp fields are now added to neutron core resources. +features: + - Add timestamp fields 'created_at', 'updated_at' into neutron core + resources like network, subnet, port and subnetpool. diff --git a/setup.cfg b/setup.cfg index 7ff30f8c97b..e6ff2a1059f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,6 +82,7 @@ neutron.service_plugins = flavors = neutron.services.flavors.flavors_plugin:FlavorsPlugin auto_allocate = neutron.services.auto_allocate.plugin:Plugin network_ip_availability = neutron.services.network_ip_availability.plugin:NetworkIPAvailabilityPlugin + timestamp_core = neutron.services.timestamp.timestamp_plugin:TimeStampPlugin neutron.qos.notification_drivers = message_queue = neutron.services.qos.notification_drivers.message_queue:RpcQosServiceNotificationDriver neutron.ml2.type_drivers =