Addition of PaaS 17/12617/1
authorPatricia Reinoso <patricia.reinoso@canonical.com>
Wed, 26 Oct 2022 08:45:49 +0000 (08:45 +0000)
committerPatricia Reinoso <patricia.reinoso@canonical.com>
Wed, 26 Oct 2022 08:45:49 +0000 (08:45 +0000)
Add the following commands to the OSM Client:
 - osm paas-add
 - osm paas-delete
 - osm paas-update
 - osm paas-list
 - osm paas-show

Change-Id: I5a0ba86c2b3a6a9239c2f336c33a0777cc72654b
Signed-off-by: Patricia Reinoso <patricia.reinoso@canonical.com>
osmclient/scripts/osm.py
osmclient/scripts/tests/test_paas_operations.py [new file with mode: 0644]
osmclient/sol005/client.py
osmclient/sol005/paas.py [new file with mode: 0644]
osmclient/sol005/tests/test_paas.py [new file with mode: 0644]

index ab56a07..de121b1 100755 (executable)
@@ -3879,16 +3879,10 @@ def sdnc_show(ctx, name):
     accessible via L3 routing, e.g. "{(k8s_net1:vim_network1) [,(k8s_net2:vim_network2) ...]}"''',
 )
 @click.option(
-    "--init-helm2/--skip-helm2",
-    required=False,
-    default=True,
-    help="Initialize helm v2",
+    "--init-helm2/--skip-helm2", required=False, default=True, help="Initialize helm v2"
 )
 @click.option(
-    "--init-helm3/--skip-helm3",
-    required=False,
-    default=True,
-    help="Initialize helm v3",
+    "--init-helm3/--skip-helm3", required=False, default=True, help="Initialize helm v3"
 )
 @click.option(
     "--init-jujubundle/--skip-jujubundle",
@@ -4208,11 +4202,7 @@ def k8scluster_show(ctx, name, literal):
     prompt=True,
     help="Name of the cloud credentialsto be used for the K8s cloud",
 )
-@click.option(
-    "--model-config",
-    default={},
-    help="Configuration options for the models",
-)
+@click.option("--model-config", default={}, help="Configuration options for the models")
 @click.option("--description", default=None, help="human readable description")
 @click.pass_context
 def vca_add(
@@ -4301,10 +4291,7 @@ def load_file(file_path: str) -> Dict:
     "--k8s-credentials",
     help="Name of the cloud credentialsto be used for the K8s cloud",
 )
-@click.option(
-    "--model-config",
-    help="Configuration options for the models",
-)
+@click.option("--model-config", help="Configuration options for the models")
 @click.option("--description", default=None, help="human readable description")
 @click.pass_context
 def vca_update(
@@ -4440,6 +4427,193 @@ def vca_show(ctx, name, literal):
     print(table)
 
 
+###########################
+# PaaS operations
+###########################
+
+
+@cli_osm.command(name="paas-add", short_help="adds a PaaS to OSM.")
+@click.argument("name")
+@click.option(
+    "--paas_type",
+    type=click.Choice(["juju"]),
+    default="juju",
+    prompt=True,
+    help="Type of PaaS that can be used. (For the moment, only juju is supported).",
+)
+@click.option(
+    "--endpoints",
+    prompt=True,
+    help="Comma-separated list of IP or hostnames of the PaaS.",
+)
+@click.option("--user", prompt=True, help="Username with admin priviledges.")
+@click.option("--secret", prompt=True, help="Password of the specified username.")
+@click.option(
+    "--config", default={}, help="Extra configuration needed by PaaS service."
+)
+@click.option("--description", default=None, help="Human readable description.")
+@click.pass_context
+def paas_add(ctx, name, paas_type, endpoints, user, secret, config, description):
+    """adds a PaaS to OSM.
+    Args:
+        name (str): Name of the new PaaS.
+    """
+    check_client_version(ctx.obj, ctx.command.name)
+    paas = {
+        "name": name,
+        "paas_type": paas_type,
+        "endpoints": endpoints.split(","),
+        "user": user,
+        "secret": secret,
+    }
+    if description:
+        paas["description"] = description
+    if config:
+        config = load(config)
+        paas["config"] = config
+    ctx.obj.paas.create(paas)
+
+
+@cli_osm.command(name="paas-update", short_help="updates a PaaS")
+@click.argument("name")
+@click.option("--newname", help="New name for the PaaS")
+@click.option(
+    "--paas_type",
+    type=click.Choice(["juju"]),
+    help="Type of PaaS that can be used. (For the moment, only juju is supported)",
+)
+@click.option(
+    "--endpoints", help="Comma-separated list of IP or hostnames of the Juju controller"
+)
+@click.option("--user", help="Username with admin priviledges")
+@click.option("--secret", help="Password of the specified username")
+@click.option("--config", help="Extra configuration needed by PaaS service")
+@click.option("--description", default=None, help="Human readable description")
+@click.pass_context
+def paas_update(
+    ctx, name, newname, paas_type, endpoints, user, secret, config, description
+):
+    """updates a PaaS.
+    Args:
+        name (str): Name or ID of the PaaS to update.
+    """
+    check_client_version(ctx.obj, ctx.command.name)
+    paas = {}
+    if newname:
+        paas["name"] = newname
+    if paas_type:
+        paas["paas_type"] = paas_type
+    if endpoints:
+        paas["endpoints"] = endpoints.split(",")
+    if user:
+        paas["user"] = user
+    if secret:
+        paas["secret"] = secret
+    if description:
+        paas["description"] = description
+    if config:
+        config = load(config)
+        paas["config"] = config
+    ctx.obj.paas.update(name, paas)
+
+
+@cli_osm.command(name="paas-delete", short_help="deletes a PaaS")
+@click.argument("name")
+@click.option(
+    "--force", is_flag=True, help="forces the deletion from the DB (not recommended)"
+)
+@click.pass_context
+def paas_delete(ctx, name, force):
+    """deletes a PaaS.
+
+    Args:
+        name (str): Name or ID of the PaaS to delete.
+    """
+    check_client_version(ctx.obj, ctx.command.name)
+    ctx.obj.paas.delete(name, force=force)
+
+
+@cli_osm.command(name="paas-list")
+@click.option(
+    "--filter",
+    default=None,
+    multiple=True,
+    help="Restricts the list to the PaaS matching the filter",
+)
+@click.option("--literal", is_flag=True, help="Print literally, no pretty table")
+@click.option("--long", is_flag=True, help="get more details")
+@click.pass_context
+def paas_list(ctx, filter, literal, long):
+    """List PaaSs"""
+    check_client_version(ctx.obj, ctx.command.name)
+    if filter:
+        filter = "&".join(filter)
+    resp = ctx.obj.paas.list(filter)
+    if literal:
+        print(yaml.safe_dump(resp, indent=4, default_flow_style=False))
+        return
+
+    table = _get_paas_table_header(long)
+    project_list = ctx.obj.project.list()
+    for paas in resp:
+        logger.debug("PaaS details: {}".format(yaml.safe_dump(paas)))
+        if long:
+            _add_paas_long_row(table, paas, project_list)
+        else:
+            _add_paas_row(table, paas)
+    table.align = "l"
+    print(table)
+
+
+def _get_paas_table_header(long):
+    if long:
+        return PrettyTable(
+            ["Name", "Id", "Project", "Operational State", "Detailed Status"]
+        )
+    return PrettyTable(["Name", "Id", "Operational State"])
+
+
+def _add_paas_long_row(table, paas, project_list):
+    _, project_name = get_project(project_list, paas)
+    detailed_status = paas.get("_admin", {}).get("detailed-status", "-")
+    table.add_row(
+        [
+            paas["name"],
+            paas["_id"],
+            project_name,
+            paas.get("_admin", {}).get("operationalState", "-"),
+            wrap_text(text=detailed_status, width=40),
+        ]
+    )
+
+
+def _add_paas_row(table, paas):
+    table.add_row(
+        [paas["name"], paas["_id"], paas.get("_admin", {}).get("operationalState", "-")]
+    )
+
+
+@cli_osm.command(name="paas-show", short_help="Shows the details of a PaaS")
+@click.argument("name")
+@click.option("--literal", is_flag=True, help="Print literally, no pretty table")
+@click.pass_context
+def paas_show(ctx, name, literal):
+    """Shows the details of a PaaS.
+
+    Args:
+        name (str): Name or ID of the PaaS to show.
+    """
+    resp = ctx.obj.paas.get(name)
+    if literal:
+        print(yaml.safe_dump(resp, indent=4, default_flow_style=False))
+        return
+    table = PrettyTable(["key", "attribute"])
+    for k, v in list(resp.items()):
+        table.add_row([k, wrap_text(text=json.dumps(v, indent=2), width=100)])
+    table.align = "l"
+    print(table)
+
+
 ###########################
 # Repo operations
 ###########################
@@ -5634,10 +5808,7 @@ def update(ctx, ns_name, updatetype, config, timeout, wait):
     NS_NAME: Network service instance name or ID.
 
     """
-    op_data = {
-        "timeout": timeout,
-        "updateType": updatetype,
-    }
+    op_data = {"timeout": timeout, "updateType": updatetype}
     if config:
         op_data["config"] = yaml.safe_load(config)
 
@@ -5769,16 +5940,11 @@ def process_ns_heal_params(ctx, param, value):
 @cli_osm.command(
     name="ns-heal",
     short_help="heals (recreates) VNFs or VDUs of a NS instance",
-    context_settings=dict(
-        ignore_unknown_options=True,
-    ),
+    context_settings=dict(ignore_unknown_options=True),
 )
 @click.argument("ns_name")
 @click.argument(
-    "args",
-    nargs=-1,
-    type=click.UNPROCESSED,
-    callback=process_ns_heal_params,
+    "args", nargs=-1, type=click.UNPROCESSED, callback=process_ns_heal_params
 )
 @click.option("--timeout", type=int, default=None, help="timeout in seconds")
 @click.option(
@@ -5861,16 +6027,11 @@ def process_vnf_heal_params(ctx, param, value):
 @cli_osm.command(
     name="vnf-heal",
     short_help="heals (recreates) a VNF instance or the VDUs of a VNF instance",
-    context_settings=dict(
-        ignore_unknown_options=True,
-    ),
+    context_settings=dict(ignore_unknown_options=True),
 )
 @click.argument("vnf_name")
 @click.argument(
-    "args",
-    nargs=-1,
-    type=click.UNPROCESSED,
-    callback=process_vnf_heal_params,
+    "args", nargs=-1, type=click.UNPROCESSED, callback=process_vnf_heal_params
 )
 @click.option("--timeout", type=int, default=None, help="timeout in seconds")
 @click.option(
@@ -5880,14 +6041,7 @@ def process_vnf_heal_params(ctx, param, value):
     help="do not return the control immediately, but keep it until the operation is completed, or timeout",
 )
 @click.pass_context
-def vnf_heal2(
-    ctx,
-    vnf_name,
-    args,
-    heal_params,
-    timeout,
-    wait,
-):
+def vnf_heal2(ctx, vnf_name, args, heal_params, timeout, wait):
     """heals (recreates) a VNF instance or the VDUs of a VNF instance
 
     VNF_NAME: name or ID of the VNF instance
@@ -6426,7 +6580,7 @@ def cli():
         print(
             'Maybe "--hostname" option or OSM_HOSTNAME environment variable needs to be specified'
         )
-    except ClientException as exc:
+    except (ClientException, NotFound) as exc:
         print("ERROR: {}".format(exc))
     except (FileNotFoundError, PermissionError) as exc:
         print("Cannot open file: {}".format(exc))
diff --git a/osmclient/scripts/tests/test_paas_operations.py b/osmclient/scripts/tests/test_paas_operations.py
new file mode 100644 (file)
index 0000000..9edd017
--- /dev/null
@@ -0,0 +1,312 @@
+# Copyright 2021 Canonical Ltd.
+#
+# 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 json
+import unittest
+from unittest.mock import Mock, patch
+import yaml
+
+from click.testing import CliRunner
+from osmclient.common.exceptions import NotFound
+from osmclient.scripts import osm
+
+
+@patch("builtins.print")
+@patch("osmclient.scripts.osm.PrettyTable")
+@patch("osmclient.scripts.osm.client.Client")
+@patch("osmclient.scripts.osm.check_client_version")
+@patch("osmclient.scripts.osm.get_project")
+class TestPaaS(unittest.TestCase):
+    def setUp(self):
+        self.runner = CliRunner()
+        self.ctx_obj = Mock()
+        self.table = Mock()
+        self.paas_data = {
+            "name": "name",
+            "_id": "1234",
+            "_admin": {"detailed-status": "status", "operationalState": "state"},
+        }
+
+    def test_paas_add(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        self.runner.invoke(
+            osm.cli_osm,
+            [
+                "paas-add",
+                "name",
+                "--paas_type",
+                "juju",
+                "--endpoints",
+                "1.2.3.4:17070",
+                "--user",
+                "user",
+                "--secret",
+                "secret",
+                "--description",
+                "description",
+                "--config",
+                json.dumps({"juju-https-proxy": "http://squid:3128"}),
+            ],
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.paas.create.assert_called_with(
+            {
+                "name": "name",
+                "paas_type": "juju",
+                "endpoints": ["1.2.3.4:17070"],
+                "user": "user",
+                "secret": "secret",
+                "description": "description",
+                "config": {"juju-https-proxy": "http://squid:3128"},
+            }
+        )
+        mock_pretty_table.assert_not_called()
+        self.table.add_row.assert_not_called()
+        mock_print.assert_not_called()
+
+    def test_paas_update(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        self.runner.invoke(osm.cli_osm, ["paas-update", "name"])
+        mock_check_client_version.assert_called()
+        self.ctx_obj.paas.update.assert_called_with("name", {})
+        mock_pretty_table.assert_not_called()
+        self.table.add_row.assert_not_called()
+        mock_print.assert_not_called()
+
+    def test_paas_update_with_args(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        self.runner.invoke(
+            osm.cli_osm,
+            [
+                "paas-update",
+                "name",
+                "--newname",
+                "paas_new_name",
+                "--paas_type",
+                "juju",
+                "--endpoints",
+                "1.2.3.4:17070",
+                "--user",
+                "user",
+                "--secret",
+                "secret",
+                "--description",
+                "description",
+                "--config",
+                json.dumps({"juju-https-proxy": "http://squid:3128"}),
+            ],
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.paas.update.assert_called_with(
+            "name",
+            {
+                "name": "paas_new_name",
+                "paas_type": "juju",
+                "endpoints": ["1.2.3.4:17070"],
+                "user": "user",
+                "secret": "secret",
+                "description": "description",
+                "config": {"juju-https-proxy": "http://squid:3128"},
+            },
+        )
+        mock_pretty_table.assert_not_called()
+        self.table.add_row.assert_not_called()
+        mock_print.assert_not_called()
+
+    def test_paas_delete(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        self.runner.invoke(osm.cli_osm, ["paas-delete", "name"])
+        mock_check_client_version.assert_called()
+        self.ctx_obj.paas.delete.assert_called_with("name", force=False)
+        mock_pretty_table.assert_not_called()
+        self.table.add_row.assert_not_called()
+        mock_print.assert_not_called()
+
+    def test_paas_delete_force(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        self.runner.invoke(osm.cli_osm, ["paas-delete", "name", "--force"])
+        mock_check_client_version.assert_called()
+        self.ctx_obj.paas.delete.assert_called_with("name", force=True)
+        mock_pretty_table.assert_not_called()
+        self.table.add_row.assert_not_called()
+        mock_print.assert_not_called()
+
+    def test_paas_list(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        mock_pretty_table.return_value = self.table
+
+        self.ctx_obj.paas.list.return_value = [self.paas_data]
+        self.runner.invoke(osm.cli_osm, ["paas-list", "--filter", "somefilter"])
+        mock_check_client_version.assert_called()
+        self.ctx_obj.paas.list.assert_called_with("somefilter")
+        mock_pretty_table.assert_called_with(["Name", "Id", "Operational State"])
+        mock_get_project.assert_not_called()
+        self.table.add_row.assert_called_with(["name", "1234", "state"])
+        mock_print.assert_called_with(self.table)
+
+    def test_paas_list_long(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        mock_pretty_table.return_value = self.table
+        mock_get_project.return_value = ("5678", "project_name")
+        self.ctx_obj.paas.list.return_value = [self.paas_data]
+        self.runner.invoke(
+            osm.cli_osm, ["paas-list", "--filter", "somefilter", "--long"]
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.paas.list.assert_called_with("somefilter")
+        mock_pretty_table.assert_called_with(
+            ["Name", "Id", "Project", "Operational State", "Detailed Status"]
+        )
+        self.table.add_row.assert_called_with(
+            ["name", "1234", "project_name", "state", "status"]
+        )
+        mock_print.assert_called_with(self.table)
+
+    def test_paas_list_literal(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        self.ctx_obj.paas.list.return_value = [self.paas_data]
+        self.runner.invoke(osm.cli_osm, ["paas-list", "--literal"])
+        mock_check_client_version.assert_called()
+        self.ctx_obj.paas.list.assert_called()
+        mock_pretty_table.assert_not_called()
+        self.table.add_row.assert_not_called()
+        mock_print.assert_called_with(
+            yaml.safe_dump([self.paas_data], indent=4, default_flow_style=False)
+        )
+
+    def test_paas_list_empty(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        mock_pretty_table.return_value = self.table
+        self.ctx_obj.paas.list.return_value = []
+        self.runner.invoke(osm.cli_osm, ["paas-list", "--filter", "somefilter"])
+        mock_check_client_version.assert_called()
+        self.ctx_obj.paas.list.assert_called_with("somefilter")
+        mock_get_project.assert_not_called()
+        mock_pretty_table.assert_called_with(["Name", "Id", "Operational State"])
+        mock_print.assert_called_with(self.table)
+
+    def test_paas_show(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        mock_pretty_table.return_value = self.table
+        self.ctx_obj.paas.get.return_value = self.paas_data
+        self.runner.invoke(osm.cli_osm, ["paas-show", "name"])
+        self.ctx_obj.paas.get.assert_called_with("name")
+        mock_pretty_table.assert_called_with(["key", "attribute"])
+        self.assertEqual(self.table.add_row.call_count, len(self.paas_data))
+        mock_print.assert_called_with(self.table)
+
+    def test_paas_show_literal(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        self.ctx_obj.paas.get.return_value = self.paas_data
+        self.runner.invoke(osm.cli_osm, ["paas-show", "name", "--literal"])
+        self.ctx_obj.paas.get.assert_called_with("name")
+        mock_pretty_table.assert_not_called()
+        self.table.add_row.assert_not_called()
+        mock_print.assert_called_with(
+            yaml.safe_dump(self.paas_data, indent=4, default_flow_style=False)
+        )
+
+    def test_paas_show_literal_throws_exception(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        mock_client.return_value = self.ctx_obj
+        self.ctx_obj.paas.get.side_effect = NotFound()
+        self.runner.invoke(osm.cli_osm, ["paas-show", "name", "--literal"])
+        self.ctx_obj.paas.get.assert_called_with("name")
+        mock_pretty_table.assert_not_called()
+        self.table.add_row.assert_not_called()
+        mock_print.assert_not_called()
index a69f3cc..5dd6f06 100644 (file)
@@ -36,6 +36,7 @@ from osmclient.sol005 import role
 from osmclient.sol005 import pdud
 from osmclient.sol005 import k8scluster
 from osmclient.sol005 import vca
+from osmclient.sol005 import paas
 from osmclient.sol005 import repo
 from osmclient.sol005 import osmrepo
 from osmclient.sol005 import subscription
@@ -99,6 +100,7 @@ class Client(object):
         self.pdu = pdud.Pdu(self._http_client, client=self)
         self.k8scluster = k8scluster.K8scluster(self._http_client, client=self)
         self.vca = vca.VCA(self._http_client, client=self)
+        self.paas = paas.PAAS(self._http_client, client=self)
         self.repo = repo.Repo(self._http_client, client=self)
         self.osmrepo = osmrepo.OSMRepo(self._http_client, client=self)
         self.package_tool = package_tool.PackageTool(client=self)
diff --git a/osmclient/sol005/paas.py b/osmclient/sol005/paas.py
new file mode 100644 (file)
index 0000000..3dcadf6
--- /dev/null
@@ -0,0 +1,146 @@
+# Copyright 2021 Canonical Ltd.
+#
+# 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.
+
+"""
+OSM PaaS API handling
+"""
+
+from osmclient.common import utils
+from osmclient.common.exceptions import ClientException, NotFound
+import json
+
+
+class PAAS(object):
+    def __init__(self, http=None, client=None):
+        self._http = http
+        self._client = client
+        self._apiName = "/admin"
+        self._apiVersion = "/v1"
+        self._apiResource = "/paas"
+        self._apiBase = "{}{}{}".format(
+            self._apiName, self._apiVersion, self._apiResource
+        )
+
+    def _is_id(self, name):
+        return utils.validate_uuid4(name)
+
+    def create(self, paas):
+        """Create PaaS.
+        Args:
+            paas (dict): includes the PaaS information.
+
+        Raises:
+            ClientException
+        """
+        self._client.get_token()
+        http_code, resp = self._http.post_cmd(
+            endpoint=self._apiBase, postfields_dict=paas
+        )
+        resp = json.loads(resp) if resp else {}
+        if "id" not in resp:
+            raise ClientException("unexpected response from server - {}".format(resp))
+        print("PaaS {} created with ID {}".format(paas["name"], resp["id"]))
+
+    def update(self, name, paas):
+        """Updates a PaaS based on name or ID.
+        Args:
+            name (str): PaaS name or ID.
+            paas (dict): includes the new PaaS information to update.
+
+        Raises:
+            NotFound: if PaaS does not exists in DB.
+        """
+        paas_id = name
+        self._client.get_token()
+        try:
+            if not self._is_id(name):
+                paas_id = self.get(name)["_id"]
+            self._http.patch_cmd(
+                endpoint="{}/{}".format(self._apiBase, paas_id), postfields_dict=paas
+            )
+        except NotFound:
+            raise NotFound("PaaS {} not found".format(name))
+
+    def get_id(self, name):
+        """Returns a PaaS ID from a PaaS name.
+        Args:
+            name (str): PaaS name.
+        Raises:
+            NotFound: if PaaS does not exists in DB.
+        """
+        for paas in self.list():
+            if name == paas["name"]:
+                return paas["_id"]
+        raise NotFound("PaaS {} not found".format(name))
+
+    def delete(self, name, force=False):
+        """Deletes a PaaS based on name or ID.
+        Args:
+            name (str): PaaS name or ID.
+            force (bool): if True, PaaS is deleted without any check.
+        Raises:
+            NotFound: if PaaS does not exists in DB.
+            ClientException: if delete fails.
+        """
+        self._client.get_token()
+        paas_id = name
+        querystring = "?FORCE=True" if force else ""
+        try:
+            if not self._is_id(name):
+                paas_id = self.get_id(name)
+            http_code, resp = self._http.delete_cmd(
+                "{}/{}{}".format(self._apiBase, paas_id, querystring)
+            )
+        except NotFound:
+            raise NotFound("PaaS {} not found".format(name))
+        if http_code == 202:
+            print("Deletion in progress")
+        elif http_code == 204:
+            print("Deleted")
+        else:
+            msg = resp or ""
+            raise ClientException("Failed to delete PaaS {} - {}".format(name, msg))
+
+    def list(self, cmd_filter=None):
+        """Returns a list of PaaS"""
+        self._client.get_token()
+        filter_string = ""
+        if cmd_filter:
+            filter_string = "?{}".format(cmd_filter)
+        _, resp = self._http.get2_cmd("{}{}".format(self._apiBase, filter_string))
+        if resp:
+            return json.loads(resp)
+        return list()
+
+    def get(self, name):
+        """Returns a PaaS based on name or id.
+        Args:
+            name (str): PaaS name or ID.
+
+        Raises:
+            NotFound: if PaaS does not exists in DB.
+            ClientException: if get fails.
+        """
+        self._client.get_token()
+        paas_id = name
+        try:
+            if not self._is_id(name):
+                paas_id = self.get_id(name)
+            _, resp = self._http.get2_cmd("{}/{}".format(self._apiBase, paas_id))
+            resp = json.loads(resp) if resp else {}
+            if "_id" not in resp:
+                raise ClientException("Failed to get PaaS info: {}".format(resp))
+            return resp
+        except NotFound:
+            raise NotFound("PaaS {} not found".format(name))
diff --git a/osmclient/sol005/tests/test_paas.py b/osmclient/sol005/tests/test_paas.py
new file mode 100644 (file)
index 0000000..de7bcd3
--- /dev/null
@@ -0,0 +1,267 @@
+# Copyright 2021 Canonical Ltd.
+#
+# 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 json
+import unittest
+from unittest.mock import Mock, patch
+
+from osmclient.common.exceptions import ClientException, NotFound
+from osmclient.sol005.paas import PAAS
+
+
+class TestPaaS(unittest.TestCase):
+    def setUp(self):
+        self.paas = PAAS(Mock(), Mock())
+        self.paas_data = {
+            "name": "name",
+            "type": "juju",
+            "endpoints": ["127.0.0.1:17070"],
+            "user": "user",
+            "secret": "secret",
+            "description": "description",
+            "config": {},
+        }
+
+    @patch("builtins.print")
+    def test_create_success(self, mock_print):
+        self.paas._http.post_cmd.return_value = (200, '{"id": "1234"}')
+        self.paas.create(self.paas_data)
+        self.paas._client.get_token.assert_called()
+        self.paas._http.post_cmd.assert_called_with(
+            endpoint="/admin/v1/paas", postfields_dict=self.paas_data
+        )
+        mock_print.assert_called_with("PaaS name created with ID 1234")
+
+    @patch("builtins.print")
+    def test_create_raise_exception(self, mock_print):
+        self.paas._http.post_cmd.return_value = (404, None)
+        with self.assertRaises(ClientException):
+            self.paas.create(self.paas_data)
+        self.paas._client.get_token.assert_called()
+        self.paas._http.post_cmd.assert_called_with(
+            endpoint="/admin/v1/paas", postfields_dict=self.paas_data
+        )
+        mock_print.assert_not_called()
+
+    @patch("osmclient.sol005.paas.utils")
+    def test_update_success(self, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get = Mock()
+        self.paas.get.return_value = {"_id": "1234"}
+        self.paas.update("paas_name", self.paas_data)
+        self.paas._http.patch_cmd.assert_called_with(
+            endpoint="/admin/v1/paas/1234", postfields_dict=self.paas_data
+        )
+
+    @patch("osmclient.sol005.paas.utils")
+    def test_update_fail_not_found_exception(self, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get = Mock()
+        self.paas.get.side_effect = NotFound()
+        with self.assertRaises(NotFound):
+            self.paas.update("paas_name", self.paas_data)
+        self.paas._http.patch_cmd.assert_not_called()
+
+    @patch("osmclient.sol005.paas.utils")
+    def test_update_fail_client_exception(self, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get = Mock()
+        self.paas.get.side_effect = ClientException()
+        with self.assertRaises(ClientException):
+            self.paas.update("paas_name", self.paas_data)
+        self.paas._http.patch_cmd.assert_not_called()
+
+    @patch("osmclient.sol005.paas.utils")
+    def test_update_using_id(self, mock_utils):
+        mock_utils.validate_uuid4.return_value = True
+        self.paas.get = Mock()
+        self.paas.update("1234", self.paas_data)
+        self.paas._http.patch_cmd.assert_called_with(
+            endpoint="/admin/v1/paas/1234", postfields_dict=self.paas_data
+        )
+        self.paas.get.assert_not_called()
+
+    def test_get_id_sucess(self):
+        self.paas_data.update({"_id": "1234"})
+        self.paas.list = Mock()
+        self.paas.list.return_value = [self.paas_data]
+        paas_id = self.paas.get_id("name")
+        self.assertEqual(paas_id, "1234")
+
+    def test_get_id_not_found(self):
+        self.paas.list = Mock()
+        self.paas.list.return_value = []
+        with self.assertRaises(NotFound):
+            self.paas.get_id("name")
+
+    @patch("osmclient.sol005.paas.utils")
+    @patch("builtins.print")
+    def test_delete_success_202(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get_id = Mock()
+        self.paas.get_id.return_value = "1234"
+        self.paas._http.delete_cmd.return_value = (202, None)
+        self.paas.delete("paas_name")
+        self.paas._client.get_token.assert_called()
+        self.paas._http.delete_cmd.assert_called_with("/admin/v1/paas/1234")
+        mock_print.assert_called_with("Deletion in progress")
+
+    @patch("osmclient.sol005.paas.utils")
+    @patch("builtins.print")
+    def test_delete_success_204(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get_id = Mock()
+        self.paas.get_id.return_value = "1234"
+        self.paas._http.delete_cmd.return_value = (204, None)
+        self.paas.delete("paas_name", force=True)
+        self.paas._client.get_token.assert_called()
+        self.paas._http.delete_cmd.assert_called_with("/admin/v1/paas/1234?FORCE=True")
+        mock_print.assert_called_with("Deleted")
+
+    @patch("osmclient.sol005.paas.utils")
+    @patch("builtins.print")
+    def test_delete_fail_404(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get_id = Mock()
+        self.paas.get_id.return_value = "1234"
+        self.paas._http.delete_cmd.return_value = (404, "Not found")
+        with self.assertRaises(ClientException):
+            self.paas.delete("paas_name")
+        self.paas._client.get_token.assert_called()
+        self.paas._http.delete_cmd.assert_called_with("/admin/v1/paas/1234")
+        mock_print.assert_not_called()
+
+    @patch("osmclient.sol005.paas.utils")
+    @patch("builtins.print")
+    def test_delete_failed_id_not_found(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get_id = Mock()
+        self.paas.get_id.side_effect = NotFound()
+        self.paas._http.delete_cmd.return_value = (204, None)
+        with self.assertRaises(NotFound):
+            self.paas.delete("paas_name", force=True)
+        self.paas._client.get_token.assert_called()
+        self.paas._http.delete_cmd.assert_not_called()
+        mock_print.assert_not_called()
+
+    @patch("osmclient.sol005.paas.utils")
+    @patch("builtins.print")
+    def test_delete_using_id(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = True
+        self.paas.get_id = Mock()
+        self.paas._http.delete_cmd.return_value = (202, None)
+        paas_id = "1234"
+        self.paas.delete(paas_id)
+        self.paas.get_id.assert_not_called()
+        self.paas._client.get_token.assert_called()
+        self.paas._http.delete_cmd.assert_called_with("/admin/v1/paas/1234")
+        mock_print.assert_called_with("Deletion in progress")
+
+    @patch("osmclient.sol005.paas.utils")
+    @patch("builtins.print")
+    def test_delete_using_id_client_exception(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = True
+        self.paas.get_id = Mock()
+        self.paas._http.delete_cmd.return_value = (5, None)
+        paas_id = "1234"
+        with self.assertRaises(ClientException):
+            self.paas.delete(paas_id, force=True)
+        self.paas._client.get_token.assert_called()
+        self.paas.get_id.assert_not_called()
+        self.paas._http.delete_cmd.assert_called_with("/admin/v1/paas/1234?FORCE=True")
+        mock_print.assert_not_called()
+
+    @patch("osmclient.sol005.paas.utils")
+    @patch("builtins.print")
+    def test_delete_using_id_not_found(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = True
+        self.paas.get_id = Mock()
+        self.paas._http.delete_cmd.side_effect = NotFound()
+        paas_id = "1234"
+        with self.assertRaises(NotFound):
+            self.paas.delete(paas_id, force=True)
+        self.paas._client.get_token.assert_called()
+        self.paas.get_id.assert_not_called()
+        self.paas._http.delete_cmd.assert_called_with("/admin/v1/paas/1234?FORCE=True")
+        mock_print.assert_not_called()
+
+    def test_list_success(self):
+        self.paas._http.get2_cmd.return_value = (None, '[{"_id": "1234"}]')
+        paas_list = self.paas.list("my_filter")
+        self.paas._client.get_token.assert_called()
+        self.paas._http.get2_cmd.assert_called_with("/admin/v1/paas?my_filter")
+        self.assertEqual(paas_list, [{"_id": "1234"}])
+
+    def test_list_no_response(self):
+        self.paas._http.get2_cmd.return_value = (None, None)
+        paas_list = self.paas.list()
+        self.paas._client.get_token.assert_called()
+        self.paas._http.get2_cmd.assert_called_with("/admin/v1/paas")
+        self.assertEqual(paas_list, [])
+
+    @patch("osmclient.sol005.paas.utils")
+    def test_get_success(self, mock_utils):
+        self.paas_data.update({"_id": "1234"})
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get_id = Mock()
+        self.paas.get_id.return_value = "1234"
+        self.paas._http.get2_cmd.return_value = (0, json.dumps(self.paas_data))
+        paas = self.paas.get("paas_name")
+        self.paas._client.get_token.assert_called()
+        self.paas._http.get2_cmd.assert_called_with("/admin/v1/paas/1234")
+        self.assertEqual(paas, self.paas_data)
+
+    @patch("osmclient.sol005.paas.utils")
+    def test_get_client_exception(self, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get_id = Mock()
+        self.paas.get_id.return_value = "1234"
+        self.paas._http.get2_cmd.return_value = (404, json.dumps({}))
+        with self.assertRaises(ClientException):
+            self.paas.get("paas_name")
+        self.paas._client.get_token.assert_called()
+        self.paas._http.get2_cmd.assert_called_with("/admin/v1/paas/1234")
+
+    @patch("osmclient.sol005.paas.utils")
+    def test_get_not_found_exception(self, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get_id = Mock()
+        self.paas.get_id.return_value = "1234"
+        self.paas._http.get2_cmd.side_effect = NotFound()
+        with self.assertRaises(NotFound):
+            self.paas.get("paas_name")
+        self.paas._client.get_token.assert_called()
+        self.paas._http.get2_cmd.assert_called_with("/admin/v1/paas/1234")
+
+    @patch("osmclient.sol005.paas.utils")
+    def test_get_success_use_id(self, mock_utils):
+        self.paas_data.update({"_id": "1234"})
+        mock_utils.validate_uuid4.return_value = True
+        self.paas._http.get2_cmd.return_value = (0, json.dumps(self.paas_data))
+        paas = self.paas.get("1234")
+        self.paas._client.get_token.assert_called()
+        self.paas._http.get2_cmd.assert_called_with("/admin/v1/paas/1234")
+        self.assertEqual(paas, self.paas_data)
+
+    @patch("osmclient.sol005.paas.utils")
+    def test_get_with_get_id_exception(self, mock_utils):
+        self.paas_data.update({"_id": "1234"})
+        mock_utils.validate_uuid4.return_value = False
+        self.paas.get_id = Mock()
+        self.paas.get_id.side_effect = NotFound()
+        self.paas._http.get2_cmd.return_value = (404, json.dumps(self.paas_data))
+        with self.assertRaises(NotFound):
+            self.paas.get("paas_name")
+        self.paas._client.get_token.assert_called()
+        self.paas._http.get2_cmd.assert_not_called()