Addition of PaaS 19/12619/1
authorPatricia Reinoso <patricia.reinoso@canonical.com>
Wed, 26 Oct 2022 08:55:54 +0000 (08:55 +0000)
committerPatricia Reinoso <patricia.reinoso@canonical.com>
Wed, 26 Oct 2022 08:55:54 +0000 (08:55 +0000)
Add "paas" topic and operations

Change-Id: Ib3d274d406e42025b4d1f52bf733d7c112e6116c
Signed-off-by: Patricia Reinoso <patricia.reinoso@canonical.com>
osm_nbi/admin_topics.py
osm_nbi/engine.py
osm_nbi/nbi.py
osm_nbi/resources_to_operations.yml
osm_nbi/roles_to_operations.yml
osm_nbi/tests/test_admin_topics.py
osm_nbi/validation.py

index b695693..daeb260 100644 (file)
@@ -37,6 +37,8 @@ from osm_nbi.validation import (
     k8srepo_edit_schema,
     vca_new_schema,
     vca_edit_schema,
+    paas_new_schema,
+    paas_edit_schema,
     osmrepo_new_schema,
     osmrepo_edit_schema,
     validate_input,
@@ -370,14 +372,21 @@ class CommonVimWimSdn(BaseTopic):
         """
         super().format_on_new(content, project_id=project_id, make_public=make_public)
         content["schema_version"] = schema_version = "1.11"
+        self._encrypt_password(content, schema_version)
+        self._encrypt_config_fields(content, schema_version)
+        content["_admin"]["operationalState"] = "PROCESSING"
+        self._insert_create_operation(content)
+        return "{}:0".format(content["_id"])
 
-        # encrypt passwords
+    def _encrypt_password(self, content, schema_version):
         if content.get(self.password_to_encrypt):
             content[self.password_to_encrypt] = self.db.encrypt(
                 content[self.password_to_encrypt],
                 schema_version=schema_version,
                 salt=content["_id"],
             )
+
+    def _encrypt_config_fields(self, content, schema_version):
         config_to_encrypt_keys = self.config_to_encrypt.get(
             schema_version
         ) or self.config_to_encrypt.get("default")
@@ -390,8 +399,7 @@ class CommonVimWimSdn(BaseTopic):
                         salt=content["_id"],
                     )
 
-        content["_admin"]["operationalState"] = "PROCESSING"
-
+    def _insert_create_operation(self, content):
         # create operation
         content["_admin"]["operations"] = [self._create_operation("create")]
         content["_admin"]["current_operation"] = None
@@ -399,50 +407,25 @@ class CommonVimWimSdn(BaseTopic):
         if content.get("vim_type"):
             if content["vim_type"] == "openstack":
                 compute = {
-                    "ram": {
-                        "total": None,
-                        "used": None
-                    },
-                    "vcpus": {
-                        "total": None,
-                        "used": None
-                    },
-                    "instances": {
-                        "total": None,
-                        "used": None
-                    }
+                    "ram": {"total": None, "used": None},
+                    "vcpus": {"total": None, "used": None},
+                    "instances": {"total": None, "used": None},
                 }
                 storage = {
-                    "volumes": {
-                        "total": None,
-                        "used": None
-                    },
-                    "snapshots": {
-                        "total": None,
-                        "used": None
-                    },
-                    "storage": {
-                        "total": None,
-                        "used": None
-                    }
+                    "volumes": {"total": None, "used": None},
+                    "snapshots": {"total": None, "used": None},
+                    "storage": {"total": None, "used": None},
                 }
                 network = {
-                    "networks": {
-                        "total": None,
-                        "used": None
-                    },
-                    "subnets": {
-                        "total": None,
-                        "used": None
-                    },
-                    "floating_ips": {
-                        "total": None,
-                        "used": None
-                    }
+                    "networks": {"total": None, "used": None},
+                    "subnets": {"total": None, "used": None},
+                    "floating_ips": {"total": None, "used": None},
+                }
+                content["resources"] = {
+                    "compute": compute,
+                    "storage": storage,
+                    "network": network,
                 }
-                content["resources"] = {"compute": compute, "storage": storage, "network": network}
-
-        return "{}:0".format(content["_id"])
 
     def delete(self, session, _id, dry_run=False, not_send_msg=None):
         """
@@ -746,6 +729,39 @@ class VcaTopic(CommonVimWimSdn):
         super().check_conflict_on_del(session, _id, db_content)
 
 
+class PaasTopic(CommonVimWimSdn):
+    topic = "paas"
+    topic_msg = "paas"
+    schema_new = paas_new_schema
+    schema_edit = paas_edit_schema
+    multiproject = True
+    password_to_encrypt = "secret"
+    config_to_encrypt = {}
+
+    def format_on_edit(self, final_content, edit_content):
+        oid = super().format_on_edit(final_content, edit_content)
+        final_content["_admin"]["operationalState"] = "PROCESSING"
+        final_content["_admin"]["detailed-status"] = "Editing"
+        return oid
+
+    def _check_if_used_by_ns(self):
+        pass
+
+    def check_conflict_on_del(self, session, _id, db_content):
+        """
+        Check if deletion can be done because of dependencies if it is not force.
+        :param session: contains "username", "admin", "force", "public", "project_id", "set_project"
+        :param _id: internal _id
+        :param db_content: The database content of this item _id
+        :return: None if ok or raises EngineException with the conflict
+        """
+        if session["force"]:
+            return
+        self._check_if_used_by_ns()
+
+        super().check_conflict_on_del(session, _id, db_content)
+
+
 class K8sRepoTopic(CommonVimWimSdn):
     topic = "k8srepos"
     topic_msg = "k8srepo"
@@ -1093,10 +1109,7 @@ class UserTopicAuth(UserTopic):
                     if to_add["project"] in (
                         mapping["project"],
                         mapping["project_name"],
-                    ) and to_add["role"] in (
-                        mapping["role"],
-                        mapping["role_name"],
-                    ):
+                    ) and to_add["role"] in (mapping["role"], mapping["role_name"]):
 
                         if mapping in mappings_to_remove:  # do not remove
                             mappings_to_remove.remove(mapping)
