From 6c304dfab28ffd1bbe69b9ada3d11e8fbbde014b Mon Sep 17 00:00:00 2001 From: "Zhang Rong(Jon)" Date: Tue, 28 Dec 2021 10:43:54 +0800 Subject: [PATCH] Move registration API to configuration 1. Create new domain file for configuration, keep registration command object in configuration domain file 2. Update API and test case, "/provision/v1" as base URL, call "smo-endpoint" to create a new endpoint Issue-ID: INF-250 Change-Id: Id85ad6c28a2fd1c6da065c0846c172bfc7ac4f6b Signed-off-by: Zhang Rong(Jon) --- docs/installation-guide.rst | 5 +- o2app/adapter/unit_of_work.py | 6 +- o2app/entrypoints/flask_application.py | 4 +- o2app/entrypoints/redis_eventconsumer.py | 9 +- o2app/service/handlers.py | 6 +- o2common/config/config.py | 4 + o2ims/adapter/ocloud_repository.py | 32 ++--- o2ims/adapter/orm.py | 10 +- o2ims/domain/configuration_obj.py | 52 ++++++++ o2ims/domain/configuration_repo.py | 57 ++++++++ o2ims/domain/events.py | 2 +- o2ims/domain/subscription_obj.py | 18 --- o2ims/domain/subscription_repo.py | 40 ------ o2ims/service/command/registration_handler.py | 34 ++--- ...egistration_event.py => configuration_event.py} | 6 +- o2ims/views/__init__.py | 23 +++- o2ims/views/api_ns.py | 10 ++ o2ims/views/ocloud_dto.py | 31 +---- o2ims/views/ocloud_route.py | 68 ++-------- o2ims/views/ocloud_view.py | 57 +------- o2ims/views/provision_dto.py | 49 +++++++ o2ims/views/provision_route.py | 74 +++++++++++ o2ims/views/provision_view.py | 71 ++++++++++ tests/conftest.py | 14 +- tests/unit/test_ocloud.py | 90 ++----------- tests/unit/test_provision.py | 147 +++++++++++++++++++++ 26 files changed, 571 insertions(+), 348 deletions(-) create mode 100644 o2ims/domain/configuration_obj.py create mode 100644 o2ims/domain/configuration_repo.py rename o2ims/service/event/{registration_event.py => configuration_event.py} (86%) create mode 100644 o2ims/views/api_ns.py create mode 100644 o2ims/views/provision_dto.py create mode 100644 o2ims/views/provision_route.py create mode 100644 o2ims/views/provision_view.py create mode 100644 tests/unit/test_provision.py diff --git a/docs/installation-guide.rst b/docs/installation-guide.rst index 0297aa1..8768ddb 100644 --- a/docs/installation-guide.rst +++ b/docs/installation-guide.rst @@ -164,14 +164,15 @@ The following instruction should be done outside of INF platform controller host curl -k http(s)://:30205/o2ims_infrastructureInventory/v1 -3 Register O-Cloud to SMO +3. Register O-Cloud to SMO +-------------------------- - assumed you have setup SMO O2 endpoint for registration - O2 service will post the O-Cloud registration data to that SMO O2 endpoint :: - curl -k -X POST http(s)://:30205/provision/smo-endpoint/v1 -d '{"smo-o2-endpoint": ""}' + curl -k -X POST http(s)://:30205/provision/v1/smo-endpoint -d '{"endpoint": ""}' References diff --git a/o2app/adapter/unit_of_work.py b/o2app/adapter/unit_of_work.py index de48001..000d181 100644 --- a/o2app/adapter/unit_of_work.py +++ b/o2app/adapter/unit_of_work.py @@ -51,8 +51,8 @@ class SqlAlchemyUnitOfWork(AbstractUnitOfWork): .ResourceSqlAlchemyRepository(self.session) self.subscriptions = ocloud_repository\ .SubscriptionSqlAlchemyRepository(self.session) - self.registrations = ocloud_repository\ - .RegistrationSqlAlchemyRepository(self.session) + self.configurations = ocloud_repository\ + .ConfigurationSqlAlchemyRepository(self.session) self.deployment_managers = ocloud_repository\ .DeploymentManagerSqlAlchemyRepository(self.session) self.nfdeployment_descs = dms_repository\ @@ -93,7 +93,7 @@ class SqlAlchemyUnitOfWork(AbstractUnitOfWork): for entry in self.subscriptions.seen: while hasattr(entry, 'events') and len(entry.events) > 0: yield entry.events.pop(0) - for entry in self.registrations.seen: + for entry in self.configurations.seen: while hasattr(entry, 'events') and len(entry.events) > 0: yield entry.events.pop(0) for entry in self.nfdeployment_descs.seen: diff --git a/o2app/entrypoints/flask_application.py b/o2app/entrypoints/flask_application.py index f9d5f85..0d1a11a 100644 --- a/o2app/entrypoints/flask_application.py +++ b/o2app/entrypoints/flask_application.py @@ -16,7 +16,7 @@ from flask import Flask from flask_restx import Api from o2app import bootstrap -from o2ims.views import ocloud_route as ims_route +from o2ims.views import configure_namespace as ims_route_configure_namespace from o2dms.api import configure_namespace as dms_route_configure_namespace @@ -30,5 +30,5 @@ api = Api(app, version='1.0.0', ) bus = bootstrap.bootstrap() -ims_route.configure_namespace(api, bus) +ims_route_configure_namespace(api) dms_route_configure_namespace(api) diff --git a/o2app/entrypoints/redis_eventconsumer.py b/o2app/entrypoints/redis_eventconsumer.py index d4c7c65..5630174 100644 --- a/o2app/entrypoints/redis_eventconsumer.py +++ b/o2app/entrypoints/redis_eventconsumer.py @@ -22,7 +22,8 @@ from o2dms.domain import commands from o2ims.domain import commands as imscmd from o2common.helper import o2logging -from o2ims.domain.subscription_obj import Message2SMO, NotificationEventEnum, RegistrationMessage +from o2ims.domain.subscription_obj import Message2SMO, NotificationEventEnum,\ + RegistrationMessage logger = o2logging.get_logger(__name__) r = redis.Redis(**config.get_redis_host_and_port()) @@ -36,7 +37,7 @@ def main(): pubsub = r.pubsub(ignore_subscribe_messages=True) pubsub.subscribe("NfDeploymentStateChanged") pubsub.subscribe('ResourceChanged') - pubsub.subscribe('RegistrationChanged') + pubsub.subscribe('ConfigurationChanged') pubsub.subscribe('OcloudChanged') for m in pubsub.listen(): @@ -71,10 +72,10 @@ def handle_dms_changed(m, bus): eventtype=data['notificationEventType'], updatetime=data['updatetime'])) bus.handle(cmd) - elif channel == 'RegistrationChanged': + elif channel == 'ConfigurationChanged': datastr = m['data'] data = json.loads(datastr) - logger.info('RegistrationChanged with cmd:{}'.format(data)) + logger.info('ConfigurationChanged with cmd:{}'.format(data)) cmd = imscmd.Register2SMO(data=RegistrationMessage(id=data['id'])) bus.handle(cmd) elif channel == 'OcloudChanged': diff --git a/o2app/service/handlers.py b/o2app/service/handlers.py index deef1a4..9e88ff8 100644 --- a/o2app/service/handlers.py +++ b/o2app/service/handlers.py @@ -29,7 +29,7 @@ from o2ims.service.auditor import ocloud_handler, dms_handler, \ pserver_eth_handler from o2ims.service.command import notify_handler, registration_handler from o2ims.service.event import ocloud_event, resource_event, \ - resource_pool_event, registration_event + resource_pool_event, configuration_event # if TYPE_CHECKING: # from . import unit_of_work @@ -55,8 +55,8 @@ EVENT_HANDLERS = { events.ResourceChanged: [resource_event.notify_resource_change], events.ResourcePoolChanged: [resource_pool_event.\ notify_resourcepool_change], - events.RegistrationChanged: [registration_event.\ - notify_registration_change], + events.ConfigurationChanged: [configuration_event.\ + notify_configuration_change], } # type: Dict[Type[events.Event], Callable] diff --git a/o2common/config/config.py b/o2common/config/config.py index 7207006..8a869da 100644 --- a/o2common/config/config.py +++ b/o2common/config/config.py @@ -41,6 +41,10 @@ def get_o2ims_api_base(): return get_root_api_base() + 'o2ims_infrastructureInventory/v1' +def get_provision_api_base(): + return get_root_api_base() + 'provision/v1' + + def get_o2dms_api_base(): return get_root_api_base() + "o2dms" diff --git a/o2ims/adapter/ocloud_repository.py b/o2ims/adapter/ocloud_repository.py index bd14722..8b54f53 100644 --- a/o2ims/adapter/ocloud_repository.py +++ b/o2ims/adapter/ocloud_repository.py @@ -14,11 +14,11 @@ from typing import List -from o2ims.domain import ocloud, subscription_obj +from o2ims.domain import ocloud, subscription_obj, configuration_obj from o2ims.domain.ocloud_repo import OcloudRepository, ResourceTypeRepository,\ ResourcePoolRepository, ResourceRepository, DeploymentManagerRepository -from o2ims.domain.subscription_repo import SubscriptionRepository, \ - RegistrationRepository +from o2ims.domain.subscription_repo import SubscriptionRepository +from o2ims.domain.configuration_repo import ConfigurationRepository from o2common.helper import o2logging logger = o2logging.get_logger(__name__) @@ -162,24 +162,24 @@ class SubscriptionSqlAlchemyRepository(SubscriptionRepository): subscriptionId=subscription_id).delete() -class RegistrationSqlAlchemyRepository(RegistrationRepository): +class ConfigurationSqlAlchemyRepository(ConfigurationRepository): def __init__(self, session): super().__init__() self.session = session - def _add(self, registration: subscription_obj.Registration): - self.session.add(registration) + def _add(self, configuration: configuration_obj.Configuration): + self.session.add(configuration) - def _get(self, registration_id) -> subscription_obj.Registration: - return self.session.query(subscription_obj.Registration).filter_by( - registrationId=registration_id).first() + def _get(self, configuration_id) -> configuration_obj.Configuration: + return self.session.query(configuration_obj.Configuration).filter_by( + configurationId=configuration_id).first() - def _list(self) -> List[subscription_obj.Registration]: - return self.session.query(subscription_obj.Registration) + def _list(self) -> List[configuration_obj.Configuration]: + return self.session.query(configuration_obj.Configuration) - def _update(self, registration: subscription_obj.Registration): - self.session.add(registration) + def _update(self, configuration: configuration_obj.Configuration): + self.session.add(configuration) - def _delete(self, registration_id): - self.session.query(subscription_obj.Registration).filter_by( - registrationId=registration_id).delete() + def _delete(self, configuration_id): + self.session.query(configuration_obj.Configuration).filter_by( + configurationId=configuration_id).delete() diff --git a/o2ims/adapter/orm.py b/o2ims/adapter/orm.py index 29fff79..f59a235 100644 --- a/o2ims/adapter/orm.py +++ b/o2ims/adapter/orm.py @@ -34,6 +34,7 @@ from sqlalchemy.orm import mapper, relationship from o2ims.domain import ocloud as ocloudModel from o2ims.domain import subscription_obj as subModel +from o2ims.domain import configuration_obj as confModel from o2ims.domain.resource_type import ResourceTypeEnum from o2common.helper import o2logging @@ -145,13 +146,14 @@ subscription = Table( Column("filter", String(255)), ) -registration = Table( - "registration", +configuration = Table( + "configuration", metadata, Column("updatetime", DateTime), Column("createtime", DateTime), - Column("registrationId", String(255), primary_key=True), + Column("configurationId", String(255), primary_key=True), + Column("conftype", String(255)), Column("callback", String(255)), Column("status", String(255)), Column("comments", String(255)), @@ -181,7 +183,7 @@ def start_o2ims_mappers(engine=None): } ) mapper(subModel.Subscription, subscription) - mapper(subModel.Registration, registration) + mapper(confModel.Configuration, configuration) if engine is not None: metadata.create_all(engine) diff --git a/o2ims/domain/configuration_obj.py b/o2ims/domain/configuration_obj.py new file mode 100644 index 0000000..f7b6c6b --- /dev/null +++ b/o2ims/domain/configuration_obj.py @@ -0,0 +1,52 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 __future__ import annotations +from enum import Enum +# from dataclasses import dataclass + +from o2common.domain.base import AgRoot, Serializer + + +class RegistrationStatusEnum(str, Enum): + CREATED = 'CREATED' + NOTIFIED = 'NOTIFIED' + FAILED = 'FAILED' + + +class ConfigurationTypeEnum(str, Enum): + SMO = 'SMO' + + +class Configuration(AgRoot, Serializer): + def __init__(self, id: str, url: str, + conf_type: ConfigurationTypeEnum, + status: RegistrationStatusEnum = + RegistrationStatusEnum.CREATED, + comments: str = '') -> None: + super().__init__() + self.configurationId = id + self.conftype = conf_type + self.callback = url + self.status = status + self.comments = comments + + def serialize_smo(self): + if self.conftype != ConfigurationTypeEnum.SMO: + return + + d = Serializer.serialize(self) + + d['endpoint'] = d['callback'] + return d diff --git a/o2ims/domain/configuration_repo.py b/o2ims/domain/configuration_repo.py new file mode 100644 index 0000000..9ff95fa --- /dev/null +++ b/o2ims/domain/configuration_repo.py @@ -0,0 +1,57 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 +from typing import List, Set +from o2ims.domain import configuration_obj as obj + + +class ConfigurationRepository(abc.ABC): + def __init__(self): + self.seen = set() # type: Set[obj.Configuration] + + def add(self, configuration: obj.Configuration): + self._add(configuration) + self.seen.add(configuration) + + def get(self, configuration_id) -> obj.Configuration: + configuration = self._get(configuration_id) + if configuration: + self.seen.add(configuration) + return configuration + + def list(self) -> List[obj.Configuration]: + return self._list() + + def update(self, configuration: obj.Configuration): + self._update(configuration) + + def delete(self, configuration_id): + self._delete(configuration_id) + + @abc.abstractmethod + def _add(self, configuration: obj.Configuration): + raise NotImplementedError + + @abc.abstractmethod + def _get(self, configuration_id) -> obj.Configuration: + raise NotImplementedError + + @abc.abstractmethod + def _update(self, configuration: obj.Configuration): + raise NotImplementedError + + @abc.abstractmethod + def _delete(self, configuration_id): + raise NotImplementedError diff --git a/o2ims/domain/events.py b/o2ims/domain/events.py index 6f81a84..4858040 100644 --- a/o2ims/domain/events.py +++ b/o2ims/domain/events.py @@ -49,6 +49,6 @@ class ResourceChanged(Event): @dataclass -class RegistrationChanged(Event): +class ConfigurationChanged(Event): id: str updatetime: datetime.now() diff --git a/o2ims/domain/subscription_obj.py b/o2ims/domain/subscription_obj.py index ff8beaf..1746ad9 100644 --- a/o2ims/domain/subscription_obj.py +++ b/o2ims/domain/subscription_obj.py @@ -45,24 +45,6 @@ class Message2SMO(Serializer): self.updatetime = updatetime -class RegistrationStatusEnum(str, Enum): - CREATED = 'CREATED' - NOTIFIED = 'NOTIFIED' - FAILED = 'FAILED' - - -class Registration(AgRoot, Serializer): - def __init__(self, id: str, url: str, - status: RegistrationStatusEnum = - RegistrationStatusEnum.CREATED, - comments: str = '') -> None: - super().__init__() - self.registrationId = id - self.callback = url - self.status = status - self.comments = comments - - class RegistrationMessage(Serializer): def __init__(self, is_all: bool = None, id: str = '') -> None: self.all = is_all if is_all is not None else False diff --git a/o2ims/domain/subscription_repo.py b/o2ims/domain/subscription_repo.py index 82df377..d12c00d 100644 --- a/o2ims/domain/subscription_repo.py +++ b/o2ims/domain/subscription_repo.py @@ -55,43 +55,3 @@ class SubscriptionRepository(abc.ABC): @abc.abstractmethod def _delete(self, subscription_id): raise NotImplementedError - - -class RegistrationRepository(abc.ABC): - def __init__(self): - self.seen = set() # type: Set[subobj.Subscription] - - def add(self, registration: subobj.Registration): - self._add(registration) - self.seen.add(registration) - - def get(self, registration_id) -> subobj.Registration: - registration = self._get(registration_id) - if registration: - self.seen.add(registration) - return registration - - def list(self) -> List[subobj.Registration]: - return self._list() - - def update(self, registration: subobj.Registration): - self._update(registration) - - def delete(self, registration_id): - self._delete(registration_id) - - @abc.abstractmethod - def _add(self, registration: subobj.Registration): - raise NotImplementedError - - @abc.abstractmethod - def _get(self, registration_id) -> subobj.Registration: - raise NotImplementedError - - @abc.abstractmethod - def _update(self, registration: subobj.Registration): - raise NotImplementedError - - @abc.abstractmethod - def _delete(self, registration_id): - raise NotImplementedError diff --git a/o2ims/service/command/registration_handler.py b/o2ims/service/command/registration_handler.py index 0a4395d..d363149 100644 --- a/o2ims/service/command/registration_handler.py +++ b/o2ims/service/command/registration_handler.py @@ -23,7 +23,8 @@ from retry import retry from o2common.service.unit_of_work import AbstractUnitOfWork from o2common.config import config from o2ims.domain import commands -from o2ims.domain.subscription_obj import RegistrationStatusEnum +from o2ims.domain.configuration_obj import ConfigurationTypeEnum, \ + RegistrationStatusEnum from o2common.helper import o2logging logger = o2logging.get_logger(__name__) @@ -37,33 +38,36 @@ def registry_to_smo( data = cmd.data logger.info('The Register2SMO all is {}'.format(data.all)) if data.all: - regs = uow.registrations.list() - for reg in regs: - reg_data = reg.serialize() - logger.debug('Registration: {}'.format(reg_data['registrationId'])) + confs = uow.configrations.list() + for conf in confs: + if conf.conftype != ConfigurationTypeEnum.SMO: + continue + reg_data = conf.serialize() + logger.debug('Configuration: {}'.format( + reg_data['configurationId'])) register_smo(uow, reg_data) else: with uow: - reg = uow.registrations.get(data.id) - if reg is None: + conf = uow.configurations.get(data.id) + if conf is None: return - logger.debug('Registration: {}'.format(reg.registrationId)) - reg_data = reg.serialize() - register_smo(uow, reg_data) + logger.debug('Configuration: {}'.format(conf.configurationId)) + conf_data = conf.serialize() + register_smo(uow, conf_data) def register_smo(uow, reg_data): call_res = call_smo(reg_data) logger.debug('Call SMO response is {}'.format(call_res)) if call_res: - reg = uow.registrations.get(reg_data['registrationId']) + reg = uow.configurations.get(reg_data['configurationId']) if reg is None: return reg.status = RegistrationStatusEnum.NOTIFIED - logger.debug('Updating Registration: {}'.format( - reg.registrationId)) - uow.registrations.update(reg) + logger.debug('Updating Configurations: {}'.format( + reg.configurationId)) + uow.configurations.update(reg) uow.commit() @@ -82,7 +86,7 @@ def register_smo(uow, reg_data): @retry((ConnectionRefusedError), tries=2, delay=2) def call_smo(reg_data: dict): callback_data = json.dumps({ - 'consumerSubscriptionId': reg_data['registrationId'], + 'consumerSubscriptionId': reg_data['configurationId'], 'imsUrl': config.get_api_url() }) logger.info('URL: {}, data: {}'.format( diff --git a/o2ims/service/event/registration_event.py b/o2ims/service/event/configuration_event.py similarity index 86% rename from o2ims/service/event/registration_event.py rename to o2ims/service/event/configuration_event.py index e05270e..ddaeb35 100644 --- a/o2ims/service/event/registration_event.py +++ b/o2ims/service/event/configuration_event.py @@ -20,11 +20,11 @@ from o2common.helper import o2logging logger = o2logging.get_logger(__name__) -def notify_registration_change( - event: events.RegistrationChanged, +def notify_configuration_change( + event: events.ConfigurationChanged, publish: Callable, ): logger.info('In notify_registration_change') - publish("RegistrationChanged", event) + publish("ConfigurationChanged", event) logger.debug("published Registration Changed: {}".format( event.id)) diff --git a/o2ims/views/__init__.py b/o2ims/views/__init__.py index 0647235..a43118b 100644 --- a/o2ims/views/__init__.py +++ b/o2ims/views/__init__.py @@ -12,8 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -from flask_restx import Namespace -api_ims_inventory_v1 = Namespace( - "O2IMS_Inventory", - description='IMS Inventory related operations.') +from o2common.config import config +from . import ocloud_route, provision_route +from . import api_ns + +from o2common.helper import o2logging +logger = o2logging.get_logger(__name__) + + +def configure_namespace(app): + apiims = config.get_o2ims_api_base() + apiprovision = config.get_provision_api_base() + logger.info( + "Expose IMS API:{}\nExpose Provision API: {}". + format(apiims, apiprovision)) + + ocloud_route.configure_api_route() + provision_route.configure_api_route() + app.add_namespace(api_ns.api_ims_inventory_v1, path=apiims) + app.add_namespace(api_ns.api_provision_v1, path=apiprovision) diff --git a/o2ims/views/api_ns.py b/o2ims/views/api_ns.py new file mode 100644 index 0000000..955a598 --- /dev/null +++ b/o2ims/views/api_ns.py @@ -0,0 +1,10 @@ +from flask_restx import Namespace + + +api_ims_inventory_v1 = Namespace( + "O2IMS_Inventory", + description='IMS Inventory related operations.') + +api_provision_v1 = Namespace( + "PROVISION", + description='Provision related operations.') diff --git a/o2ims/views/ocloud_dto.py b/o2ims/views/ocloud_dto.py index 6bf994a..c6896e0 100644 --- a/o2ims/views/ocloud_dto.py +++ b/o2ims/views/ocloud_dto.py @@ -14,7 +14,7 @@ from flask_restx import fields -from o2ims.views import api_ims_inventory_v1 +from o2ims.views.api_ns import api_ims_inventory_v1 class OcloudDTO: @@ -178,32 +178,3 @@ class SubscriptionDTO: description='Subscription ID'), } ) - - -class RegistrationDTO: - - registration_get = api_ims_inventory_v1.model( - "RegistrationGetDto", - { - 'registrationId': fields.String(required=True, - description='Registration ID'), - 'callback': fields.String, - 'notified': fields.Boolean, - } - ) - - registration = api_ims_inventory_v1.model( - "RegistrationCreateDto", - { - 'callback': fields.String( - required=True, description='Registration SMO callback address') - } - ) - - registration_post_resp = api_ims_inventory_v1.model( - "RegistrationCreatedRespDto", - { - 'registrationId': fields.String(required=True, - description='registration ID'), - } - ) diff --git a/o2ims/views/ocloud_route.py b/o2ims/views/ocloud_route.py index 99adb5f..88b369e 100644 --- a/o2ims/views/ocloud_route.py +++ b/o2ims/views/ocloud_route.py @@ -14,14 +14,17 @@ from flask_restx import Resource -from o2ims.views import ocloud_view, api_ims_inventory_v1 -from o2common.config import config +from o2common.service.messagebus import MessageBus +from o2ims.views import ocloud_view +from o2ims.views.api_ns import api_ims_inventory_v1 from o2ims.views.ocloud_dto import OcloudDTO, ResourceTypeDTO,\ - ResourcePoolDTO, ResourceDTO, DeploymentManagerDTO, SubscriptionDTO,\ - RegistrationDTO + ResourcePoolDTO, ResourceDTO, DeploymentManagerDTO, SubscriptionDTO -apibase = config.get_o2ims_api_base() +def configure_api_route(): + # Set global bus for resource + global bus + bus = MessageBus.get_instance() # ---------- OClouds ---------- # @@ -207,58 +210,3 @@ class SubscriptionGetDelRouter(Resource): def delete(self, subscriptionID): result = ocloud_view.subscription_delete(subscriptionID, bus.uow) return result, 204 - - -# ---------- Registration ---------- # -@api_ims_inventory_v1.route("/registrations") -class RegistrationListRouter(Resource): - - model = RegistrationDTO.registration_get - expect = RegistrationDTO.registration - post_resp = RegistrationDTO.registration_post_resp - - @api_ims_inventory_v1.doc('List registrations') - @api_ims_inventory_v1.marshal_list_with(model) - def get(self): - return ocloud_view.registrations(bus.uow) - - @api_ims_inventory_v1.doc('Create a registration') - @api_ims_inventory_v1.expect(expect) - @api_ims_inventory_v1.marshal_with(post_resp, code=201) - def post(self): - data = api_ims_inventory_v1.payload - result = ocloud_view.registration_create(data, bus) - return result, 201 - - -@api_ims_inventory_v1.route("/registrations/") -@api_ims_inventory_v1.param('registrationID', 'ID of the registration') -@api_ims_inventory_v1.response(404, 'Registration not found') -class RegistrationGetDelRouter(Resource): - - model = RegistrationDTO.registration_get - - @api_ims_inventory_v1.doc('Get registration by ID') - @api_ims_inventory_v1.marshal_with(model) - def get(self, registrationID): - result = ocloud_view.registration_one( - registrationID, bus.uow) - if result is not None: - return result - api_ims_inventory_v1.abort(404, "Registration {} doesn't exist".format( - registrationID)) - - @api_ims_inventory_v1.doc('Delete registration by ID') - @api_ims_inventory_v1.response(204, 'Registration deleted') - def delete(self, registrationID): - result = ocloud_view.registration_delete(registrationID, bus.uow) - return result, 204 - - -def configure_namespace(app, bus_new): - - # Set global bus for resource - global bus - bus = bus_new - - app.add_namespace(api_ims_inventory_v1, path=apibase) diff --git a/o2ims/views/ocloud_view.py b/o2ims/views/ocloud_view.py index ae8b204..3735298 100644 --- a/o2ims/views/ocloud_view.py +++ b/o2ims/views/ocloud_view.py @@ -12,14 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import uuid -from datetime import datetime -from o2common.service import unit_of_work, messagebus -from o2ims.domain import events -from o2ims.views.ocloud_dto import RegistrationDTO, SubscriptionDTO -from o2ims.domain.subscription_obj import Registration, Subscription +from o2common.service import unit_of_work +from o2ims.views.ocloud_dto import SubscriptionDTO +from o2ims.domain.subscription_obj import Subscription def oclouds(uow: unit_of_work.AbstractUnitOfWork): @@ -118,51 +115,3 @@ def subscription_delete(subscriptionId: str, uow.subscriptions.delete(subscriptionId) uow.commit() return True - - -def registrations(uow: unit_of_work.AbstractUnitOfWork): - with uow: - li = uow.registrations.list() - return [r.serialize() for r in li] - - -def registration_one(registrationId: str, - uow: unit_of_work.AbstractUnitOfWork): - with uow: - first = uow.registrations.get(registrationId) - return first.serialize() if first is not None else None - - -def registration_create(registrationDto: RegistrationDTO.registration, - bus: messagebus.MessageBus): - - reg_uuid = str(uuid.uuid4()) - registration = Registration( - reg_uuid, registrationDto['callback']) - with bus.uow as uow: - uow.registrations.add(registration) - logging.debug('before event length {}'.format( - len(registration.events))) - registration.events.append(events.RegistrationChanged( - reg_uuid, - datetime.now())) - logging.debug('after event length {}'.format(len(registration.events))) - uow.commit() - _handle_events(bus) - return {"registrationId": reg_uuid} - - -def registration_delete(registrationId: str, - uow: unit_of_work.AbstractUnitOfWork): - with uow: - uow.registrations.delete(registrationId) - uow.commit() - return True - - -def _handle_events(bus: messagebus.MessageBus): - # handle events - events = bus.uow.collect_new_events() - for event in events: - bus.handle(event) - return True diff --git a/o2ims/views/provision_dto.py b/o2ims/views/provision_dto.py new file mode 100644 index 0000000..f65ac49 --- /dev/null +++ b/o2ims/views/provision_dto.py @@ -0,0 +1,49 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 flask_restx import fields + +from o2ims.views.api_ns import api_provision_v1 + + +class SmoEndpointDTO: + + endpoint_get = api_provision_v1.model( + "SmoEndpointGetDto", + { + 'configurationId': fields.String(required=True, + description='Configuration ID'), + 'endpoint': fields.String, + 'status': fields.String, + 'comments': fields.String, + } + ) + + endpoint = api_provision_v1.model( + "SmoEndpointCreateDto", + { + 'endpoint': fields.String( + required=True, + description='Configuration SMO callback address', + example='http://mock_smo:80/registration') + } + ) + + endpoint_post_resp = api_provision_v1.model( + "SmoEndpointCreatedRespDto", + { + 'configurationId': fields.String(required=True, + description='Configuration ID'), + } + ) diff --git a/o2ims/views/provision_route.py b/o2ims/views/provision_route.py new file mode 100644 index 0000000..7c91e7e --- /dev/null +++ b/o2ims/views/provision_route.py @@ -0,0 +1,74 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 flask_restx import Resource + +from o2common.service.messagebus import MessageBus +from o2ims.views import provision_view +from o2ims.views.api_ns import api_provision_v1 +from o2ims.views.provision_dto import SmoEndpointDTO + + +def configure_api_route(): + # Set global bus for resource + global bus + bus = MessageBus.get_instance() + + +# ---------- SMO endpoint ---------- # +@api_provision_v1.route("/smo-endpoint") +class SmoEndpointListRouter(Resource): + + model = SmoEndpointDTO.endpoint_get + expect = SmoEndpointDTO.endpoint + post_resp = SmoEndpointDTO.endpoint_post_resp + + @api_provision_v1.doc('List SMO endpoints') + @api_provision_v1.marshal_list_with(model) + def get(self): + return provision_view.configurations(bus.uow) + + @api_provision_v1.doc('Create a SMO endpoint') + @api_provision_v1.expect(expect) + @api_provision_v1.marshal_with(post_resp, code=201) + def post(self): + data = api_provision_v1.payload + result = provision_view.configuration_create(data, bus) + return result, 201 + + +@api_provision_v1.route("/smo-endpoint/") +@api_provision_v1.param('configurationID', + 'ID of the SMO endpoint configuration') +@api_provision_v1.response(404, 'SMO Endpoint configuration not found') +class SmoEndpointGetDelRouter(Resource): + + model = SmoEndpointDTO.endpoint_get + + @api_provision_v1.doc('Get configuration by ID') + @api_provision_v1.marshal_with(model) + def get(self, configurationID): + result = provision_view.configuration_one( + configurationID, bus.uow) + if result is not None: + return result + api_provision_v1.abort(404, + "SMO Endpoint configuration {} doesn't exist". + format(configurationID)) + + @api_provision_v1.doc('Delete configuration by ID') + @api_provision_v1.response(204, 'Configuration deleted') + def delete(self, configurationID): + result = provision_view.configuration_delete(configurationID, bus.uow) + return result, 204 diff --git a/o2ims/views/provision_view.py b/o2ims/views/provision_view.py new file mode 100644 index 0000000..54e5a15 --- /dev/null +++ b/o2ims/views/provision_view.py @@ -0,0 +1,71 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 logging +import uuid +from datetime import datetime + +from o2common.service import unit_of_work, messagebus +from o2ims.domain import events +from o2ims.views.provision_dto import SmoEndpointDTO +from o2ims.domain.configuration_obj import Configuration, ConfigurationTypeEnum + + +def configurations(uow: unit_of_work.AbstractUnitOfWork): + with uow: + li = uow.configurations.list() + return [r.serialize_smo() for r in li] + + +def configuration_one(configurationId: str, + uow: unit_of_work.AbstractUnitOfWork): + with uow: + first = uow.configurations.get(configurationId) + return first.serialize_smo() if first is not None else None + + +def configuration_create(configurationDto: SmoEndpointDTO.endpoint, + bus: messagebus.MessageBus): + + conf_uuid = str(uuid.uuid4()) + configuration = Configuration( + conf_uuid, configurationDto['endpoint'], ConfigurationTypeEnum.SMO) + with bus.uow as uow: + uow.configurations.add(configuration) + logging.debug('before event length {}'.format( + len(configuration.events))) + configuration.events.append(events.ConfigurationChanged( + conf_uuid, + datetime.now())) + logging.debug('after event length {}'.format( + len(configuration.events))) + uow.commit() + _handle_events(bus) + return {"configurationId": conf_uuid} + + +def configuration_delete(configurationId: str, + uow: unit_of_work.AbstractUnitOfWork): + with uow: + uow.configurations.delete(configurationId) + uow.commit() + return True + + +def _handle_events(bus: messagebus.MessageBus): + # handle events + events = bus.uow.collect_new_events() + for event in events: + bus.handle(event) + return True diff --git a/tests/conftest.py b/tests/conftest.py index 9d7f136..a1655b4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ from o2ims.adapter.orm import metadata, start_o2ims_mappers # from o2ims.adapter.clients.orm_stx import start_o2ims_stx_mappers from o2app.adapter import unit_of_work -from o2ims.views.ocloud_route import configure_namespace +from o2ims.views import configure_namespace from o2app.bootstrap import bootstrap @@ -38,8 +38,8 @@ def mock_flask_uow(mock_uow): app = Flask(__name__) app.config["TESTING"] = True api = Api(app) - bus = bootstrap(False, uow) - configure_namespace(api, bus) + bootstrap(False, uow) + configure_namespace(api) return session, app @@ -75,8 +75,8 @@ def sqlite_flask_uow(sqlite_uow): app = Flask(__name__) app.config["TESTING"] = True api = Api(app) - bus = bootstrap(False, sqlite_uow) - configure_namespace(api, bus) + bootstrap(False, sqlite_uow) + configure_namespace(api) yield sqlite_uow, app @@ -135,8 +135,8 @@ def postgres_flask_uow(postgres_uow): app = Flask(__name__) app.config["TESTING"] = True api = Api(app) - bus = bootstrap(False, postgres_uow) - configure_namespace(api, bus) + bootstrap(False, postgres_uow) + configure_namespace(api) yield postgres_uow, app diff --git a/tests/unit/test_ocloud.py b/tests/unit/test_ocloud.py index 95dd372..271f5c5 100644 --- a/tests/unit/test_ocloud.py +++ b/tests/unit/test_ocloud.py @@ -15,7 +15,7 @@ import uuid from unittest.mock import MagicMock -from o2ims.domain import ocloud, subscription_obj +from o2ims.domain import ocloud, subscription_obj, configuration_obj from o2ims.domain import resource_type as rt from o2ims.views import ocloud_view from o2common.config import config @@ -94,12 +94,13 @@ def test_new_subscription(): subscription1.subscriptionId == subscription_id1 -def test_new_registration(): - registration_id1 = str(uuid.uuid4()) - registration1 = subscription_obj.Registration( - registration_id1, "https://callback/uri/write/here") - assert registration_id1 is not None and\ - registration1.registrationId == registration_id1 +def test_new_configuration(): + configuration_id1 = str(uuid.uuid4()) + configuration1 = configuration_obj.Configuration( + configuration_id1, "https://callback/uri/write/here", + "SMO") + assert configuration_id1 is not None and\ + configuration1.configurationId == configuration_id1 def test_view_olcouds(mock_uow): @@ -319,44 +320,6 @@ def test_view_subscription_one(mock_uow): "subscriptionId")) == subscription_id1 -def test_view_registrations(mock_uow): - session, uow = mock_uow - - registration_id1 = str(uuid.uuid4()) - reg1 = MagicMock() - reg1.serialize.return_value = { - "registrationId": registration_id1, - } - session.return_value.query.return_value = [reg1] - - registration_list = ocloud_view.registrations(uow) - assert str(registration_list[0].get( - "registrationId")) == registration_id1 - - -def test_view_registration_one(mock_uow): - session, uow = mock_uow - - registration_id1 = str(uuid.uuid4()) - session.return_value.query.return_value.filter_by.return_value.first.\ - return_value.serialize.return_value = None - - # Query return None - registration_res = ocloud_view.registration_one( - registration_id1, uow) - assert registration_res is None - - session.return_value.query.return_value.filter_by.return_value.first.\ - return_value.serialize.return_value = { - "registrationId": registration_id1, - } - - registration_res = ocloud_view.registration_one( - registration_id1, uow) - assert str(registration_res.get( - "registrationId")) == registration_id1 - - def test_flask_get_list(mock_flask_uow): session, app = mock_flask_uow session.query.return_value = [] @@ -382,9 +345,6 @@ def test_flask_get_list(mock_flask_uow): resp = client.get(apibase+"/subscriptions") assert resp.get_data() == b'[]\n' - resp = client.get(apibase+"/registrations") - assert resp.get_data() == b'[]\n' - def test_flask_get_one(mock_flask_uow): session, app = mock_flask_uow @@ -421,10 +381,6 @@ def test_flask_get_one(mock_flask_uow): resp = client.get(apibase+"/subscriptions/"+subscription_id1) assert resp.status_code == 404 - registration_id1 = str(uuid.uuid4()) - resp = client.get(apibase+"/registrations/"+registration_id1) - assert resp.status_code == 404 - def test_flask_post(mock_flask_uow): session, app = mock_flask_uow @@ -442,13 +398,6 @@ def test_flask_post(mock_flask_uow): assert resp.status_code == 201 assert 'subscriptionId' in resp.get_json() - reg_callback = 'http://registration/callback/url' - resp = client.post(apibase+'/registrations', json={ - 'callback': reg_callback, - }) - assert resp.status_code == 201 - assert 'registrationId' in resp.get_json() - def test_flask_delete(mock_flask_uow): session, app = mock_flask_uow @@ -461,10 +410,6 @@ def test_flask_delete(mock_flask_uow): resp = client.delete(apibase+"/subscriptions/"+subscription_id1) assert resp.status_code == 204 - registration_id1 = str(uuid.uuid4()) - resp = client.delete(apibase+"/registrations/"+registration_id1) - assert resp.status_code == 204 - def test_flask_not_allowed(mock_flask_uow): _, app = mock_flask_uow @@ -582,22 +527,3 @@ def test_flask_not_allowed(mock_flask_uow): assert resp.status == '405 METHOD NOT ALLOWED' resp = client.patch(uri) assert resp.status == '405 METHOD NOT ALLOWED' - - # Testing registrations not support method - ########################## - uri = apibase + "/registrations" - resp = client.put(uri) - assert resp.status == '405 METHOD NOT ALLOWED' - resp = client.patch(uri) - assert resp.status == '405 METHOD NOT ALLOWED' - resp = client.delete(uri) - assert resp.status == '405 METHOD NOT ALLOWED' - - subscription_id1 = str(uuid.uuid4()) - uri = apibase + "/registrations/" + subscription_id1 - resp = client.post(uri) - assert resp.status == '405 METHOD NOT ALLOWED' - resp = client.put(uri) - assert resp.status == '405 METHOD NOT ALLOWED' - resp = client.patch(uri) - assert resp.status == '405 METHOD NOT ALLOWED' diff --git a/tests/unit/test_provision.py b/tests/unit/test_provision.py new file mode 100644 index 0000000..3585802 --- /dev/null +++ b/tests/unit/test_provision.py @@ -0,0 +1,147 @@ +# Copyright (C) 2021 Wind River Systems, Inc. +# +# 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 uuid +from unittest.mock import MagicMock + +from o2ims.domain import configuration_obj +from o2ims.views import provision_view +from o2common.config import config + + +def test_new_smo_endpoint(): + configuration_id1 = str(uuid.uuid4()) + configuration1 = configuration_obj.Configuration( + configuration_id1, "https://callback/uri/write/here", + "SMO") + assert configuration_id1 is not None and\ + configuration1.configurationId == configuration_id1 + + +def test_view_smo_endpoint(mock_uow): + session, uow = mock_uow + + configuration_id1 = str(uuid.uuid4()) + conf1 = MagicMock() + conf1.serialize_smo.return_value = { + "configurationId": configuration_id1, + } + session.return_value.query.return_value = [conf1] + + configuration_list = provision_view.configurations(uow) + assert str(configuration_list[0].get( + "configurationId")) == configuration_id1 + + +def test_view_smo_endpoint_one(mock_uow): + session, uow = mock_uow + + configuration_id1 = str(uuid.uuid4()) + session.return_value.query.return_value.filter_by.return_value.first.\ + return_value.serialize_smo.return_value = None + + # Query return None + configuration_res = provision_view.configuration_one( + configuration_id1, uow) + assert configuration_res is None + + session.return_value.query.return_value.filter_by.return_value.first.\ + return_value.serialize_smo.return_value = { + "configurationId": configuration_id1, + } + + configuration_res = provision_view.configuration_one( + configuration_id1, uow) + assert str(configuration_res.get( + "configurationId")) == configuration_id1 + + +def test_flask_get_list(mock_flask_uow): + session, app = mock_flask_uow + session.query.return_value = [] + apibase = config.get_provision_api_base() + + with app.test_client() as client: + # Get list and return empty list + ########################## + resp = client.get(apibase+"/smo-endpoint") + assert resp.get_data() == b'[]\n' + + +def test_flask_get_one(mock_flask_uow): + session, app = mock_flask_uow + + session.return_value.query.return_value.filter_by.return_value.\ + first.return_value = None + apibase = config.get_provision_api_base() + + with app.test_client() as client: + # Get one and return 404 + ########################### + configuration_id1 = str(uuid.uuid4()) + resp = client.get(apibase+"/smo-endpoint/"+configuration_id1) + assert resp.status_code == 404 + + +def test_flask_post(mock_flask_uow): + session, app = mock_flask_uow + apibase = config.get_provision_api_base() + + with app.test_client() as client: + session.return_value.execute.return_value = [] + + conf_callback = 'http://registration/callback/url' + resp = client.post(apibase+'/smo-endpoint', json={ + 'endpoint': conf_callback + }) + assert resp.status_code == 201 + assert 'configurationId' in resp.get_json() + + +def test_flask_delete(mock_flask_uow): + session, app = mock_flask_uow + apibase = config.get_provision_api_base() + + with app.test_client() as client: + session.return_value.execute.return_value.first.return_value = {} + + configuration_id1 = str(uuid.uuid4()) + resp = client.delete(apibase+"/smo-endpoint/"+configuration_id1) + assert resp.status_code == 204 + + +def test_flask_not_allowed(mock_flask_uow): + _, app = mock_flask_uow + apibase = config.get_provision_api_base() + + with app.test_client() as client: + + # Testing SMO endpoint not support method + ########################## + uri = apibase + "/smo-endpoint" + resp = client.put(uri) + assert resp.status == '405 METHOD NOT ALLOWED' + resp = client.patch(uri) + assert resp.status == '405 METHOD NOT ALLOWED' + resp = client.delete(uri) + assert resp.status == '405 METHOD NOT ALLOWED' + + configuration_id1 = str(uuid.uuid4()) + uri = apibase + "/smo-endpoint/" + configuration_id1 + resp = client.post(uri) + assert resp.status == '405 METHOD NOT ALLOWED' + resp = client.put(uri) + assert resp.status == '405 METHOD NOT ALLOWED' + resp = client.patch(uri) + assert resp.status == '405 METHOD NOT ALLOWED' -- 2.16.6