Move registration API to configuration 76/7476/3
authorZhang Rong(Jon) <rong.zhang@windriver.com>
Tue, 28 Dec 2021 02:43:54 +0000 (10:43 +0800)
committerZhang Rong(Jon) <rong.zhang@windriver.com>
Tue, 28 Dec 2021 06:42:33 +0000 (14:42 +0800)
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) <rong.zhang@windriver.com>
26 files changed:
docs/installation-guide.rst
o2app/adapter/unit_of_work.py
o2app/entrypoints/flask_application.py
o2app/entrypoints/redis_eventconsumer.py
o2app/service/handlers.py
o2common/config/config.py
o2ims/adapter/ocloud_repository.py
o2ims/adapter/orm.py
o2ims/domain/configuration_obj.py [new file with mode: 0644]
o2ims/domain/configuration_repo.py [new file with mode: 0644]
o2ims/domain/events.py
o2ims/domain/subscription_obj.py
o2ims/domain/subscription_repo.py
o2ims/service/command/registration_handler.py
o2ims/service/event/configuration_event.py [moved from o2ims/service/event/registration_event.py with 86% similarity]
o2ims/views/__init__.py
o2ims/views/api_ns.py [new file with mode: 0644]
o2ims/views/ocloud_dto.py
o2ims/views/ocloud_route.py
o2ims/views/ocloud_view.py
o2ims/views/provision_dto.py [new file with mode: 0644]
o2ims/views/provision_route.py [new file with mode: 0644]
o2ims/views/provision_view.py [new file with mode: 0644]
tests/conftest.py
tests/unit/test_ocloud.py
tests/unit/test_provision.py [new file with mode: 0644]

index 0297aa1..8768ddb 100644 (file)
@@ -164,14 +164,15 @@ The following instruction should be done outside of INF platform controller host
   curl -k http(s)://<OAM IP>: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)://<OAM IP>:30205/provision/smo-endpoint/v1 -d '{"smo-o2-endpoint": "<SMO O2 endpoint for registration>"}'
+  curl -k -X POST http(s)://<OAM IP>:30205/provision/v1/smo-endpoint -d '{"endpoint": "<SMO O2 endpoint for registration>"}'
 
 
 References
index de48001..000d181 100644 (file)
@@ -51,8 +51,8 @@ class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
             .ResourceSqlAlchemyRepository(self.session)\r
         self.subscriptions = ocloud_repository\\r
             .SubscriptionSqlAlchemyRepository(self.session)\r
-        self.registrations = ocloud_repository\\r
-            .RegistrationSqlAlchemyRepository(self.session)\r
+        self.configurations = ocloud_repository\\r
+            .ConfigurationSqlAlchemyRepository(self.session)\r
         self.deployment_managers = ocloud_repository\\r
             .DeploymentManagerSqlAlchemyRepository(self.session)\r
         self.nfdeployment_descs = dms_repository\\r
@@ -93,7 +93,7 @@ class SqlAlchemyUnitOfWork(AbstractUnitOfWork):
         for entry in self.subscriptions.seen:\r
             while hasattr(entry, 'events') and len(entry.events) > 0:\r
                 yield entry.events.pop(0)\r
-        for entry in self.registrations.seen:\r
+        for entry in self.configurations.seen:\r
             while hasattr(entry, 'events') and len(entry.events) > 0:\r
                 yield entry.events.pop(0)\r
         for entry in self.nfdeployment_descs.seen:\r
index f9d5f85..0d1a11a 100644 (file)
@@ -16,7 +16,7 @@ from flask import Flask
 from flask_restx import Api\r
 \r
 from o2app import bootstrap\r
-from o2ims.views import ocloud_route as ims_route\r
+from o2ims.views import configure_namespace as ims_route_configure_namespace\r
 from o2dms.api import configure_namespace as dms_route_configure_namespace\r
 \r
 \r
@@ -30,5 +30,5 @@ api = Api(app, version='1.0.0',
           )\r
 bus = bootstrap.bootstrap()\r
 \r
-ims_route.configure_namespace(api, bus)\r
+ims_route_configure_namespace(api)\r
 dms_route_configure_namespace(api)\r
index d4c7c65..5630174 100644 (file)
@@ -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':
index deef1a4..9e88ff8 100644 (file)
@@ -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]
 
 
index 7207006..8a869da 100644 (file)
@@ -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"
 
index bd14722..8b54f53 100644 (file)
 
 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()
index 29fff79..f59a235 100644 (file)
@@ -34,6 +34,7 @@ from sqlalchemy.orm import mapper, relationship
 \r
 from o2ims.domain import ocloud as ocloudModel\r
 from o2ims.domain import subscription_obj as subModel\r