@@ -1113,10 +1126,7 @@ class UserTopicAuth(UserTopic):
                         if to_set["project"] in (
                             mapping["project"],
                             mapping["project_name"],
-                        ) and to_set["role"] in (
-                            mapping["role"],
-                            mapping["role_name"],
-                        ):
+                        ) and to_set["role"] in (mapping["role"], mapping["role_name"]):
                             if mapping in mappings_to_remove:  # do not remove
                                 mappings_to_remove.remove(mapping)
                             break  # do not add, it is already at user
@@ -1129,10 +1139,7 @@ class UserTopicAuth(UserTopic):
                         if to_set["project"] in (
                             mapping["project"],
                             mapping["project_name"],
-                        ) and to_set["role"] in (
-                            mapping["role"],
-                            mapping["role_name"],
-                        ):
+                        ) and to_set["role"] in (mapping["role"], mapping["role_name"]):
                             break
                     else:
                         # delete
index 37f1fb2..1f0308a 100644 (file)
@@ -36,7 +36,7 @@ from osm_nbi.authconn_tacacs import AuthconnTacacs
 from osm_nbi.base_topic import EngineException, versiontuple
 from osm_nbi.admin_topics import VimAccountTopic, WimAccountTopic, SdnTopic
 from osm_nbi.admin_topics import K8sClusterTopic, K8sRepoTopic, OsmRepoTopic
