From: David Garcia Date: Wed, 31 Mar 2021 17:13:10 +0000 (+0200) Subject: Feature 10239: Distributed VCA X-Git-Tag: release-v10.0-start~2 X-Git-Url: https://osm.etsi.org/gitweb/?p=osm%2Fosmclient.git;a=commitdiff_plain;h=961145b6c1ab65c8cc5953aae008781e285c1461 Feature 10239: Distributed VCA 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 --- diff --git a/Dockerfile b/Dockerfile index 99a85a0..1a95764 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 4af0dad..12f0576 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -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 index 0000000..030a51f --- /dev/null +++ b/osmclient/scripts/tests/tests_vca.py @@ -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"}) diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py index 36062bf..57bc2b1 100644 --- a/osmclient/sol005/client.py +++ b/osmclient/sol005/client.py @@ -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) diff --git a/osmclient/sol005/k8scluster.py b/osmclient/sol005/k8scluster.py index 0b99a37..92ef49e 100644 --- a/osmclient/sol005/k8scluster.py +++ b/osmclient/sol005/k8scluster.py @@ -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, diff --git a/osmclient/sol005/ns.py b/osmclient/sol005/ns.py index d457cbd..b6ccb9f 100644 --- a/osmclient/sol005/ns.py +++ b/osmclient/sol005/ns.py @@ -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 index 0000000..25a2aeb --- /dev/null +++ b/osmclient/sol005/tests/test_vca.py @@ -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 index 0000000..763739c --- /dev/null +++ b/osmclient/sol005/vca.py @@ -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)) diff --git a/osmclient/sol005/vim.py b/osmclient/sol005/vim.py index e5bf399..8b11c8d 100644 --- a/osmclient/sol005/vim.py +++ b/osmclient/sol005/vim.py @@ -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"]) diff --git a/osmclient/v1/client.py b/osmclient/v1/client.py index cad9c94..7d01cfa 100644 --- a/osmclient/v1/client.py +++ b/osmclient/v1/client.py @@ -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)