+from o2ims.domain import configuration_obj as confModel\r
 from o2ims.domain.resource_type import ResourceTypeEnum\r
 \r
 from o2common.helper import o2logging\r
@@ -145,13 +146,14 @@ subscription = Table(
     Column("filter", String(255)),\r
 )\r
 \r
-registration = Table(\r
-    "registration",\r
+configuration = Table(\r
+    "configuration",\r
     metadata,\r
     Column("updatetime", DateTime),\r
     Column("createtime", DateTime),\r
 \r
-    Column("registrationId", String(255), primary_key=True),\r
+    Column("configurationId", String(255), primary_key=True),\r
+    Column("conftype", String(255)),\r
     Column("callback", String(255)),\r
     Column("status", String(255)),\r
     Column("comments", String(255)),\r
@@ -181,7 +183,7 @@ def start_o2ims_mappers(engine=None):
         }\r
     )\r
     mapper(subModel.Subscription, subscription)\r
-    mapper(subModel.Registration, registration)\r
+    mapper(confModel.Configuration, configuration)\r
 \r
     if engine is not None:\r
         metadata.create_all(engine)\r
diff --git a/o2ims/domain/configuration_obj.py b/o2ims/domain/configuration_obj.py
new file mode 100644 (file)
index 0000000..f7b6c6b
--- /dev/null
@@ -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 (file)
index 0000000..9ff95fa
--- /dev/null
@@ -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
index 6f81a84..4858040 100644 (file)
@@ -49,6 +49,6 @@ class ResourceChanged(Event):
 
 
 @dataclass
-class RegistrationChanged(Event):
+class ConfigurationChanged(Event):
     id: str
     updatetime: datetime.now()
index ff8beaf..1746ad9 100644 (file)
@@ -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
index 82df377..d12c00d 100644 (file)
@@ -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
index 0a4395d..d363149 100644 (file)
@@ -23,7 +23,8 @@ from retry import retry
 from o2common.service.unit_of_work import AbstractUnitOfWork\r
 from o2common.config import config\r
 from o2ims.domain import commands\r
-from o2ims.domain.subscription_obj import RegistrationStatusEnum\r
+from o2ims.domain.configuration_obj import ConfigurationTypeEnum, \\r
+    RegistrationStatusEnum\r
 \r
 from o2common.helper import o2logging\r
 logger = o2logging.get_logger(__name__)\r