-from osm_nbi.admin_topics import VcaTopic
+from osm_nbi.admin_topics import VcaTopic, PaasTopic
 from osm_nbi.admin_topics import UserTopicAuth, ProjectTopicAuth, RoleTopicAuth
 from osm_nbi.descriptor_topics import (
     VnfdTopic,
@@ -78,6 +78,7 @@ class Engine(object):
         "sdns": SdnTopic,
         "k8sclusters": K8sClusterTopic,
         "vca": VcaTopic,
+        "paas": PaasTopic,
         "k8srepos": K8sRepoTopic,
         "osmrepos": OsmRepoTopic,
         "users": UserTopicAuth,  # Valid for both internal and keystone authentication backends
@@ -293,7 +294,9 @@ class Engine(object):
         :return: The list, it can be empty if no one match the filter_q.
         """
         if topic not in self.map_topic:
-            raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR)
+            raise EngineException(
+                "Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR
+            )
         return self.map_topic[topic].list(session, filter_q, api_req)
 
     def get_item(self, session, topic, _id, filter_q=None, api_req=False):
@@ -307,7 +310,9 @@ class Engine(object):
         :return: dictionary, raise exception if not found.
         """
         if topic not in self.map_topic:
-            raise EngineException("Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR)
+            raise EngineException(
+                "Unknown topic {}!!!".format(topic), HTTPStatus.INTERNAL_SERVER_ERROR
+            )
         return self.map_topic[topic].show(session, _id, filter_q, api_req)
 
     def get_file(self, session, topic, _id, path=None, accept_header=None):
index d78379f..80548ce 100644 (file)
@@ -115,6 +115,8 @@ URL: /osm                                                       GET     POST
                 /<id>                                           O                       O       O
             /k8sclusters                                        O       O
                 /<id>                                           O                       O       O
+            /paas                                               O5      O5
+                /<id>                                           O5                      O5      O5
             /k8srepos                                           O       O
                 /<id>                                           O                               O
             /osmrepos                                           O       O
@@ -288,6 +290,14 @@ valid_url_methods = {
                     "ROLE_PERMISSION": "vca:id:",
                 },
             },
+            "paas": {
+                "METHODS": ("GET", "POST"),
+                "ROLE_PERMISSION": "paas:",
+                "<ID>": {
+                    "METHODS": ("GET", "DELETE", "PATCH"),
+                    "ROLE_PERMISSION": "paas:id:",
+                },
+            },
             "k8srepos": {
                 "METHODS": ("GET", "POST"),
                 "ROLE_PERMISSION": "k8srepos:",
@@ -463,8 +473,8 @@ valid_url_methods = {
                     },
                     "verticalscale": {
                         "METHODS": ("POST",),
-                        "ROLE_PERMISSION": "ns_instances:id:verticalscale:"
-                           },
+                        "ROLE_PERMISSION": "ns_instances:id:verticalscale:",
+                    },
                 },
             },
             "ns_lcm_op_occs": {
@@ -497,33 +507,42 @@ valid_url_methods = {
     },
     "vnflcm": {
         "v1": {
-            "vnf_instances": {"METHODS": ("GET", "POST"),
-                              "ROLE_PERMISSION": "vnflcm_instances:",
-                              "<ID>": {"METHODS": ("GET", "DELETE"),
-                                       "ROLE_PERMISSION": "vnflcm_instances:id:",
-                                       "scale": {"METHODS": ("POST",),
-                                                 "ROLE_PERMISSION": "vnflcm_instances:id:scale:"
-                                                },
-                                       "terminate": {"METHODS": ("POST",),
-                                                     "ROLE_PERMISSION": "vnflcm_instances:id:terminate:"
-                                                    },
-                                       "instantiate": {"METHODS": ("POST",),
-                                                       "ROLE_PERMISSION": "vnflcm_instances:id:instantiate:"
-                                                      },
-                                       }
-                            },
-            "vnf_lcm_op_occs": {"METHODS": ("GET",),
-                               "ROLE_PERMISSION": "vnf_instances:opps:",
-                               "<ID>": {"METHODS": ("GET",),
-                                        "ROLE_PERMISSION": "vnf_instances:opps:id:"
-                                        },
-                               },
-            "subscriptions": {"METHODS": ("GET", "POST"),
-                              "ROLE_PERMISSION": "vnflcm_subscriptions:",
-                              "<ID>": {"METHODS": ("GET", "DELETE"),
-                                       "ROLE_PERMISSION": "vnflcm_subscriptions:id:"
-                                       }
-                              },
+            "vnf_instances": {
+                "METHODS": ("GET", "POST"),
+                "ROLE_PERMISSION": "vnflcm_instances:",
+                "<ID>": {
+                    "METHODS": ("GET", "DELETE"),
+                    "ROLE_PERMISSION": "vnflcm_instances:id:",
+                    "scale": {
+                        "METHODS": ("POST",),
+                        "ROLE_PERMISSION": "vnflcm_instances:id:scale:",
+                    },
+                    "terminate": {
+                        "METHODS": ("POST",),
+                        "ROLE_PERMISSION": "vnflcm_instances:id:terminate:",
+                    },
+                    "instantiate": {
+                        "METHODS": ("POST",),
+                        "ROLE_PERMISSION": "vnflcm_instances:id:instantiate:",
+                    },
+                },
+            },
+            "vnf_lcm_op_occs": {
+                "METHODS": ("GET",),
+                "ROLE_PERMISSION": "vnf_instances:opps:",
+                "<ID>": {
+                    "METHODS": ("GET",),
+                    "ROLE_PERMISSION": "vnf_instances:opps:id:",
+                },
+            },
+            "subscriptions": {
+                "METHODS": ("GET", "POST"),
+                "ROLE_PERMISSION": "vnflcm_subscriptions:",
+                "<ID>": {
+                    "METHODS": ("GET", "DELETE"),
+                    "ROLE_PERMISSION": "vnflcm_subscriptions:id:",
+                },
+            },
         }
     },
     "nst": {
@@ -620,12 +639,14 @@ valid_url_methods = {
     },
     "nsfm": {
         "v1": {
-            "alarms": {"METHODS": ("GET", "PATCH"),
-                       "ROLE_PERMISSION": "alarms:",
-                       "<ID>": {"METHODS": ("GET", "PATCH"),
-                                "ROLE_PERMISSION": "alarms:id:",
-                                },
-                       }
+            "alarms": {
+                "METHODS": ("GET", "PATCH"),
+                "ROLE_PERMISSION": "alarms:",
+                "<ID>": {
+                    "METHODS": ("GET", "PATCH"),
+                    "ROLE_PERMISSION": "alarms:id:",
+                },
+            }
         },
     },
 }
@@ -884,54 +905,87 @@ class Server(object):
 
     # NS Fault Management
     @cherrypy.expose
-    def nsfm(self, version=None, topic=None, uuid=None, project_name=None, ns_id=None, *args, **kwargs):
-        if topic == 'alarms':
+    def nsfm(
+        self,
+        version=None,
+        topic=None,
+        uuid=None,
+        project_name=None,
+        ns_id=None,
+        *args,
+        **kwargs
+    ):
+        if topic == "alarms":
             try:
                 method = cherrypy.request.method
-                role_permission = self._check_valid_url_method(method, "nsfm", version, topic, None, None, *args)
-                query_string_operations = self._extract_query_string_operations(kwargs, method)
+                role_permission = self._check_valid_url_method(
+                    method, "nsfm", version, topic, None, None, *args
+                )
+                query_string_operations = self._extract_query_string_operations(
+                    kwargs, method
+                )
 
-                self.authenticator.authorize(role_permission, query_string_operations, None)
+                self.authenticator.authorize(
+                    role_permission, query_string_operations, None
+                )
 
                 # to handle get request
-                if cherrypy.request.method == 'GET':
+                if cherrypy.request.method == "GET":
                     # if request is on basis of uuid
-                    if uuid and uuid != 'None':
+                    if uuid and uuid != "None":
                         try:
                             alarm = self.engine.db.get_one("alarms", {"uuid": uuid})
-                            alarm_action = self.engine.db.get_one("alarms_action", {"uuid": uuid})
+                            alarm_action = self.engine.db.get_one(
+                                "alarms_action", {"uuid": uuid}
+                            )
                             alarm.update(alarm_action)
-                            vnf = self.engine.db.get_one("vnfrs", {"nsr-id-ref": alarm["tags"]["ns_id"]})
+                            vnf = self.engine.db.get_one(
+                                "vnfrs", {"nsr-id-ref": alarm["tags"]["ns_id"]}
+                            )
                             alarm["vnf-id"] = vnf["_id"]
                             return self._format_out(str(alarm))
                         except Exception:
                             return self._format_out("Please provide valid alarm uuid")
-                    elif ns_id and ns_id != 'None':
+                    elif ns_id and ns_id != "None":
                         # if request is on basis of ns_id
                         try:
-                            alarms = self.engine.db.get_list("alarms", {"tags.ns_id": ns_id})
+                            alarms = self.engine.db.get_list(
+                                "alarms", {"tags.ns_id": ns_id}
+                            )
                             for alarm in alarms:
-                                alarm_action = self.engine.db.get_one("alarms_action", {"uuid": alarm['uuid']})
+                                alarm_action = self.engine.db.get_one(
+                                    "alarms_action", {"uuid": alarm["uuid"]}
+                                )
                                 alarm.update(alarm_action)
                             return self._format_out(str(alarms))
                         except Exception:
                             return self._format_out("Please provide valid ns id")
                     else:
                         # to return only alarm which are related to given project
-                        project = self.engine.db.get_one("projects", {"name": project_name})
-                        project_id = project.get('_id')
-                        ns_list = self.engine.db.get_list("nsrs", {"_admin.projects_read": project_id})
+                        project = self.engine.db.get_one(
+                            "projects", {"name": project_name}
+                        )
+                        project_id = project.get("_id")
+                        ns_list = self.engine.db.get_list(
+                            "nsrs", {"_admin.projects_read": project_id}
+                        )
                         ns_ids = []
                         for ns in ns_list:
                             ns_ids.append(ns.get("_id"))
                         alarms = self.engine.db.get_list("alarms")
-                        alarm_list = [alarm for alarm in alarms if alarm["tags"]["ns_id"] in ns_ids]
+                        alarm_list = [
+                            alarm
+                            for alarm in alarms
+                            if alarm["tags"]["ns_id"] in ns_ids
+                        ]
                         for alrm in alarm_list:
-                            action = self.engine.db.get_one("alarms_action", {"uuid": alrm.get("uuid")})
+                            action = self.engine.db.get_one(
+                                "alarms_action", {"uuid": alrm.get("uuid")}
+                            )
                             alrm.update(action)
                         return self._format_out(str(alarm_list))
                 # to handle patch request for alarm update
-                elif cherrypy.request.method == 'PATCH':
+                elif cherrypy.request.method == "PATCH":
                     data = yaml.load(cherrypy.request.body, Loader=yaml.SafeLoader)
                     try:
                         # check if uuid is valid
@@ -940,24 +994,43 @@ class Server(object):
                         return self._format_out("Please provide valid alarm uuid.")
                     if data.get("is_enable") is not None:
                         if data.get("is_enable"):
-                            alarm_status = 'ok'
+                            alarm_status = "ok"
                         else:
-                            alarm_status = 'disabled'
-                        self.engine.db.set_one("alarms", {"uuid": data.get("uuid")},
-                                               {"alarm_status": alarm_status})
+                            alarm_status = "disabled"
+                        self.engine.db.set_one(
+                            "alarms",
+                            {"uuid": data.get("uuid")},
+                            {"alarm_status": alarm_status},
+                        )
                     else:
-                        self.engine.db.set_one("alarms", {"uuid": data.get("uuid")},
-                                               {"threshold": data.get("threshold")})
+                        self.engine.db.set_one(
+                            "alarms",
+                            {"uuid": data.get("uuid")},
+                            {"threshold": data.get("threshold")},
+                        )
                     return self._format_out("Alarm updated")
             except Exception as e:
                 cherrypy.response.status = e.http_code.value
-                if isinstance(e, (NbiException, EngineException, DbException, FsException, MsgException, AuthException,
-                              ValidationError, AuthconnException)):
+                if isinstance(
+                    e,
+                    (
+                        NbiException,
+                        EngineException,
+                        DbException,
+                        FsException,
+                        MsgException,
+                        AuthException,
+                        ValidationError,
+                        AuthconnException,
+                    ),
+                ):
                     http_code_value = cherrypy.response.status = e.http_code.value
                     http_code_name = e.http_code.name
                     cherrypy.log("Exception {}".format(e))
                 else:
-                    http_code_value = cherrypy.response.status = HTTPStatus.BAD_REQUEST.value  # INTERNAL_SERVER_ERROR
+                    http_code_value = (
+                        cherrypy.response.status
+                    ) = HTTPStatus.BAD_REQUEST.value  # INTERNAL_SERVER_ERROR
                     cherrypy.log("CRITICAL: Exception {}".format(e), traceback=True)
                     http_code_name = HTTPStatus.BAD_REQUEST.name
                 problem_details = {
@@ -1004,10 +1077,11 @@ class Server(object):
             self._format_login(token_info)
             # password expiry check
             if self.authenticator.check_password_expiry(outdata):
-                outdata = {"id": outdata["id"],
-                           "message": "change_password",
-                           "user_id": outdata["user_id"]
-                           }
+                outdata = {
+                    "id": outdata["id"],
+                    "message": "change_password",
+                    "user_id": outdata["user_id"],
+                }
             # cherrypy.response.cookie["Authorization"] = outdata["id"]
             # cherrypy.response.cookie["Authorization"]['expires'] = 3600
         elif method == "DELETE":
@@ -1453,7 +1527,9 @@ class Server(object):
                     filter_q = None
                     if "vcaStatusRefresh" in kwargs:
                         filter_q = {"vcaStatusRefresh": kwargs["vcaStatusRefresh"]}
-                    outdata = self.engine.get_item(engine_session, engine_topic, _id, filter_q, True)
+                    outdata = self.engine.get_item(
+                        engine_session, engine_topic, _id, filter_q, True
+                    )
 
             elif method == "POST":
                 cherrypy.response.status = HTTPStatus.CREATED.value
@@ -1560,8 +1636,12 @@ class Server(object):
                 elif topic == "vnf_instances" and item:
                     indata["lcmOperationType"] = item
                     indata["vnfInstanceId"] = _id
-                    _id, _ = self.engine.new_item(rollback, engine_session, "vnflcmops", indata, kwargs)
-                    self._set_location_header(main_topic, version, "vnf_lcm_op_occs", _id)
+                    _id, _ = self.engine.new_item(
+                        rollback, engine_session, "vnflcmops", indata, kwargs
+                    )
+                    self._set_location_header(
+                        main_topic, version, "vnf_lcm_op_occs", _id
+                    )
                     outdata = {"id": _id}
                     cherrypy.response.status = HTTPStatus.ACCEPTED.value
                 else:
index 3c5ba3d..3c553e0 100644 (file)
@@ -257,6 +257,20 @@ resources_to_operations:
 
   "PATCH /admin/v1/vca/<id>": "vca:id:patch"
 
+################################################################################
+###################################### PAASs ###################################
+################################################################################
+
+  "GET /admin/v1/paas": "paas:get"
+
+  "POST /admin/v1/paas": "paas:post"
+
+  "GET /admin/v1/paas/<id>": "paas:id:get"
+
+  "DELETE /admin/v1/paas/<id>": "paas:id:delete"
+
+  "PATCH /admin/v1/paas/<id>": "paas:id:patch"
+
 ################################################################################
 ################################# K8s Repos ##############################
 ################################################################################
index a97e0c1..4d1ae46 100644 (file)
@@ -140,6 +140,10 @@ roles:
         vca:        false
         vca:get:    true
         vca:id:get: true
+        # PAAS
+        paas:        false
+        paas:get:    true
+        paas:id:get: true
         # K8s repos
         k8srepos:           true
         # OSM repos
index 8124ce4..ae50198 100755 (executable)
@@ -32,6 +32,7 @@ from osm_nbi.admin_topics import (
     UserTopicAuth,
     CommonVimWimSdn,
     VcaTopic,
+    PaasTopic,
 )
 from osm_nbi.engine import EngineException
 from osm_nbi.authconn import AuthconnNotFoundException
@@ -165,6 +166,105 @@ class TestVcaTopic(TestCase):
         mock_check_conflict_on_del.assert_not_called()
 
 
+class TestPaaSTopic(TestCase):
+    def setUp(self):
+        self.db = Mock(dbbase.DbBase())
+        self.fs = Mock(fsbase.FsBase())
+        self.msg = Mock(msgbase.MsgBase())
+        self.auth = Mock(authconn.Authconn(None, None, None))
+        self.paas_topic = PaasTopic(self.db, self.fs, self.msg, self.auth)
+
+    def test_format_on_new(self):
+        content = {
+            "_id": "id",
+            "secret": "secret_to_encrypt",
+        }
+        self.db.encrypt.side_effect = ["encrypted_secret"]
+
+        expecte_oid = "id:0"
+        expected_num_operations = 1
+        oid = self.paas_topic.format_on_new(content)
+
+        self.assertEqual(oid, expecte_oid)
+        self.assertEqual(content["secret"], "encrypted_secret")
+        self.assertEqual(content["_admin"]["operationalState"], "PROCESSING")
+        self.assertEqual(content["_admin"]["current_operation"], None)
+        self.assertEqual(len(content["_admin"]["operations"]), expected_num_operations)
+        self.assertEqual(
+            content["_admin"]["operations"][0]["lcmOperationType"], "create"
+        )
+        self.db.encrypt.assert_called_with(
+            "secret_to_encrypt", schema_version="1.11", salt="id"
+        )
+
+    @patch("osm_nbi.base_topic.BaseTopic._get_project_filter")
+    def test_check_conflict_on_new(self, mock_get_project_filter):
+        indata = {"name": "new_paas_name"}
+        session = {}
+        mock_get_project_filter.return_value = {}
+        self.db.get_one.return_value = None
+        self.paas_topic.check_conflict_on_new(session, indata)
+
+    @patch("osm_nbi.base_topic.BaseTopic._get_project_filter")
+    def test_check_conflict_on_new_raise_exception(self, mock_get_project_filter):
+        indata = {"name": "new_paas_name"}
+        session = {}
+        mock_get_project_filter.return_value = {}
+        self.db.get_one.return_value = ["Found_PaaS"]
+        with self.assertRaises(EngineException):
+            self.paas_topic.check_conflict_on_new(session, indata)
+
+    @patch("osm_nbi.base_topic.BaseTopic._get_project_filter")
+    def test_check_conflict_on_edit(self, mock_get_project_filter):
+        edit_content = {"name": "new_paas_name"}
+        final_content = {}
+        session = {"force": None}
+        mock_get_project_filter.return_value = {}
+        self.db.get_one.return_value = None
+        self.paas_topic.check_conflict_on_edit(
+            session, final_content, edit_content, "id"
+        )
+
+    @patch("osm_nbi.base_topic.BaseTopic._get_project_filter")
+    def test_check_conflict_on_edit_raise_exception(self, mock_get_project_filter):
+        edit_content = {"name": "new_paas_name"}
+        final_content = {}
+        session = {"force": None}
+        mock_get_project_filter.return_value = {}
+        self.db.get_one.return_value = ["Found_PaaS"]
+        with self.assertRaises(EngineException):
+            self.paas_topic.check_conflict_on_edit(
+                session, final_content, edit_content, "id"
+            )
+
+    def test_format_on_edit(self):
+        edit_content = {
+            "_id": "id",
+            "secret": "secret_to_encrypt",
+        }
+        final_content = {
+            "_id": "id",
+            "_admin": {"operations": [{"lcmOperationType": "create"}]},
+            "schema_version": "1.11",
+        }
+        self.db.encrypt.side_effect = ["encrypted_secret"]
+        expected_oid = "id:1"
+        expected_num_operations = 2
+        print(self.paas_topic.password_to_encrypt)
+        oid = self.paas_topic.format_on_edit(final_content, edit_content)
+
+        self.assertEqual(oid, expected_oid)
+        self.assertEqual(final_content["secret"], "encrypted_secret")
+        self.assertEqual(
+            len(final_content["_admin"]["operations"]), expected_num_operations
+        )
+        self.assertEqual(final_content["_admin"]["operationalState"], "PROCESSING")
+        self.assertEqual(final_content["_admin"]["detailed-status"], "Editing")
+        self.db.encrypt.assert_called_with(
+            "secret_to_encrypt", schema_version="1.11", salt="id"
+        )
+
+
 class Test_ProjectTopicAuth(TestCase):
     @classmethod
     def setUpClass(cls):
@@ -1013,7 +1113,9 @@ class Test_UserTopicAuth(TestCase):
                 },
             )
             content = self.auth.update_user.call_args[0][0]
