Feature 10239: Distributed VCA 76/10576/13
authorDavid Garcia <david.garcia@canonical.com>
Wed, 31 Mar 2021 17:13:10 +0000 (19:13 +0200)
committerbeierlm <mark.beierl@canonical.com>
Wed, 28 Apr 2021 08:50:04 +0000 (10:50 +0200)
Add the following commands to the OSM Client:
 - osm vca-add
 - osm vca-delete
 - osm vca-update
 - osm vca-list
 - osm vca-show
Other changes:
 - Add the --vca argument in the osm vim-create command, to be able to
associate a vca with a VIM
Depends on: https://osm.etsi.org/gerrit/#/c/osm/NBI/+/10574/

Change-Id: I1d322745d16c5ade27444be5afd37904f7306c5c
Signed-off-by: David Garcia <david.garcia@canonical.com>
Dockerfile
osmclient/scripts/osm.py
osmclient/scripts/tests/tests_vca.py [new file with mode: 0644]
osmclient/sol005/client.py
osmclient/sol005/k8scluster.py
osmclient/sol005/ns.py
osmclient/sol005/tests/test_vca.py [new file with mode: 0644]
osmclient/sol005/vca.py [new file with mode: 0644]
osmclient/sol005/vim.py
osmclient/v1/client.py

index 99a85a0..1a95764 100644 (file)
@@ -39,3 +39,6 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get -y install \
     libcurl4-openssl-dev \
     libssl-dev \
     wget
+
+ENV LC_ALL C.UTF-8
+ENV LANG C.UTF-8
\ No newline at end of file
index 4af0dad..12f0576 100755 (executable)
@@ -31,6 +31,7 @@ import textwrap
 import pkg_resources
 import logging
 from datetime import datetime
+from typing import Any, Dict
 
 
 def wrap_text(text, width):
@@ -3070,6 +3071,7 @@ def pdu_delete(ctx, name, force):
     help="do not return the control immediately, but keep it "
     "until the operation is completed, or timeout",
 )
