diff --git a/setup.cfg b/setup.cfg index 510e7be6d..d228de064 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,7 @@ tacker.service_plugins = dummy = tacker.tests.unit.dummy_plugin:DummyServicePlugin vnfm = tacker.vm.plugin:VNFMPlugin nfvo = tacker.nfvo.nfvo_plugin:NfvoPlugin + commonservices = tacker.plugins.common_services.common_services_plugin:CommonServicesPlugin tacker.nfvo.vim.drivers = openstack = tacker.nfvo.drivers.vim.openstack_driver:OpenStack_Driver tacker.openstack.common.cache.backends = diff --git a/tacker/common/config.py b/tacker/common/config.py index 3229ff40f..dd8937d8f 100644 --- a/tacker/common/config.py +++ b/tacker/common/config.py @@ -40,7 +40,7 @@ core_opts = [ help=_("The API paste config file to use")), cfg.StrOpt('api_extensions_path', default="", help=_("The path for API extensions")), - cfg.ListOpt('service_plugins', default=['nfvo', 'vnfm'], + cfg.ListOpt('service_plugins', default=['nfvo', 'vnfm', 'commonservices'], help=_("The service plugins Tacker will use")), cfg.StrOpt('policy_file', default="policy.json", help=_("The policy file to use")), diff --git a/tacker/db/common_services/__init__.py b/tacker/db/common_services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/db/common_services/common_services_db.py b/tacker/db/common_services/common_services_db.py new file mode 100644 index 000000000..3e626994c --- /dev/null +++ b/tacker/db/common_services/common_services_db.py @@ -0,0 +1,99 @@ +# Copyright 2016 Brocade Communications System, Inc. +# 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 sqlalchemy as sa +from sqlalchemy.orm import exc as orm_exc + +from oslo_log import log as logging + +from tacker.common import log +from tacker.db import db_base +from tacker.db import model_base +from tacker.db import types +from tacker.extensions import common_services +from tacker import manager + + +LOG = logging.getLogger(__name__) + +EVENT_ATTRIBUTES = ('id', 'resource_id', 'resource_type', 'resource_state', + 'timestamp', 'event_type', 'event_details') + + +class Event(model_base.BASE): + id = sa.Column(sa.Integer, primary_key=True, nullable=False, + autoincrement=True) + resource_id = sa.Column(types.Uuid, nullable=False) + resource_state = sa.Column(sa.String(64), nullable=False) + resource_type = sa.Column(sa.String(64), nullable=False) + timestamp = sa.Column(sa.DateTime, nullable=False) + event_type = sa.Column(sa.String(64), nullable=False) + event_details = sa.Column(types.Json) + + +class CommonServicesPluginDb(common_services.CommonServicesPluginBase, + db_base.CommonDbMixin): + + def __init__(self): + super(CommonServicesPluginDb, self).__init__() + + @property + def _core_plugin(self): + return manager.TackerManager.get_plugin() + + def _make_event_dict(self, event_db, fields=None): + res = dict((key, event_db[key]) for key in EVENT_ATTRIBUTES) + return self._fields(res, fields) + + def _fields(self, resource, fields): + if fields: + return dict(((key, item) for key, item in resource.items() + if key in fields)) + return resource + + @log.log + def create_event(self, context, res_id, res_type, res_state, evt_type, + tstamp, details=""): + try: + with context.session.begin(subtransactions=True): + event_db = Event( + resource_id=res_id, + resource_type=res_type, + resource_state=res_state, + event_details=details, + event_type=evt_type, + timestamp=tstamp) + context.session.add(event_db) + except Exception as e: + LOG.exception(_("create event error: %s"), str(e)) + raise common_services.EventCreationFailureException( + error_str=str(e)) + return self._make_event_dict(event_db) + + @log.log + def get_event(self, context, event_id, fields=None): + try: + events_db = self._get_by_id(context, Event, event_id) + except orm_exc.NoResultFound: + raise common_services.EventNotFoundException(evt_id=event_id) + return self._make_event_dict(events_db, fields) + + @log.log + def get_events(self, context, filters=None, fields=None, sorts=None, + limit=None, marker_obj=None, page_reverse=False): + return self._get_collection(context, Event, self._make_event_dict, + filters, fields, sorts, limit, + marker_obj, page_reverse) diff --git a/tacker/db/migration/alembic_migrations/versions/4ee19c8a6d0a_audit_support_events.py b/tacker/db/migration/alembic_migrations/versions/4ee19c8a6d0a_audit_support_events.py new file mode 100644 index 000000000..9f1286a84 --- /dev/null +++ b/tacker/db/migration/alembic_migrations/versions/4ee19c8a6d0a_audit_support_events.py @@ -0,0 +1,45 @@ +# Copyright 2016 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. +# + +"""audit_support_events + +Revision ID: 4ee19c8a6d0a +Revises: acf941e54075 +Create Date: 2016-06-07 03:16:53.513392 + +""" + +# revision identifiers, used by Alembic. +revision = '4ee19c8a6d0a' +down_revision = '941b5a6fff9e' + +from alembic import op +import sqlalchemy as sa + +from tacker.db import types + + +def upgrade(active_plugins=None, options=None): + op.create_table('events', + sa.Column('id', sa.Integer, nullable=False, autoincrement=True), + sa.Column('resource_id', types.Uuid, nullable=False), + sa.Column('resource_state', sa.String(64), nullable=False), + sa.Column('resource_type', sa.String(64), nullable=False), + sa.Column('event_type', sa.String(64), nullable=False), + sa.Column('timestamp', sa.DateTime, nullable=False), + sa.Column('event_details', types.Json), + sa.PrimaryKeyConstraint('id'), + mysql_engine='InnoDB' + ) diff --git a/tacker/db/migration/alembic_migrations/versions/HEAD b/tacker/db/migration/alembic_migrations/versions/HEAD index d81214c3b..021b3bd5b 100644 --- a/tacker/db/migration/alembic_migrations/versions/HEAD +++ b/tacker/db/migration/alembic_migrations/versions/HEAD @@ -1,2 +1 @@ -941b5a6fff9e - +4ee19c8a6d0a \ No newline at end of file diff --git a/tacker/extensions/common_services.py b/tacker/extensions/common_services.py new file mode 100644 index 000000000..46ee247cc --- /dev/null +++ b/tacker/extensions/common_services.py @@ -0,0 +1,145 @@ +# Copyright 2016 Brocade Communications Systems Inc +# 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 six + +from tacker.api import extensions +from tacker.api.v1 import attributes as attr +from tacker.api.v1 import resource_helper +from tacker.common import exceptions +from tacker.plugins.common import constants +from tacker.services import service_base + + +class EventCreationFailureException(exceptions.TackerException): + message = _("Failed to create an event: %(error_str)s") + + +class EventNotFoundException(exceptions.TackerException): + message = _("Specified Event id %(evt_id)s is invalid. Please verify and " + "pass a valid Event id") + + +class InvalidModelException(exceptions.TackerException): + message = _("Specified model is invalid, only Event model supported") + + +RESOURCE_ATTRIBUTE_MAP = { + + 'events': { + 'id': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'resource_id': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True + }, + 'resource_type': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True + }, + 'resource_state': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True + }, + 'timestamp': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'event_details': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + 'event_type': { + 'allow_post': False, + 'allow_put': False, + 'is_visible': True, + }, + } +} + + +class Common_services(extensions.ExtensionDescriptor): + @classmethod + def get_name(cls): + return 'COMMONSERVICES' + + @classmethod + def get_alias(cls): + return 'Commonservices' + + @classmethod + def get_description(cls): + return "Extension for CommonServices" + + @classmethod + def get_namespace(cls): + return 'http://wiki.openstack.org/Tacker' + + @classmethod + def get_updated(cls): + return "2016-06-06T13:00:00-00:00" + + @classmethod + def get_resources(cls): + special_mappings = {} + plural_mappings = resource_helper.build_plural_mappings( + special_mappings, RESOURCE_ATTRIBUTE_MAP) + attr.PLURALS.update(plural_mappings) + return resource_helper.build_resource_info( + plural_mappings, RESOURCE_ATTRIBUTE_MAP, constants.COMMONSERVICES, + translate_name=True) + + @classmethod + def get_plugin_interface(cls): + return CommonServicesPluginBase + + def update_attributes_map(self, attributes): + super(Common_services, self).update_attributes_map( + attributes, extension_attrs_map=RESOURCE_ATTRIBUTE_MAP) + + def get_extended_resources(self, version): + version_map = {'1.0': RESOURCE_ATTRIBUTE_MAP} + return version_map.get(version, {}) + + +@six.add_metaclass(abc.ABCMeta) +class CommonServicesPluginBase(service_base.NFVPluginBase): + def get_plugin_name(self): + return constants.COMMONSERVICES + + def get_plugin_type(self): + return constants.COMMONSERVICES + + def get_plugin_description(self): + return 'Tacker CommonServices plugin' + + @abc.abstractmethod + def get_event(self, context, event_id, fields=None): + pass + + @abc.abstractmethod + def get_events(self, context, filters=None, fields=None, sorts=None, + limit=None, marker_obj=None, page_reverse=False): + pass diff --git a/tacker/plugins/common/constants.py b/tacker/plugins/common/constants.py index c939bd496..35b3fde00 100644 --- a/tacker/plugins/common/constants.py +++ b/tacker/plugins/common/constants.py @@ -18,12 +18,14 @@ CORE = "CORE" DUMMY = "DUMMY" VNFM = "VNFM" NFVO = "NFVO" +COMMONSERVICES = "COMMONSERVICES" COMMON_PREFIXES = { CORE: "", DUMMY: "/dummy_svc", VNFM: "", - NFVO: "" + NFVO: "", + COMMONSERVICES: "" } # Service operation status constants diff --git a/tacker/plugins/common_services/__init__.py b/tacker/plugins/common_services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tacker/plugins/common_services/common_services_plugin.py b/tacker/plugins/common_services/common_services_plugin.py new file mode 100644 index 000000000..58789288c --- /dev/null +++ b/tacker/plugins/common_services/common_services_plugin.py @@ -0,0 +1,48 @@ +# Copyright 2016 Brocade Communications System, Inc. +# 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 tacker.common import log +from tacker.db.common_services import common_services_db + +LOG = logging.getLogger(__name__) + + +class CommonServicesPlugin(common_services_db.CommonServicesPluginDb): + """Reference plugin for COMMONSERVICES extension + + Implements the COMMONSERVICES extension and defines public facing APIs for + common utility operations. + """ + + supported_extension_aliases = ['CommonServices'] + + def __init__(self): + super(CommonServicesPlugin, self).__init__() + + @log.log + def get_event(self, context, event_id, fields=None): + return super(CommonServicesPlugin, self).get_event(context, event_id, + fields) + + @log.log + def get_events(self, context, filters=None, fields=None, sorts=None, + limit=None, marker_obj=None, page_reverse=False): + return super(CommonServicesPlugin, self).get_events(context, filters, + fields, sorts, limit, + marker_obj, + page_reverse) diff --git a/tacker/tests/unit/test_common_services_plugin.py b/tacker/tests/unit/test_common_services_plugin.py new file mode 100644 index 000000000..3081a15cb --- /dev/null +++ b/tacker/tests/unit/test_common_services_plugin.py @@ -0,0 +1,159 @@ +# Copyright 2016 Brocade Communications System, Inc. +# 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 oslo_utils import timeutils + +from tacker import context +from tacker.db.common_services import common_services_db +from tacker.extensions import common_services +from tacker.plugins.common_services import common_services_plugin +from tacker.tests.unit.db import base as db_base + + +class TestCommonServicesPlugin(db_base.SqlTestCase): + def setUp(self): + super(TestCommonServicesPlugin, self).setUp() + self.addCleanup(mock.patch.stopall) + self.context = context.get_admin_context() + self.event_db_plugin = common_services_db.CommonServicesPluginDb() + self.coreutil_plugin = common_services_plugin.CommonServicesPlugin() + + def _get_dummy_event_obj(self): + return { + 'resource_id': '6261579e-d6f3-49ad-8bc3-a9cb974778ff', + 'resource_state': 'ACTIVE', + 'resource_type': 'VNF', + 'event_details': '', + 'event_type': 'scale_up', + 'timestamp': timeutils.parse_strtime('2016-07-20T05:43:52.765172') + } + + def test_create_event(self): + evt_obj = self._get_dummy_event_obj() + result = self.event_db_plugin.create_event(self.context, + evt_obj['resource_id'], + evt_obj['resource_type'], + evt_obj['resource_state'], + evt_obj['event_type'], + evt_obj['timestamp'], + evt_obj['event_details']) + self.assertIsNotNone(result) + self.assertIn('id', result) + self.assertIn('resource_id', result) + self.assertIn('resource_state', result) + self.assertIn('resource_type', result) + self.assertIn('event_type', result) + self.assertIn('event_details', result) + self.assertIn('timestamp', result) + + def test_event_not_found(self): + self.assertRaises(common_services.EventNotFoundException, + self.coreutil_plugin.get_event, self.context, '99') + + def test_InvalidModelInputExceptionNotThrown(self): + evt_obj = self._get_dummy_event_obj() + result = self.event_db_plugin.create_event(self.context, + evt_obj['resource_id'], + evt_obj['resource_type'], + evt_obj['resource_state'], + evt_obj['event_type'], + evt_obj['timestamp'], + evt_obj['event_details']) + try: + self.coreutil_plugin.get_event(self, context, str(result['id'])) + except common_services.InvalidModelException: + self.assertTrue(False) + except Exception: + self.assertTrue(True) + + def test_get_event_by_id(self): + evt_obj = self._get_dummy_event_obj() + evt_created = self.event_db_plugin.create_event( + self.context, evt_obj['resource_id'], + evt_obj['resource_type'], + evt_obj['resource_state'], + evt_obj['event_type'], + evt_obj['timestamp'], + evt_obj['event_details']) + self.assertIsNotNone(evt_created) + evt_get = self.coreutil_plugin.get_event(self.context, + evt_created['id']) + self.assertEqual(evt_created['resource_id'], evt_get['resource_id']) + self.assertEqual(evt_created['resource_state'], + evt_get['resource_state']) + self.assertEqual(evt_created['resource_type'], + evt_get['resource_type']) + self.assertEqual(evt_created['event_type'], evt_get['event_type']) + self.assertEqual(evt_created['event_details'], + evt_get['event_details']) + self.assertEqual(evt_created['timestamp'], evt_get['timestamp']) + + def test_get_events(self): + evt_obj = self._get_dummy_event_obj() + self.event_db_plugin.create_event(self.context, + evt_obj['resource_id'], + evt_obj['resource_type'], + evt_obj['resource_state'], + evt_obj['event_type'], + evt_obj['timestamp'], + evt_obj['event_details']) + result = self.coreutil_plugin.get_events(self.context) + self.assertTrue(len(result)) + + def test_get_events_filtered_invalid_id(self): + evt_obj = self._get_dummy_event_obj() + self.event_db_plugin.create_event(self.context, + evt_obj['resource_id'], + evt_obj['resource_type'], + evt_obj['resource_state'], + evt_obj['event_type'], + evt_obj['timestamp'], + evt_obj['event_details']) + result = self.coreutil_plugin.get_events(self.context, {'id': 'xyz'}) + self.assertFalse(len(result)) + + def test_get_events_filtered_valid_id(self): + evt_obj = self._get_dummy_event_obj() + self.event_db_plugin.create_event(self.context, + evt_obj['resource_id'], + evt_obj['resource_type'], + evt_obj['resource_state'], + evt_obj['event_type'], + evt_obj['timestamp'], + evt_obj['event_details']) + result = self.coreutil_plugin.get_events(self.context, {'id': '1'}) + self.assertTrue(len(result)) + + def test_get_events_valid_fields(self): + evt_obj = self._get_dummy_event_obj() + self.event_db_plugin.create_event(self.context, + evt_obj['resource_id'], + evt_obj['resource_type'], + evt_obj['resource_state'], + evt_obj['event_type'], + evt_obj['timestamp'], + evt_obj['event_details']) + result = self.coreutil_plugin.get_events(self.context, {'id': '1'}, + ['id', 'event_type']) + self.assertTrue(len(result)) + self.assertIn('id', result[0]) + self.assertNotIn('resource_id', result[0]) + self.assertNotIn('resource_state', result[0]) + self.assertNotIn('resource_type', result[0]) + self.assertIn('event_type', result[0]) + self.assertNotIn('event_details', result[0]) + self.assertNotIn('timestamp', result[0])