-            self.assertEqual(content["old_password"], old_password, "Wrong old password")
+            self.assertEqual(
+                content["old_password"], old_password, "Wrong old password"
+            )
             self.assertEqual(content["password"], new_pasw, "Wrong user password")
 
     def test_delete_user(self):
index a17e241..32a2d3e 100644 (file)
@@ -449,7 +449,12 @@ ns_update = {
         "nsInstanceId": id_schema,
         "timeout_ns_update": integer1_schema,
         "updateType": {
-            "enum": ["CHANGE_VNFPKG", "REMOVE_VNF", "MODIFY_VNF_INFORMATION", "OPERATE_VNF"]
+            "enum": [
+                "CHANGE_VNFPKG",
+                "REMOVE_VNF",
+                "MODIFY_VNF_INFORMATION",
+                "OPERATE_VNF",
+            ]
         },
         "modifyVnfInfoData": {
             "type": "object",
@@ -482,10 +487,10 @@ ns_update = {
                     },
                     "required": ["vdu_id", "count-index"],
                     "additionalProperties": False,
-                }
+                },
             },
             "required": ["vnfInstanceId", "changeStateTo"],
-        }
+        },
     },
     "required": ["updateType"],
     "additionalProperties": False,
@@ -556,16 +561,16 @@ ns_migrate = {
         "migrateToHost": string_schema,
         "vdu": {
             "type": "object",
-                "properties": {
-                    "vduId": name_schema,
-                    "vduCountIndex": integer0_schema,
-                },
-                "required": ["vduId"],
-                "additionalProperties": False,
+            "properties": {
+                "vduId": name_schema,
+                "vduCountIndex": integer0_schema,
+            },
+            "required": ["vduId"],
+            "additionalProperties": False,
         },
     },
     "required": ["vnfInstanceId"],