@@ -37,33 +38,36 @@ def registry_to_smo(
     data = cmd.data\r
     logger.info('The Register2SMO all is {}'.format(data.all))\r
     if data.all:\r
-        regs = uow.registrations.list()\r
-        for reg in regs:\r
-            reg_data = reg.serialize()\r
-            logger.debug('Registration: {}'.format(reg_data['registrationId']))\r
+        confs = uow.configrations.list()\r
+        for conf in confs:\r
+            if conf.conftype != ConfigurationTypeEnum.SMO:\r
+                continue\r
+            reg_data = conf.serialize()\r
+            logger.debug('Configuration: {}'.format(\r
+                reg_data['configurationId']))\r
 \r
             register_smo(uow, reg_data)\r
     else:\r
         with uow:\r
-            reg = uow.registrations.get(data.id)\r
-            if reg is None:\r
+            conf = uow.configurations.get(data.id)\r
+            if conf is None:\r
                 return\r
-            logger.debug('Registration: {}'.format(reg.registrationId))\r
-            reg_data = reg.serialize()\r
-            register_smo(uow, reg_data)\r
+            logger.debug('Configuration: {}'.format(conf.configurationId))\r
+            conf_data = conf.serialize()\r
+            register_smo(uow, conf_data)\r
 \r
 \r
 def register_smo(uow, reg_data):\r
     call_res = call_smo(reg_data)\r
     logger.debug('Call SMO response is {}'.format(call_res))\r
     if call_res:\r
-        reg = uow.registrations.get(reg_data['registrationId'])\r
+        reg = uow.configurations.get(reg_data['configurationId'])\r
         if reg is None:\r
             return\r
         reg.status = RegistrationStatusEnum.NOTIFIED\r
-        logger.debug('Updating Registration: {}'.format(\r
-            reg.registrationId))\r
-        uow.registrations.update(reg)\r
+        logger.debug('Updating Configurations: {}'.format(\r
+            reg.configurationId))\r
+        uow.configurations.update(reg)\r
         uow.commit()\r
 \r
 \r
@@ -82,7 +86,7 @@ def register_smo(uow, reg_data):
 @retry((ConnectionRefusedError), tries=2, delay=2)\r
 def call_smo(reg_data: dict):\r
     callback_data = json.dumps({\r
-        'consumerSubscriptionId': reg_data['registrationId'],\r
+        'consumerSubscriptionId': reg_data['configurationId'],\r
         'imsUrl': config.get_api_url()\r
     })\r
     logger.info('URL: {}, data: {}'.format(\r
similarity index 86%
rename from o2ims/service/event/registration_event.py
rename to o2ims/service/event/configuration_event.py
index e05270e..ddaeb35 100644 (file)
@@ -20,11 +20,11 @@ from o2common.helper import o2logging
 logger = o2logging.get_logger(__name__)\r
 \r
 \r
-def notify_registration_change(\r
-    event: events.RegistrationChanged,\r
+def notify_configuration_change(\r
+    event: events.ConfigurationChanged,\r
     publish: Callable,\r
 ):\r
     logger.info('In notify_registration_change')\r
-    publish("RegistrationChanged", event)\r
+    publish("ConfigurationChanged", event)\r
     logger.debug("published Registration Changed: {}".format(\r
         event.id))\r
index 0647235..a43118b 100644 (file)
 #  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 (file)
index 0000000..955a598
--- /dev/null
@@ -0,0 +1,10 @@
+from flask_restx import Namespace\r
+\r
+\r
+api_ims_inventory_v1 = Namespace(\r
+    "O2IMS_Inventory",\r
+    description='IMS Inventory related operations.')\r
+\r
+api_provision_v1 = Namespace(\r
+    "PROVISION",\r
+    description='Provision related operations.')\r
index 6bf994a..c6896e0 100644 (file)
@@ -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'),
-        }
-    )
index 99adb5f..88b369e 100644 (file)
 
 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/<registrationID>")
-@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)
index ae8b204..3735298 100644 (file)
 #  See the License for the specific language governing permissions and\r
 #  limitations under the License.\r
 \r
-import logging\r
 import uuid\r
-from datetime import datetime\r
 \r
-from o2common.service import unit_of_work, messagebus\r
-from o2ims.domain import events\r
-from o2ims.views.ocloud_dto import RegistrationDTO, SubscriptionDTO\r
-from o2ims.domain.subscription_obj import Registration, Subscription\r
+from o2common.service import unit_of_work\r
+from o2ims.views.ocloud_dto import SubscriptionDTO\r
+from o2ims.domain.subscription_obj import Subscription\r
 \r
 \r
 def oclouds(uow: unit_of_work.AbstractUnitOfWork):\r
@@ -118,51 +115,3 @@ def subscription_delete(subscriptionId: str,
         uow.subscriptions.delete(subscriptionId)\r
         uow.commit()\r
     return True\r
-\r
-\r
-def registrations(uow: unit_of_work.AbstractUnitOfWork):\r
-    with uow:\r
-        li = uow.registrations.list()\r
-    return [r.serialize() for r in li]\r
-\r
-\r
-def registration_one(registrationId: str,\r
-                     uow: unit_of_work.AbstractUnitOfWork):\r
-    with uow:\r
-        first = uow.registrations.get(registrationId)\r
-        return first.serialize() if first is not None else None\r
-\r
-\r
-def registration_create(registrationDto: RegistrationDTO.registration,\r
-                        bus: messagebus.MessageBus):\r
-\r
-    reg_uuid = str(uuid.uuid4())\r
-    registration = Registration(\r
-        reg_uuid, registrationDto['callback'])\r
-    with bus.uow as uow:\r
-        uow.registrations.add(registration)\r
-        logging.debug('before event length {}'.format(\r
-            len(registration.events)))\r
-        registration.events.append(events.RegistrationChanged(\r
-            reg_uuid,\r
-            datetime.now()))\r
-        logging.debug('after event length {}'.format(len(registration.events)))\r
-        uow.commit()\r
-    _handle_events(bus)\r
-    return {"registrationId": reg_uuid}\r
-\r
-\r
-def registration_delete(registrationId: str,\r
-                        uow: unit_of_work.AbstractUnitOfWork):\r
-    with uow:\r
-        uow.registrations.delete(registrationId)\r
-        uow.commit()\r
-    return True\r
-\r
-\r
-def _handle_events(bus: messagebus.MessageBus):\r
-    # handle events\r
-    events = bus.uow.collect_new_events()\r
-    for event in events:\r
-        bus.handle(event)\r
-    return True\r
diff --git a/o2ims/views/provision_dto.py b/o2ims/views/provision_dto.py
new file mode 100644 (file)
index 0000000..f65ac49
--- /dev/null
@@ -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 (file)
index 0000000..7c91e7e
--- /dev/null
@@ -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/<configurationID>")
+@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 (file)
index 0000000..54e5a15
--- /dev/null
@@ -0,0 +1,71 @@
+# Copyright (C) 2021 Wind River Systems, Inc.\r
+#\r
+#  Licensed under the Apache License, Version 2.0 (the "License");\r
+#  you may not use this file except in compliance with the License.\r
+#  You may obtain a copy of the License at\r
+#\r
+#      http://www.apache.org/licenses/LICENSE-2.0\r
+#\r
+#  Unless required by applicable law or agreed to in writing, software\r
+#  distributed under the License is distributed on an "AS IS" BASIS,\r
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+#  See the License for the specific language governing permissions and\r
+#  limitations under the License.\r
+\r
+import logging\r
+import uuid\r
+from datetime import datetime\r
+\r
+from o2common.service import unit_of_work, messagebus\r
+from o2ims.domain import events\r
+from o2ims.views.provision_dto import SmoEndpointDTO\r
+from o2ims.domain.configuration_obj import Configuration, ConfigurationTypeEnum\r
+\r
+\r
+def configurations(uow: unit_of_work.AbstractUnitOfWork):\r
+    with uow:\r
+        li = uow.configurations.list()\r
+    return [r.serialize_smo() for r in li]\r
+\r
+\r
+def configuration_one(configurationId: str,\r
+                      uow: unit_of_work.AbstractUnitOfWork):\r
+    with uow:\r
+        first = uow.configurations.get(configurationId)\r
+        return first.serialize_smo() if first is not None else None\r
+\r
+\r
+def configuration_create(configurationDto: SmoEndpointDTO.endpoint,\r
+                         bus: messagebus.MessageBus):\r
+\r
+    conf_uuid = str(uuid.uuid4())\r
+    configuration = Configuration(\r
+        conf_uuid, configurationDto['endpoint'], ConfigurationTypeEnum.SMO)\r
+    with bus.uow as uow:\r
+        uow.configurations.add(configuration)\r
+        logging.debug('before event length {}'.format(\r
+            len(configuration.events)))\r
+        configuration.events.append(events.ConfigurationChanged(\r
+            conf_uuid,\r
+            datetime.now()))\r
+        logging.debug('after event length {}'.format(\r
+            len(configuration.events)))\r
+        uow.commit()\r
+    _handle_events(bus)\r
+    return {"configurationId": conf_uuid}\r
+\r
+\r
+def configuration_delete(configurationId: str,\r
+                         uow: unit_of_work.AbstractUnitOfWork):\r
+    with uow:\r
+        uow.configurations.delete(configurationId)\r
+        uow.commit()\r
+    return True\r
+\r
+\r
+def _handle_events(bus: messagebus.MessageBus):\r
+    # handle events\r
+    events = bus.uow.collect_new_events()\r
+    for event in events:\r
+        bus.handle(event)\r
+    return True\r
index 9d7f136..a1655b4 100644 (file)
@@ -20,7 +20,7 @@ from o2ims.adapter.orm import metadata, start_o2ims_mappers
 # from o2ims.adapter.clients.orm_stx import start_o2ims_stx_mappers\r
 \r
 from o2app.adapter import unit_of_work\r
-from o2ims.views.ocloud_route import configure_namespace\r
+from o2ims.views import configure_namespace\r
 \r
 from o2app.bootstrap import bootstrap\r
 \r
@@ -38,8 +38,8 @@ def mock_flask_uow(mock_uow):
     app = Flask(__name__)\r
     app.config["TESTING"] = True\r
     api = Api(app)\r
-    bus = bootstrap(False, uow)\r
-    configure_namespace(api, bus)\r
+    bootstrap(False, uow)\r
+    configure_namespace(api)\r
     return session, app\r
 \r
 \r
@@ -75,8 +75,8 @@ def sqlite_flask_uow(sqlite_uow):
     app = Flask(__name__)\r
     app.config["TESTING"] = True\r
     api = Api(app)\r
-    bus = bootstrap(False, sqlite_uow)\r
-    configure_namespace(api, bus)\r
+    bootstrap(False, sqlite_uow)\r
+    configure_namespace(api)\r
     yield sqlite_uow, app\r
 \r
 \r
@@ -135,8 +135,8 @@ def postgres_flask_uow(postgres_uow):
     app = Flask(__name__)\r
     app.config["TESTING"] = True\r
     api = Api(app)\r
-    bus = bootstrap(False, postgres_uow)\r
-    configure_namespace(api, bus)\r
+    bootstrap(False, postgres_uow)\r
+    configure_namespace(api)\r
     yield postgres_uow, app\r
 \r
 \r
index 95dd372..271f5c5 100644 (file)
@@ -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 (file)
index 0000000..3585802
--- /dev/null
@@ -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'