+@click.option("--vca", default=None, help="VCA to be used in this VIM account")
 @click.pass_context
 def vim_create(
     ctx,
@@ -3084,6 +3086,7 @@ def vim_create(
     sdn_controller,
     sdn_port_mapping,
     wait,
+    vca,
 ):
     """creates a new VIM account"""
     logger.debug("")
@@ -3100,6 +3103,8 @@ def vim_create(
     vim["vim-type"] = account_type
     vim["description"] = description
     vim["config"] = config
+    if vca:
+        vim["vca"] = vca
     if sdn_controller or sdn_port_mapping:
         ctx.obj.vim.create(name, vim, sdn_controller, sdn_port_mapping, wait=wait)
     else:
@@ -3995,6 +4000,273 @@ def k8scluster_show(ctx, name, literal):
     #     exit(1)
 
 
+###########################
+# VCA operations
+###########################
+
+
+@cli_osm.command(name="vca-add", short_help="adds a VCA (Juju controller) to OSM")
+@click.argument("name")
+@click.option(
+    "--endpoints",
+    prompt=True,
+    help="Comma-separated list of IP or hostnames of the Juju controller",
+)
+@click.option("--user", prompt=True, help="Username with admin priviledges")
+@click.option("--secret", prompt=True, help="Password of the specified username")
+@click.option("--cacert", prompt=True, help="CA certificate")
+@click.option(
+    "--lxd-cloud",
+    prompt=True,
+    help="Name of the cloud that will be used for LXD containers (LXD proxy charms)",
+)
+@click.option(
+    "--lxd-credentials",
+    prompt=True,
+    help="Name of the cloud credentialsto be used for the LXD cloud",
+)
+@click.option(
+    "--k8s-cloud",
+    prompt=True,
+    help="Name of the cloud that will be used for K8s containers (K8s proxy charms)",
+)
+@click.option(
+    "--k8s-credentials",
+    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("--description", default=None, help="human readable description")
+@click.pass_context
+def vca_add(
+    ctx,
+    name,
+    endpoints,
+    user,
+    secret,
+    cacert,
+    lxd_cloud,
+    lxd_credentials,
+    k8s_cloud,
+    k8s_credentials,
+    model_config,
+    description,
+):
+    """adds a VCA to OSM
+
+    NAME: name of the VCA
+    """
+    check_client_version(ctx.obj, ctx.command.name)
+    vca = {}
+    vca["name"] = name
+    vca["endpoints"] = endpoints.split(",")
+    vca["user"] = user
+    vca["secret"] = secret
+    vca["cacert"] = cacert
+    vca["lxd-cloud"] = lxd_cloud
+    vca["lxd-credentials"] = lxd_credentials
+    vca["k8s-cloud"] = k8s_cloud
+    vca["k8s-credentials"] = k8s_credentials
+    if description:
+        vca["description"] = description
+    if model_config:
+        model_config = load(model_config)
+        vca["model-config"] = model_config
+    ctx.obj.vca.create(name, vca)
+
+
+def load(data: Any):
+    if os.path.isfile(data):
+        return load_file(data)
+    else:
+        try:
+            return json.loads(data)
+        except ValueError as e:
+            raise ClientException(e)
+
+
+def load_file(file_path: str) -> Dict:
+    content = None
+    with open(file_path, "r") as f:
+        content = f.read()
+    try:
+        return yaml.safe_load(content)
+    except yaml.scanner.ScannerError:
+        pass
+    try:
+        return json.loads(content)
+    except ValueError:
+        pass
+    raise ClientException(f"{file_path} must be a valid yaml or json file")
+
+
+@cli_osm.command(name="vca-update", short_help="updates a K8s cluster")
+@click.argument("name")
+@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("--cacert", help="CA certificate")
+@click.option(
+    "--lxd-cloud",
+    help="Name of the cloud that will be used for LXD containers (LXD proxy charms)",
+)
+@click.option(
+    "--lxd-credentials",
+    help="Name of the cloud credentialsto be used for the LXD cloud",
+)
+@click.option(
+    "--k8s-cloud",
+    help="Name of the cloud that will be used for K8s containers (K8s proxy charms)",
+)
+@click.option(
+    "--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("--description", default=None, help="human readable description")
+@click.pass_context
+def vca_update(
+    ctx,
+    name,
+    endpoints,
+    user,
+    secret,
+    cacert,
+    lxd_cloud,
+    lxd_credentials,
+    k8s_cloud,
+    k8s_credentials,
+    model_config,
+    description,
+):
+    """updates a K8s cluster
+
+    NAME: name or ID of the K8s cluster
+    """
+    check_client_version(ctx.obj, ctx.command.name)
+    vca = {}
+    vca["name"] = name
+    if endpoints:
+        vca["endpoints"] = endpoints.split(",")
+    if user:
+        vca["user"] = user
+    if secret:
+        vca["secret"] = secret
+    if cacert:
+        vca["cacert"] = cacert
+    if lxd_cloud:
+        vca["lxd-cloud"] = lxd_cloud
+    if lxd_credentials:
+        vca["lxd-credentials"] = lxd_credentials
+    if k8s_cloud:
+        vca["k8s-cloud"] = k8s_cloud
+    if k8s_credentials:
+        vca["k8s-credentials"] = k8s_credentials
+    if description:
+        vca["description"] = description
+    if model_config:
+        model_config = load(model_config)
+        vca["model-config"] = model_config
+    ctx.obj.vca.update(name, vca)
+
+
+@cli_osm.command(name="vca-delete", short_help="deletes a K8s cluster")
+@click.argument("name")
+@click.option(
+    "--force", is_flag=True, help="forces the deletion from the DB (not recommended)"
+)
+@click.pass_context
+def vca_delete(ctx, name, force):
+    """deletes a K8s cluster
+
+    NAME: name or ID of the K8s cluster to be deleted
+    """
+    check_client_version(ctx.obj, ctx.command.name)
+    ctx.obj.vca.delete(name, force=force)
+
+
+@cli_osm.command(name="vca-list")
+@click.option(
+    "--filter",
+    default=None,
+    multiple=True,
+    help="restricts the list to the VCAs 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 vca_list(ctx, filter, literal, long):
+    """list VCAs"""
+    check_client_version(ctx.obj, ctx.command.name)
+    if filter:
+        filter = "&".join(filter)
+    resp = ctx.obj.vca.list(filter)
+    if literal:
+        print(yaml.safe_dump(resp, indent=4, default_flow_style=False))
+        return
+    if long:
+        table = PrettyTable(
+            ["Name", "Id", "Project", "Operational State", "Detailed Status"]
+        )
+        project_list = ctx.obj.project.list()
+    else:
+        table = PrettyTable(["Name", "Id", "Operational State"])
+    for vca in resp:
+        logger.debug("VCA details: {}".format(yaml.safe_dump(vca)))
+        if long:
+            project_id, project_name = get_project(project_list, vca)
+            detailed_status = vca.get("_admin", {}).get("detailed-status", "-")
+            table.add_row(
+                [
+                    vca["name"],
+                    vca["_id"],
+                    project_name,
+                    vca.get("_admin", {}).get("operationalState", "-"),
+                    wrap_text(text=detailed_status, width=40),
+                ]
+            )
+        else:
+            table.add_row(
+                [
+                    vca["name"],
+                    vca["_id"],
+                    vca.get("_admin", {}).get("operationalState", "-"),
+                ]
+            )
+    table.align = "l"
+    print(table)
+
+
+@cli_osm.command(name="vca-show", short_help="shows the details of a K8s cluster")
+@click.argument("name")
+@click.option("--literal", is_flag=True, help="print literally, no pretty table")
+@click.pass_context
+def vca_show(ctx, name, literal):
+    """shows the details of a K8s cluster
+
+    NAME: name or ID of the K8s cluster
+    """
+    # try:
+    resp = ctx.obj.vca.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
 ###########################
diff --git a/osmclient/scripts/tests/tests_vca.py b/osmclient/scripts/tests/tests_vca.py
new file mode 100644 (file)
index 0000000..030a51f
--- /dev/null
@@ -0,0 +1,317 @@
+# 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.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 TestVca(unittest.TestCase):
+    def setUp(self):
+        self.runner = CliRunner()
+        self.ctx_obj = Mock()
+        self.table = Mock()
+        self.vca_data = {
+            "name": "name",
+            "_id": "1234",
+            "_admin": {
+                "detailed-status": "status",
+                "operationalState": "state",
+            },
+        }
+
+    def test_vca_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
+        mock_get_project.return_value = ("5678", "project")
+        self.ctx_obj.vca.list.return_value = [self.vca_data]
+        self.runner.invoke(
+            osm.cli_osm,
+            ["vca-list", "--filter", "somefilter"],
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.vca.list.assert_called_with("somefilter")
+        mock_pretty_table.assert_called_with(["Name", "Id", "Operational State"])
+        self.table.add_row.assert_called_with(["name", "1234", "state"])
+        mock_print.assert_called_with(self.table)
+
+    def test_vca_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")
+        self.ctx_obj.vca.list.return_value = [self.vca_data]
+        self.runner.invoke(
+            osm.cli_osm,
+            ["vca-list", "--filter", "somefilter", "--long"],
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.vca.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", "state", "status"]
+        )
+        mock_print.assert_called_with(self.table)
+
+    def test_vca_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.vca.list.return_value = [self.vca_data]
+        self.runner.invoke(
+            osm.cli_osm,
+            ["vca-list", "--literal"],
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.vca.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.vca_data], indent=4, default_flow_style=False)
+        )
+
+    def test_vca_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.vca.get.return_value = self.vca_data
+        self.runner.invoke(
+            osm.cli_osm,
+            ["vca-show", "name"],
+        )
+        self.ctx_obj.vca.get.assert_called_with("name")
+        mock_pretty_table.assert_called_with(["key", "attribute"])
+        self.assertEqual(self.table.add_row.call_count, len(self.vca_data))
+        mock_print.assert_called_with(self.table)
+
+    def test_vca_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.vca.get.return_value = self.vca_data
+        self.runner.invoke(
+            osm.cli_osm,
+            ["vca-show", "name", "--literal"],
+        )
+        self.ctx_obj.vca.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.vca_data, indent=4, default_flow_style=False)
+        )
+
+    def test_vca_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,
+            ["vca-update", "name"],
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.vca.update.assert_called_with("name", {"name": "name"})
+        mock_pretty_table.assert_not_called()
+        self.table.add_row.assert_not_called()
+        mock_print.assert_not_called()
+
+    def test_vca_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,
+            [
+                "vca-update",
+                "name",
+                "--endpoints",
+                "1.2.3.4:17070",
+                "--user",
+                "user",
+                "--secret",
+                "secret",
+                "--cacert",
+                "cacert",
+                "--lxd-cloud",
+                "lxd_cloud",
+                "--lxd-credentials",
+                "lxd_credentials",
+                "--k8s-cloud",
+                "k8s_cloud",
+                "--k8s-credentials",
+                "k8s_credentials",
+                "--description",
+                "description",
+                "--model-config",
+                json.dumps({"juju-https-proxy": "http://squid:3128"}),
+            ],
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.vca.update.assert_called_with(
+            "name",
+            {
+                "name": "name",
+                "endpoints": ["1.2.3.4:17070"],
+                "user": "user",
+                "secret": "secret",
+                "cacert": "cacert",
+                "lxd-cloud": "lxd_cloud",
+                "lxd-credentials": "lxd_credentials",
+                "k8s-cloud": "k8s_cloud",
+                "k8s-credentials": "k8s_credentials",
+                "description": "description",
+                "model-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_vca_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,
+            [
+                "vca-add",
+                "name",
+                "--endpoints",
+                "1.2.3.4:17070",
+                "--user",
+                "user",
+                "--secret",
+                "secret",
+                "--cacert",
+                "cacert",
+                "--lxd-cloud",
+                "lxd_cloud",
+                "--lxd-credentials",
+                "lxd_credentials",
+                "--k8s-cloud",
+                "k8s_cloud",
+                "--k8s-credentials",
+                "k8s_credentials",
+                "--description",
+                "description",
+                "--model-config",
+                json.dumps({"juju-https-proxy": "http://squid:3128"}),
+            ],
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.vca.create.assert_called_with(
+            "name",
+            {
+                "name": "name",
+                "endpoints": ["1.2.3.4:17070"],
+                "user": "user",
+                "secret": "secret",
+                "cacert": "cacert",
+                "lxd-cloud": "lxd_cloud",
+                "lxd-credentials": "lxd_credentials",
+                "k8s-cloud": "k8s_cloud",
+                "k8s-credentials": "k8s_credentials",
+                "description": "description",
+                "model-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_vca_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,
+            ["vca-delete", "name"],
+        )
+        mock_check_client_version.assert_called()
+        self.ctx_obj.vca.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_load(
+        self,
+        mock_get_project,
+        mock_check_client_version,
+        mock_client,
+        mock_pretty_table,
+        mock_print,
+    ):
+        data = osm.load(json.dumps({"juju-https-proxy": "http://squid:3128"}))
+        self.assertEqual(data, {"juju-https-proxy": "http://squid:3128"})
index 36062bf..57bc2b1 100644 (file)
@@ -35,6 +35,7 @@ from osmclient.sol005 import user as usermodule
 from osmclient.sol005 import role
 from osmclient.sol005 import pdud
 from osmclient.sol005 import k8scluster
+from osmclient.sol005 import vca
 from osmclient.sol005 import repo
 from osmclient.sol005 import osmrepo
 from osmclient.common import package_tool
@@ -95,6 +96,7 @@ class Client(object):
         self.role = role.Role(self._http_client, client=self)
         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.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)
index 0b99a37..92ef49e 100644 (file)
@@ -33,15 +33,18 @@ class K8scluster(object):
             self._apiName, self._apiVersion, self._apiResource
         )
 
-    def create(self, name, k8s_cluster):
-        def get_vim_account_id(vim_account):
-            vim = self._client.vim.get(vim_account)
-            if vim is None:
-                raise NotFound("cannot find vim account '{}'".format(vim_account))
-            return vim["_id"]
+    def _get_vim_account(self, vim_account):
+        vim = self._client.vim.get(vim_account)
+        if vim is None:
+            raise NotFound("cannot find vim account '{}'".format(vim_account))
+        return vim
 
+    def create(self, name, k8s_cluster):
         self._client.get_token()
-        k8s_cluster["vim_account"] = get_vim_account_id(k8s_cluster["vim_account"])
+        vim_account = self._get_vim_account(k8s_cluster["vim_account"])
+        k8s_cluster["vim_account"] = vim_account["_id"]
+        if "vca" in vim_account:
+            k8s_cluster["vca_id"] = vim_account["vca"]
         http_code, resp = self._http.post_cmd(
             endpoint=self._apiBase, postfields_dict=k8s_cluster
         )
@@ -65,6 +68,11 @@ class K8scluster(object):
     def update(self, name, k8s_cluster):
         self._client.get_token()
         cluster = self.get(name)
+        if "vim_account" in k8s_cluster:
+            vim_account = self._get_vim_account(k8s_cluster["vim_account"])
+            k8s_cluster["vim_account"] = vim_account["_id"]
+            if "vca" in vim_account:
+                k8s_cluster["vca_id"] = vim_account["vca"]
         http_code, resp = self._http.put_cmd(
             endpoint="{}/{}".format(self._apiBase, cluster["_id"]),
             postfields_dict=k8s_cluster,
index d457cbd..b6ccb9f 100644 (file)
@@ -181,6 +181,10 @@ class Ns(object):
             vim_account_id[vim_account] = vim["_id"]
             return vim["_id"]
 
+        def get_vca_id(vim_id):
+            vim = self._client.vim.get(vim_id)
+            return vim.get("vca")
+
         def get_wim_account_id(wim_account):
             self._logger.debug("")
             # wim_account can be False (boolean) to indicate not use wim account
@@ -194,11 +198,15 @@ class Ns(object):
             wim_account_id[wim_account] = wim["_id"]
             return wim["_id"]
 
+        vim_id = get_vim_account_id(account)
+        vca_id = get_vca_id(vim_id)
         ns = {}
         ns["nsdId"] = nsd["_id"]
         ns["nsName"] = nsr_name
         ns["nsDescription"] = description
-        ns["vimAccountId"] = get_vim_account_id(account)
+        ns["vimAccountId"] = vim_id
+        if vca_id:
+            ns["vcaId"] = vca_id
         # ns['userdata'] = {}
         # ns['userdata']['key1']='value1'
         # ns['userdata']['key2']='value2'
diff --git a/osmclient/sol005/tests/test_vca.py b/osmclient/sol005/tests/test_vca.py
new file mode 100644 (file)
index 0000000..25a2aeb
--- /dev/null
@@ -0,0 +1,162 @@
+# 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.vca import VCA
+
+
+class TestVca(unittest.TestCase):
+    def setUp(self):
+        self.vca = VCA(Mock(), Mock())
+        self.vca_data = {
+            "name": "name",
+            "endpoints": ["127.0.0.1:17070"],
+            "user": "user",
+            "secret": "secret",
+            "cacert": "cacert",
+            "lxd-cloud": "lxd_cloud",
+            "lxd-credentials": "lxd_credentials",
+            "k8s-cloud": "k8s_cloud",
+            "k8s-credentials": "k8s_credentials",
+            "description": "description",
+            "model-config": {},
+        }
+
+    @patch("builtins.print")
+    def test_create_success(self, mock_print):
+        self.vca._http.post_cmd.return_value = (200, '{"id": "1234"}')
+        self.vca.create("vca_name", self.vca_data)
+        self.vca._client.get_token.assert_called()
+        self.vca._http.post_cmd.assert_called()
+        mock_print.assert_called_with("1234")
+
+    @patch("builtins.print")
+    def test_create_missing_id(self, mock_print):
+        self.vca._http.post_cmd.return_value = (404, None)
+        with self.assertRaises(ClientException):
+            self.vca.create("vca_name", self.vca_data)
+        self.vca._client.get_token.assert_called()
+        self.vca._http.post_cmd.assert_called()
+        mock_print.assert_not_called()
+
+    def test_update_sucess(self):
+        self.vca.get = Mock()
+        self.vca.get.return_value = {"_id": "1234"}
+        self.vca.update("vca_name", self.vca_data)
+        self.vca._http.patch_cmd.assert_called_with(
+            endpoint="/admin/v1/vca/1234", postfields_dict=self.vca_data
+        )
+
+    def test_get_id_sucess(self):
+        self.vca_data.update({"_id": "1234"})
+        self.vca.list = Mock()
+        self.vca.list.return_value = [self.vca_data]
+        vca_id = self.vca.get_id("name")
+        self.assertEqual(vca_id, "1234")
+
+    def test_get_id_not_found(self):
+        self.vca.list = Mock()
+        self.vca.list.return_value = []
+        with self.assertRaises(NotFound):
+            self.vca.get_id("name")
+
+    @patch("osmclient.sol005.vca.utils")
+    @patch("builtins.print")
+    def test_delete_success_202(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.vca.get_id = Mock()
+        self.vca.get_id.return_value = "1234"
+        self.vca._http.delete_cmd.return_value = (202, None)
+        self.vca.delete("vca_name")
+        self.vca._client.get_token.assert_called()
+        self.vca._http.delete_cmd.assert_called_with("/admin/v1/vca/1234")
+        mock_print.assert_called_with("Deletion in progress")
+
+    @patch("osmclient.sol005.vca.utils")
+    @patch("builtins.print")
+    def test_delete_success_204(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.vca.get_id = Mock()
+        self.vca.get_id.return_value = "1234"
+        self.vca._http.delete_cmd.return_value = (204, None)
+        self.vca.delete("vca_name", force=True)
+        self.vca._client.get_token.assert_called()
+        self.vca._http.delete_cmd.assert_called_with("/admin/v1/vca/1234?FORCE=True")
+        mock_print.assert_called_with("Deleted")
+
+    @patch("osmclient.sol005.vca.utils")
+    @patch("builtins.print")
+    def test_delete_success_404(self, mock_print, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.vca.get_id = Mock()
+        self.vca.get_id.return_value = "1234"
+        self.vca._http.delete_cmd.return_value = (404, "Not found")
+        with self.assertRaises(ClientException):
+            self.vca.delete("vca_name")
+        self.vca._client.get_token.assert_called()
+        self.vca._http.delete_cmd.assert_called_with("/admin/v1/vca/1234")
+        mock_print.assert_not_called()
+
+    def test_list_success(self):
+        self.vca._http.get2_cmd.return_value = (None, '[{"_id": "1234"}]')
+        vca_list = self.vca.list("my_filter")
+        self.vca._client.get_token.assert_called()
+        self.vca._http.get2_cmd.assert_called_with("/admin/v1/vca?my_filter")
+        self.assertEqual(vca_list, [{"_id": "1234"}])
+
+    def test_list_no_response(self):
+        self.vca._http.get2_cmd.return_value = (None, None)
+        vca_list = self.vca.list()
+        self.vca._client.get_token.assert_called()
+        self.vca._http.get2_cmd.assert_called_with("/admin/v1/vca")
+        self.assertEqual(vca_list, [])
+
+    @patch("osmclient.sol005.vca.utils")
+    def test_get_success(self, mock_utils):
+        self.vca_data.update({"_id": "1234"})
+        mock_utils.validate_uuid4.return_value = False
+        self.vca.get_id = Mock()
+        self.vca.get_id.return_value = "1234"
+        self.vca._http.get2_cmd.return_value = (404, json.dumps(self.vca_data))
+        vca = self.vca.get("vca_name")
+        self.vca._client.get_token.assert_called()
+        self.vca._http.get2_cmd.assert_called_with("/admin/v1/vca/1234")
+        self.assertEqual(vca, self.vca_data)
+
+    @patch("osmclient.sol005.vca.utils")
+    def test_get_client_exception(self, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.vca.get_id = Mock()
+        self.vca.get_id.return_value = "1234"
+        self.vca._http.get2_cmd.return_value = (404, json.dumps({}))
+        with self.assertRaises(ClientException):
+            self.vca.get("vca_name")
+        self.vca._client.get_token.assert_called()
+        self.vca._http.get2_cmd.assert_called_with("/admin/v1/vca/1234")
+
+    @patch("osmclient.sol005.vca.utils")
+    def test_get_not_exception(self, mock_utils):
+        mock_utils.validate_uuid4.return_value = False
+        self.vca.get_id = Mock()
+        self.vca.get_id.return_value = "1234"
+        self.vca._http.get2_cmd.side_effect = NotFound()
+        with self.assertRaises(NotFound):
+            self.vca.get("vca_name")
+        self.vca._client.get_token.assert_called()
+        self.vca._http.get2_cmd.assert_called_with("/admin/v1/vca/1234")
diff --git a/osmclient/sol005/vca.py b/osmclient/sol005/vca.py
new file mode 100644 (file)
index 0000000..763739c
--- /dev/null
@@ -0,0 +1,102 @@
+# 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 K8s cluster API handling
+"""
+
+from osmclient.common import utils
+from osmclient.common.exceptions import NotFound
+from osmclient.common.exceptions import ClientException
+import json
+
+
+class VCA(object):
+    def __init__(self, http=None, client=None):
+        self._http = http
+        self._client = client
+        self._apiName = "/admin"
+        self._apiVersion = "/v1"
+        self._apiResource = "/vca"
+        self._apiBase = "{}{}{}".format(
+            self._apiName, self._apiVersion, self._apiResource
+        )
+
+    def create(self, name, vca):
+        self._client.get_token()
+        http_code, resp = self._http.post_cmd(
+            endpoint=self._apiBase, postfields_dict=vca
+        )
+        resp = json.loads(resp) if resp else {}
+        if "id" not in resp:
+            raise ClientException("unexpected response from server - {}".format(resp))
+        print(resp["id"])
+
+    def update(self, name, vca):
+        self._client.get_token()
+        vca_id = self.get(name)["_id"]
+        self._http.patch_cmd(
+            endpoint="{}/{}".format(self._apiBase, vca_id),
+            postfields_dict=vca,
+        )
+
+    def get_id(self, name):
+        """Returns a VCA id from a VCA name"""
+        for vca in self.list():
+            if name == vca["name"]:
+                return vca["_id"]
+        raise NotFound("VCA {} not found".format(name))
+
+    def delete(self, name, force=False):
+        self._client.get_token()
+        vca_id = name
+        if not utils.validate_uuid4(name):
+            vca_id = self.get_id(name)
+        querystring = "?FORCE=True" if force else ""
+        http_code, resp = self._http.delete_cmd(
+            "{}/{}{}".format(self._apiBase, vca_id, querystring)
+        )
+        if http_code == 202:
+            print("Deletion in progress")
+        elif http_code == 204:
+            print("Deleted")
+        else:
+            msg = resp or ""
+            raise ClientException("failed to delete VCA {} - {}".format(name, msg))
+
+    def list(self, cmd_filter=None):
+        """Returns a list of K8s clusters"""
+        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 VCA based on name or id"""
+        self._client.get_token()
+        vca_id = name
+        if not utils.validate_uuid4(name):
+            vca_id = self.get_id(name)
+        try:
+            _, resp = self._http.get2_cmd("{}/{}".format(self._apiBase, vca_id))
+            resp = json.loads(resp) if resp else {}
+            if "_id" not in resp:
+                raise ClientException("failed to get VCA info: {}".format(resp))
+            return resp
+        except NotFound:
+            raise NotFound("VCA {} not found".format(name))
index e5bf399..8b11c8d 100644 (file)
@@ -72,16 +72,26 @@ class Vim(object):
     def create(
         self, name, vim_access, sdn_controller=None, sdn_port_mapping=None, wait=False
     ):
+        vca_id = None
+
+        def get_vca_id(vca):
+            vca = self._client.vca.get(vca)
+            if vca is None:
+                raise NotFound("cannot find vca '{}'".format(vca))
+            return vca["_id"]
+
         self._logger.debug("")
         self._client.get_token()
+        if "vca" in vim_access:
+            vca_id = get_vca_id(vim_access["vca"])
         if "vim-type" not in vim_access:
             # 'openstack' not in vim_access['vim-type']):
             raise Exception("vim type not provided")
-
         vim_account = {}
         vim_account["name"] = name
         vim_account = self.update_vim_account_dict(vim_account, vim_access)
-
+        if vca_id:
+            vim_account["vca"] = vca_id
         vim_config = {}
         if "config" in vim_access and vim_access["config"] is not None:
             vim_config = yaml.safe_load(vim_access["config"])
index cad9c94..7d01cfa 100644 (file)
@@ -24,7 +24,6 @@ from osmclient.v1 import ns
 from osmclient.v1 import nsd
 from osmclient.v1 import vim
 from osmclient.v1 import package
-from osmclient.v1 import vca
 from osmclient.v1 import utils
 from osmclient.common import http
 from osmclient.common import package_tool
@@ -98,7 +97,6 @@ class Client(object):
         self.package = package.Package(
             http=http_client, upload_http=upload_client, client=self, **kwargs
         )
-        self.vca = vca.Vca(http_client, client=self, **kwargs)
         self.utils = utils.Utils(http_client, **kwargs)
         self.package_tool = package_tool.PackageTool(client=self)