-    "additionalProperties": False
+    "additionalProperties": False,
 }
 
 ns_heal = {
@@ -633,13 +638,13 @@ ns_verticalscale = {
                         "virtualMemory": integer1_schema,
                         "sizeOfStorage": integer0_schema,
                         "numVirtualCpu": integer1_schema,
-                        },
-                    }
+                    },
                 },
+            },
             "required": ["vnfInstanceId", "additionalParams"],
             "additionalProperties": False,
-            }
         },
+    },
     "required": ["lcmOperationType", "verticalScale", "nsInstanceId"],
     "additionalProperties": False,
 }
@@ -926,6 +931,48 @@ vca_edit_schema = {
     "additionalProperties": False,
 }
 
+# PAAS
+paas_types = {"enum": ["juju"]}
+paas_new_schema = {
+    "title": "paas creation input schema",
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "type": "object",
+    "properties": {
+        "schema_version": schema_version,
+        "schema_type": schema_type,
+        "name": name_schema,
+        "paas_type": paas_types,
+        "description": description_schema,
+        "endpoints": description_list_schema,
+        "user": string_schema,
+        "secret": passwd_schema,
+        "config": object_schema,
+    },
+    "required": [
+        "name",
+        "paas_type",
+        "endpoints",
+        "user",
+        "secret",
+    ],
+    "additionalProperties": False,
+}
+paas_edit_schema = {
+    "title": "paas edition input schema",
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "type": "object",
+    "properties": {
+        "name": name_schema,
+        "paas_type": paas_types,
+        "description": description_schema,
+        "endpoints": description_list_schema,
+        "user": string_schema,
+        "secret": passwd_schema,
+        "config": object_schema,
+    },
+    "additionalProperties": False,
+}
+
 # K8s Repos
 k8srepo_types = {"enum": ["helm-chart", "juju-bundle"]}
 k8srepo_properties = {
@@ -1126,6 +1173,7 @@ topics_with_quota = [
     "sdn_controllers",
     "k8sclusters",
     "vca",
+    "paas",
     "k8srepos",
     "osmrepos",
     "ns_subscriptions",
@@ -1436,32 +1484,46 @@ vnflcmsub_schema = {
                 "enum": [
                     "VnfIdentifierCreationNotification",
                     "VnfLcmOperationOccurrenceNotification",
-                    "VnfIdentifierDeletionNotification"
-                    ]
-            }
+                    "VnfIdentifierDeletionNotification",
+                ]
+            },
         },
         "operationTypes": {
             "type": "array",
             "items": {
                 "enum": [
-                    "INSTANTIATE", "SCALE", "SCALE_TO_LEVEL", "CHANGE_FLAVOUR", "TERMINATE",
-                    "HEAL", "OPERATE", "CHANGE_EXT_CONN", "MODIFY_INFO", "CREATE_SNAPSHOT",
-                    "REVERT_TO_SNAPSHOT", "CHANGE_VNFPKG"
-                    ]
-            }
+                    "INSTANTIATE",
+                    "SCALE",
+                    "SCALE_TO_LEVEL",
+                    "CHANGE_FLAVOUR",
+                    "TERMINATE",
+                    "HEAL",
+                    "OPERATE",
+                    "CHANGE_EXT_CONN",
+                    "MODIFY_INFO",
+                    "CREATE_SNAPSHOT",
+                    "REVERT_TO_SNAPSHOT",
+                    "CHANGE_VNFPKG",
+                ]
+            },
         },
         "operationStates": {
             "type": "array",
             "items": {
                 "enum": [
-                    "STARTING", "PROCESSING", "COMPLETED", "FAILED_TEMP", "FAILED",
-                    "ROLLING_BACK", "ROLLED_BACK"
-                    ]
-            }
-        }
+                    "STARTING",
+                    "PROCESSING",
+                    "COMPLETED",
+                    "FAILED_TEMP",
+                    "FAILED",
+                    "ROLLING_BACK",
+                    "ROLLED_BACK",
+                ]
+            },
+        },
     },
-    "required": ["VnfInstanceSubscriptionFilter", "notificationTypes"]
- }
+    "required": ["VnfInstanceSubscriptionFilter", "notificationTypes"],
+}
 
 vnf_subscription = {
     "title": "vnf subscription input schema",
@@ -1470,9 +1532,9 @@ vnf_subscription = {
     "properties": {
         "filter": vnflcmsub_schema,
         "CallbackUri": description_schema,
-        "authentication": authentication_schema
+        "authentication": authentication_schema,
     },
-    "required": ["filter", "CallbackUri"]
+    "required": ["filter", "CallbackUri"],
 }