From d1ccf0e88aeacb065b97a6c5cb82e6de53537b94 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Mon, 19 Jun 2023 11:17:26 +0200 Subject: [PATCH] Revert "Remove v1 version of osmclient which was used with old NBI (pre-Release FOUR)" This reverts commit ae90d6fd63a349680b9f8e9a975bf90ec3c598c4. Change-Id: I1f67f548c99e04279d9095b51b11cbbeacdd9609 Signed-off-by: garciadeblas --- osmclient/client.py | 15 +- osmclient/scripts/osm.py | 8 +- osmclient/sol005/client.py | 1 + osmclient/v1/__init__.py | 15 ++ osmclient/v1/client.py | 121 ++++++++++++ osmclient/v1/key.py | 40 ++++ osmclient/v1/ns.py | 230 ++++++++++++++++++++++ osmclient/v1/nsd.py | 62 ++++++ osmclient/v1/package.py | 70 +++++++ osmclient/v1/tests/test_ns.py | 51 +++++ osmclient/v1/tests/test_nsd.py | 44 +++++ osmclient/v1/tests/test_package.py | 41 ++++ osmclient/v1/tests/test_vnf.py | 58 ++++++ osmclient/v1/tests/test_vnfd.py | 45 +++++ osmclient/v1/utils.py | 30 +++ osmclient/v1/vca.py | 62 ++++++ osmclient/v1/vim.py | 299 +++++++++++++++++++++++++++++ osmclient/v1/vnf.py | 52 +++++ osmclient/v1/vnfd.py | 59 ++++++ requirements.txt | 2 + tox.ini | 2 +- 21 files changed, 1299 insertions(+), 8 deletions(-) create mode 100644 osmclient/v1/__init__.py create mode 100644 osmclient/v1/client.py create mode 100644 osmclient/v1/key.py create mode 100644 osmclient/v1/ns.py create mode 100644 osmclient/v1/nsd.py create mode 100644 osmclient/v1/package.py create mode 100644 osmclient/v1/tests/test_ns.py create mode 100644 osmclient/v1/tests/test_nsd.py create mode 100644 osmclient/v1/tests/test_package.py create mode 100644 osmclient/v1/tests/test_vnf.py create mode 100644 osmclient/v1/tests/test_vnfd.py create mode 100644 osmclient/v1/utils.py create mode 100644 osmclient/v1/vca.py create mode 100644 osmclient/v1/vim.py create mode 100644 osmclient/v1/vnf.py create mode 100644 osmclient/v1/vnfd.py diff --git a/osmclient/client.py b/osmclient/client.py index fe4869a..a076fc4 100644 --- a/osmclient/client.py +++ b/osmclient/client.py @@ -19,6 +19,7 @@ OSM client entry point """ +from osmclient.v1 import client as client from osmclient.sol005 import client as sol005client import logging import verboselogs @@ -27,7 +28,7 @@ import verboselogs verboselogs.install() -def Client(version=1, host=None, *args, **kwargs): +def Client(version=1, host=None, sol005=True, *args, **kwargs): log_format_simple = "%(levelname)s %(message)s" log_format_complete = "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s" log_formatter_simple = logging.Formatter( @@ -52,7 +53,13 @@ def Client(version=1, host=None, *args, **kwargs): logger.setLevel(level=logging.VERBOSE) elif verbose > 2: logger.setLevel(level=logging.DEBUG) - if version == 1: - return sol005client.Client(host, *args, **kwargs) + if not sol005: + if version == 1: + return client.Client(host, *args, **kwargs) + else: + raise Exception("Unsupported client version") else: - raise Exception("Unsupported client version") + if version == 1: + return sol005client.Client(host, *args, **kwargs) + else: + raise Exception("Unsupported client version") diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 74bb84b..e5be118 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -41,8 +41,9 @@ from osmclient.cli_commands import ( wim, ) import yaml +import pycurl +import os import logging -from requests import RequestException @click.group( @@ -114,7 +115,8 @@ def cli_osm(ctx, **kwargs): exit(1) # Remove None values kwargs = {k: v for k, v in kwargs.items() if v is not None} - ctx.obj = client.Client(version=1, host=hostname, **kwargs) + sol005 = os.getenv("OSM_SOL005", True) + ctx.obj = client.Client(host=hostname, sol005=sol005, **kwargs) logger = logging.getLogger("osmclient") @@ -290,7 +292,7 @@ def cli(): cli_osm() exit(0) - except RequestException as exc: + except pycurl.error as exc: print(exc) print( 'Maybe "--hostname" option or OSM_HOSTNAME environment variable needs to be specified' diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py index 70d5c13..b05dbcc 100644 --- a/osmclient/sol005/client.py +++ b/osmclient/sol005/client.py @@ -18,6 +18,7 @@ OSM SOL005 client API """ +# from osmclient.v1 import vca from osmclient.sol005 import vnfd from osmclient.sol005 import nsd from osmclient.sol005 import nst diff --git a/osmclient/v1/__init__.py b/osmclient/v1/__init__.py new file mode 100644 index 0000000..2bf7fed --- /dev/null +++ b/osmclient/v1/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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. diff --git a/osmclient/v1/client.py b/osmclient/v1/client.py new file mode 100644 index 0000000..7d01cfa --- /dev/null +++ b/osmclient/v1/client.py @@ -0,0 +1,121 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 v1 client API +""" + +from osmclient.v1 import vnf +from osmclient.v1 import vnfd +from osmclient.v1 import ns +from osmclient.v1 import nsd +from osmclient.v1 import vim +from osmclient.v1 import package +from osmclient.v1 import utils +from osmclient.common import http +from osmclient.common import package_tool + + +class Client(object): + def __init__( + self, + host=None, + so_port=8008, + so_project="default", + ro_host=None, + ro_port=9090, + upload_port=8443, + **kwargs + ): + self._user = "admin" + self._password = "admin" + + if len(host.split(":")) > 1: + # backwards compatible, port provided as part of host + self._host = host.split(":")[0] + self._so_port = host.split(":")[1] + else: + self._host = host + self._so_port = so_port + + self._so_project = so_project + + http_client = http.Http("https://{}:{}/".format(self._host, self._so_port)) + http_client.set_http_header( + ["Accept: application/vnd.yand.data+json", "Content-Type: application/json"] + ) + + self._so_version = self.get_so_version(http_client) + + if ro_host is None: + ro_host = host + ro_http_client = http.Http("http://{}:{}/".format(ro_host, ro_port)) + ro_http_client.set_http_header( + ["Accept: application/vnd.yand.data+json", "Content-Type: application/json"] + ) + + upload_client_url = "https://{}:{}/composer/upload?api_server={}{}".format( + self._host, + upload_port, + "https://localhost&upload_server=https://", + self._host, + ) + + if self._so_version == "v3": + upload_client_url = ( + "https://{}:{}/composer/upload?api_server={}{}&project_name={}".format( + self._host, + upload_port, + "https://localhost&upload_server=https://", + self._host, + self._so_project, + ) + ) + + upload_client = http.Http(upload_client_url) + + self.vnf = vnf.Vnf(http_client, client=self, **kwargs) + self.vnfd = vnfd.Vnfd(http_client, client=self, **kwargs) + self.ns = ns.Ns(http=http_client, client=self, **kwargs) + self.nsd = nsd.Nsd(http_client, client=self, **kwargs) + self.vim = vim.Vim( + http=http_client, ro_http=ro_http_client, client=self, **kwargs + ) + self.package = package.Package( + http=http_client, upload_http=upload_client, client=self, **kwargs + ) + self.utils = utils.Utils(http_client, **kwargs) + self.package_tool = package_tool.PackageTool(client=self) + + @property + def so_rbac_project_path(self): + if self._so_version == "v3": + return "project/{}/".format(self._so_project) + else: + return "" + + def get_so_version(self, http_client): + try: + resp = http_client.get_cmd("api/operational/version") + if not resp or "rw-base:version" not in resp: + return "v2" + + if resp["rw-base:version"]["version"].split(".")[0] == "5": + # SO Version 5.x.x.x.x translates to OSM V3 + return "v3" + return "v2" + except Exception: + return "v2" diff --git a/osmclient/v1/key.py b/osmclient/v1/key.py new file mode 100644 index 0000000..4e51b78 --- /dev/null +++ b/osmclient/v1/key.py @@ -0,0 +1,40 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 ssh-key API handling +""" + +import json +import pycurl +from io import BytesIO + + +class Key(object): + def __init__(self, client=None): + self._client = client + + def list(self): + data = BytesIO() + curl_cmd = self._client.get_curl_cmd("v1/api/config/key-pair?deep") + curl_cmd.setopt(pycurl.HTTPGET, 1) + curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write) + curl_cmd.perform() + curl_cmd.close() + resp = json.loads(data.getvalue().decode()) + if "nsr:key-pair" in resp: + return resp["nsr:key-pair"] + return list() diff --git a/osmclient/v1/ns.py b/osmclient/v1/ns.py new file mode 100644 index 0000000..5006c1f --- /dev/null +++ b/osmclient/v1/ns.py @@ -0,0 +1,230 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 ns API handling +""" + +from osmclient.common import utils +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +import uuid +import yaml + + +class Ns(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + + def list(self): + """Returns a list of ns's""" + resp = self._http.get_cmd( + "api/running/{}ns-instance-config".format(self._client.so_rbac_project_path) + ) + if not resp or "nsr:ns-instance-config" not in resp: + return list() + + if "nsr" not in resp["nsr:ns-instance-config"]: + return list() + + return resp["nsr:ns-instance-config"]["nsr"] + + def get(self, name): + """Returns an ns based on name""" + for ns in self.list(): + if name == ns["name"]: + return ns + raise NotFound("ns {} not found".format(name)) + + def scale(self, ns_name, ns_scale_group, instance_index): + postdata = {} + postdata["instance"] = list() + instance = {} + instance["id"] = instance_index + postdata["instance"].append(instance) + + ns = self.get(ns_name) + resp = self._http.post_cmd( + "v1/api/config/{}ns-instance-config/nsr/{}/scaling-group/{}/instance".format( + self._client.so_rbac_project_path, ns["id"], ns_scale_group + ), + postdata, + ) + if "success" not in resp: + raise ClientException( + "failed to scale ns: {} result: {}".format(ns_name, resp) + ) + + def create( + self, + nsd_name, + nsr_name, + account, + config=None, + ssh_keys=None, + description="default description", + admin_status="ENABLED", + ): + postdata = {} + postdata["nsr"] = list() + nsr = {} + nsr["id"] = str(uuid.uuid1()) + + nsd = self._client.nsd.get(nsd_name) + + if self._client._so_version == "v3": + datacenter, resource_orchestrator = self._client.vim.get_datacenter(account) + if datacenter is None or resource_orchestrator is None: + raise NotFound("cannot find datacenter account {}".format(account)) + if "uuid" not in datacenter: + raise NotFound( + "The RO Datacenter - {} is invalid. Please select another".format( + account + ) + ) + else: + # Backwards Compatiility + datacenter = self._client.vim.get_datacenter(account) + if datacenter is None: + raise NotFound("cannot find datacenter account {}".format(account)) + + nsr["nsd"] = nsd + nsr["name"] = nsr_name + nsr["short-name"] = nsr_name + nsr["description"] = description + nsr["admin-status"] = admin_status + + if self._client._so_version == "v3": + # New format for V3 + nsr["resource-orchestrator"] = resource_orchestrator + nsr["datacenter"] = datacenter["name"] + else: + # Backwards Compatiility + nsr["om-datacenter"] = datacenter["uuid"] + + if ssh_keys is not None: + # ssh_keys is comma separate list + ssh_keys_format = [] + for key in ssh_keys.split(","): + ssh_keys_format.append({"key-pair-ref": key}) + + nsr["ssh-authorized-key"] = ssh_keys_format + + ns_config = {} + + if config: + ns_config = yaml.safe_load(config) + + if ns_config and "vim-network-name" in ns_config: + for network in ns_config["vim-network-name"]: + # now find this network + vld_name = network["name"] + # vim_vld_name = network['vim-network-name'] + + for index, vld in enumerate(nsr["nsd"]["vld"]): + if vld["name"] == vld_name: + nsr["nsd"]["vld"][index]["vim-network-name"] = network[ + "vim-network-name" + ] + + postdata["nsr"].append(nsr) + + resp = self._http.post_cmd( + "api/config/{}ns-instance-config/nsr".format( + self._client.so_rbac_project_path + ), + postdata, + ) + + if "success" not in resp: + raise ClientException( + "failed to create ns: {} nsd: {} result: {}".format( + nsr_name, nsd_name, resp + ) + ) + + def get_opdata(self, id): + return self._http.get_cmd( + "api/operational/{}ns-instance-opdata/nsr/{}?deep".format( + self._client.so_rbac_project_path, id + ) + ) + + def get_field(self, ns_name, field): + nsr = self.get(ns_name) + if nsr is None: + raise NotFound("failed to retrieve ns {}".format(ns_name)) + + if field in nsr: + return nsr[field] + + nsopdata = self.get_opdata(nsr["id"]) + + if field in nsopdata["nsr:nsr"]: + return nsopdata["nsr:nsr"][field] + + raise NotFound("failed to find {} in ns {}".format(field, ns_name)) + + def _terminate(self, ns_name): + ns = self.get(ns_name) + if ns is None: + raise NotFound("cannot find ns {}".format(ns_name)) + + return self._http.delete_cmd( + "api/config/{}ns-instance-config/nsr/{}".format( + self._client.so_rbac_project_path, ns["id"] + ) + ) + + def delete(self, ns_name, wait=True): + vnfs = self.get_field(ns_name, "constituent-vnfr-ref") + + resp = self._terminate(ns_name) + if "success" not in resp: + raise ClientException("failed to delete ns {}".format(ns_name)) + + # helper method to check if pkg exists + def check_not_exists(func): + try: + func() + return False + except NotFound: + return True + + for vnf in vnfs: + if not utils.wait_for_value( + lambda: check_not_exists(lambda: self._client.vnf.get(vnf["vnfr-id"])) + ): + raise ClientException("vnf {} failed to delete".format(vnf["vnfr-id"])) + if not utils.wait_for_value( + lambda: check_not_exists(lambda: self.get(ns_name)) + ): + raise ClientException("ns {} failed to delete".format(ns_name)) + + def get_monitoring(self, ns_name): + ns = self.get(ns_name) + mon_list = {} + if ns is None: + return mon_list + + vnfs = self._client.vnf.list() + for vnf in vnfs: + if ns["id"] == vnf["nsr-id-ref"]: + if "monitoring-param" in vnf: + mon_list[vnf["name"]] = vnf["monitoring-param"] + + return mon_list diff --git a/osmclient/v1/nsd.py b/osmclient/v1/nsd.py new file mode 100644 index 0000000..527a45f --- /dev/null +++ b/osmclient/v1/nsd.py @@ -0,0 +1,62 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 nsd API handling +""" + +from osmclient.common.exceptions import NotFound +from osmclient.common.exceptions import ClientException + + +class Nsd(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + + def list(self): + resp = self._http.get_cmd( + "api/running/{}nsd-catalog/nsd".format(self._client.so_rbac_project_path) + ) + + if self._client._so_version == "v3": + if resp and "project-nsd:nsd" in resp: + return resp["project-nsd:nsd"] + else: + # Backwards Compatibility + if resp and "nsd:nsd" in resp: + return resp["nsd:nsd"] + + return list() + + def get(self, name): + for nsd in self.list(): + if name == nsd["name"]: + return nsd + raise NotFound("cannot find nsd {}".format(name)) + + def delete(self, nsd_name): + nsd = self.get(nsd_name) + if nsd is None: + raise NotFound("cannot find nsd {}".format(nsd_name)) + + resp = self._http.delete_cmd( + "api/running/{}nsd-catalog/nsd/{}".format( + self._client.so_rbac_project_path, nsd["id"] + ) + ) + if "success" not in resp: + raise ClientException("failed to delete nsd {}".format(nsd_name)) diff --git a/osmclient/v1/package.py b/osmclient/v1/package.py new file mode 100644 index 0000000..f1a4bf4 --- /dev/null +++ b/osmclient/v1/package.py @@ -0,0 +1,70 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 package API handling +""" + +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +from osmclient.common import utils + + +class Package(object): + def __init__(self, http=None, upload_http=None, client=None): + self._client = client + self._http = http + self._upload_http = upload_http + + def _wait_for_package(self, pkg_type): + if "vnfd" in pkg_type["type"]: + get_method = self._client.vnfd.get + elif "nsd" in pkg_type["type"]: + get_method = self._client.nsd.get + else: + raise ClientException("no valid package type found") + + # helper method to check if pkg exists + def check_exists(func): + try: + func() + except NotFound: + return False + return True + + return utils.wait_for_value( + lambda: check_exists(lambda: get_method(pkg_type["name"])) + ) + + def get_key_val_from_pkg(self, descriptor_file): + return utils.get_key_val_from_pkg(descriptor_file) + + def wait_for_upload(self, filename): + """wait(block) for an upload to succeed. + The filename passed is assumed to be a descriptor tarball. + """ + pkg_type = utils.get_key_val_from_pkg(filename) + + if pkg_type is None: + raise ClientException("Cannot determine package type") + + if not self._wait_for_package(pkg_type): + raise ClientException("package {} failed to upload".format(filename)) + + def upload(self, filename): + resp = self._upload_http.post_cmd(formfile=("package", filename)) + if not resp or "transaction_id" not in resp: + raise ClientException("failed to upload package") diff --git a/osmclient/v1/tests/test_ns.py b/osmclient/v1/tests/test_ns.py new file mode 100644 index 0000000..616b8a4 --- /dev/null +++ b/osmclient/v1/tests/test_ns.py @@ -0,0 +1,51 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 unittest +from mock import Mock +from osmclient.v1 import ns +from osmclient.v1 import client +from osmclient.common.exceptions import NotFound + + +class TestNs(unittest.TestCase): + def test_list_empty(self): + mock = Mock() + mock.get_cmd.return_value = list() + assert len(ns.Ns(mock, client=client.Client(host="127.0.0.1")).list()) == 0 + + def test_get_notfound(self): + mock = Mock() + mock.get_cmd.return_value = "foo" + self.assertRaises( + NotFound, ns.Ns(mock, client=client.Client(host="127.0.0.1")).get, "bar" + ) + + def test_get_found(self): + mock = Mock() + mock.get_cmd.return_value = { + "nsr:ns-instance-config": {"nsr": [{"name": "foo"}]} + } + assert ns.Ns(mock, client=client.Client(host="127.0.0.1")).get("foo") + + def test_get_monitoring_notfound(self): + mock = Mock() + mock.get_cmd.return_value = "foo" + self.assertRaises( + NotFound, + ns.Ns(mock, client=client.Client(host="127.0.0.1")).get_monitoring, + "bar", + ) diff --git a/osmclient/v1/tests/test_nsd.py b/osmclient/v1/tests/test_nsd.py new file mode 100644 index 0000000..6f0255e --- /dev/null +++ b/osmclient/v1/tests/test_nsd.py @@ -0,0 +1,44 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 unittest +from mock import Mock +from osmclient.v1 import nsd +from osmclient.v1 import client +from osmclient.common.exceptions import NotFound + + +class TestNsd(unittest.TestCase): + def test_list_empty(self): + mock = Mock() + mock.get_cmd.return_value = list() + assert len(nsd.Nsd(mock, client=client.Client(host="127.0.0.1")).list()) == 0 + + def test_get_notfound(self): + mock = Mock() + mock.get_cmd.return_value = "foo" + self.assertRaises( + NotFound, nsd.Nsd(mock, client=client.Client(host="127.0.0.1")).get, "bar" + ) + + def test_get_found(self): + mock = Mock() + if client.Client(host="127.0.0.1")._so_version == "v3": + mock.get_cmd.return_value = {"project-nsd:nsd": [{"name": "foo"}]} + else: + # Backwards Compatibility + mock.get_cmd.return_value = {"nsd:nsd": [{"name": "foo"}]} + assert nsd.Nsd(mock, client=client.Client(host="127.0.0.1")).get("foo") diff --git a/osmclient/v1/tests/test_package.py b/osmclient/v1/tests/test_package.py new file mode 100644 index 0000000..2f0362c --- /dev/null +++ b/osmclient/v1/tests/test_package.py @@ -0,0 +1,41 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 unittest +from mock import Mock +from osmclient.v1 import package +from osmclient.common.exceptions import ClientException + + +class TestPackage(unittest.TestCase): + def test_upload_fail(self): + mock = Mock() + mock.post_cmd.return_value = "foo" + self.assertRaises( + ClientException, package.Package(upload_http=mock).upload, "bar" + ) + + mock.post_cmd.return_value = None + self.assertRaises( + ClientException, package.Package(upload_http=mock).upload, "bar" + ) + + def test_wait_for_upload_bad_file(self): + mock = Mock() + mock.post_cmd.return_value = "foo" + self.assertRaises( + IOError, package.Package(upload_http=mock).wait_for_upload, "invalidfile" + ) diff --git a/osmclient/v1/tests/test_vnf.py b/osmclient/v1/tests/test_vnf.py new file mode 100644 index 0000000..40b4648 --- /dev/null +++ b/osmclient/v1/tests/test_vnf.py @@ -0,0 +1,58 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 unittest +from mock import Mock +from osmclient.v1 import vnf +from osmclient.v1 import client +from osmclient.common.exceptions import NotFound + + +class TestVnf(unittest.TestCase): + def test_list_empty(self): + mock = Mock() + mock.get_cmd.return_value = list() + assert len(vnf.Vnf(mock, client=client.Client(host="127.0.0.1")).list()) == 0 + + def test_get_notfound(self): + mock = Mock() + mock.get_cmd.return_value = "foo" + self.assertRaises( + NotFound, vnf.Vnf(mock, client=client.Client(host="127.0.0.1")).get, "bar" + ) + + def test_get_found(self): + mock = Mock() + mock.get_cmd.return_value = {"vnfr:vnfr": [{"name": "foo"}]} + assert vnf.Vnf(mock, client=client.Client(host="127.0.0.1")).get("foo") + + def test_get_monitoring_notfound(self): + mock = Mock() + mock.get_cmd.return_value = "foo" + self.assertRaises( + NotFound, + vnf.Vnf(mock, client=client.Client(host="127.0.0.1")).get_monitoring, + "bar", + ) + + def test_get_monitoring_found(self): + mock = Mock() + mock.get_cmd.return_value = { + "vnfr:vnfr": [{"name": "foo", "monitoring-param": True}] + } + assert vnf.Vnf(mock, client=client.Client(host="127.0.0.1")).get_monitoring( + "foo" + ) diff --git a/osmclient/v1/tests/test_vnfd.py b/osmclient/v1/tests/test_vnfd.py new file mode 100644 index 0000000..f069742 --- /dev/null +++ b/osmclient/v1/tests/test_vnfd.py @@ -0,0 +1,45 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 unittest +from mock import Mock +from osmclient.v1 import vnfd +from osmclient.v1 import client +from osmclient.common.exceptions import NotFound + + +class TestVnfd(unittest.TestCase): + def test_list_empty(self): + mock = Mock() + mock.get_cmd.return_value = list() + assert len(vnfd.Vnfd(mock, client=client.Client(host="127.0.0.1")).list()) == 0 + + def test_get_notfound(self): + mock = Mock() + mock.get_cmd.return_value = "foo" + self.assertRaises( + NotFound, vnfd.Vnfd(mock, client=client.Client(host="127.0.0.1")).get, "bar" + ) + + def test_get_found(self): + mock = Mock() + if client.Client(host="127.0.0.1")._so_version == "v3": + mock.get_cmd.return_value = {"project-vnfd:vnfd": [{"name": "foo"}]} + else: + # Backwards Compatibility + mock.get_cmd.return_value = {"vnfd:vnfd": [{"name": "foo"}]} + + assert vnfd.Vnfd(mock, client=client.Client(host="127.0.0.1")).get("foo") diff --git a/osmclient/v1/utils.py b/osmclient/v1/utils.py new file mode 100644 index 0000000..092f193 --- /dev/null +++ b/osmclient/v1/utils.py @@ -0,0 +1,30 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 utils +""" + + +class Utils(object): + def __init__(self, http=None): + self._http = http + + def get_vcs_info(self): + resp = self._http.get_cmd("api/operational/vcs/info") + if resp: + return resp["rw-base:info"]["components"]["component_info"] + return list() diff --git a/osmclient/v1/vca.py b/osmclient/v1/vca.py new file mode 100644 index 0000000..d2f17c6 --- /dev/null +++ b/osmclient/v1/vca.py @@ -0,0 +1,62 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 VCA API handling +""" + +from osmclient.common.exceptions import ClientException + + +class Vca(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + + def list(self): + resp = self._http.get_cmd( + "api/config/{}config-agent".format(self._client.so_rbac_project_path) + ) + if resp and "rw-config-agent:config-agent" in resp: + return resp["rw-config-agent:config-agent"]["account"] + return list() + + def delete(self, name): + if "success" not in self._http.delete_cmd( + "api/config/{}config-agent/account/{}".format( + self._client.so_rbac_project_path, name + ) + ): + raise ClientException("failed to delete config agent {}".format(name)) + + def create(self, name, account_type, server, user, secret): + postdata = {} + postdata["account"] = list() + + account = {} + account["name"] = name + account["account-type"] = account_type + account["juju"] = {} + account["juju"]["user"] = user + account["juju"]["secret"] = secret + account["juju"]["ip-address"] = server + postdata["account"].append(account) + + if "success" not in self._http.post_cmd( + "api/config/{}config-agent".format(self._client.so_rbac_project_path), + postdata, + ): + raise ClientException("failed to create config agent {}".format(name)) diff --git a/osmclient/v1/vim.py b/osmclient/v1/vim.py new file mode 100644 index 0000000..1e8b604 --- /dev/null +++ b/osmclient/v1/vim.py @@ -0,0 +1,299 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 vim API handling +""" + +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +import yaml +import time + + +class Vim(object): + def __init__(self, http=None, ro_http=None, client=None): + self._client = client + self._ro_http = ro_http + self._http = http + + def _attach(self, vim_name, vim_account): + tenant_name = "osm" + tenant = self._get_ro_tenant() + if tenant is None: + raise ClientException("tenant {} not found".format(tenant_name)) + + datacenter = self._get_ro_datacenter(vim_name) + if datacenter is None: + raise Exception("datacenter {} not found".format(vim_name)) + + return self._ro_http.post_cmd( + "openmano/{}/datacenters/{}".format(tenant["uuid"], datacenter["uuid"]), + vim_account, + ) + + def _detach(self, vim_name): + tenant_name = "osm" + tenant = self._get_ro_tenant() + if tenant is None: + raise ClientException("tenant {} not found".format(tenant_name)) + return self._ro_http.delete_cmd( + "openmano/{}/datacenters/{}".format(tenant["uuid"], vim_name) + ) + + def create(self, name, vim_access): + vim_account = {} + vim_account["datacenter"] = {} + + # currently assumes vim_acc + if "vim-type" not in vim_access: + # 'openstack' not in vim_access['vim-type']): + raise Exception("vim type not provided") + + vim_account["datacenter"]["name"] = name + vim_account["datacenter"]["type"] = vim_access["vim-type"] + + vim_config = {} + if "config" in vim_access and vim_access["config"] is not None: + vim_config = yaml.safe_load(vim_access["config"]) + + if vim_config: + vim_account["datacenter"]["config"] = vim_config + + vim_account = self.update_vim_account_dict(vim_account, vim_access, vim_config) + + resp = self._ro_http.post_cmd("openmano/datacenters", vim_account) + if resp and "error" in resp: + raise ClientException("failed to create vim") + else: + self._attach(name, vim_account) + self._update_ro_accounts() + + def _update_ro_accounts(self): + get_ro_accounts = self._http.get_cmd( + "api/operational/{}ro-account".format(self._client.so_rbac_project_path) + ) + if not get_ro_accounts or "rw-ro-account:ro-account" not in get_ro_accounts: + return + for account in get_ro_accounts["rw-ro-account:ro-account"]["account"]: + if account["ro-account-type"] == "openmano": + # Refresh the Account Status + refresh_body = { + "input": { + "ro-account": account["name"], + "project-name": self._client._so_project, + } + } + refresh_status = self._http.post_cmd( + "api/operations/update-ro-account-status", refresh_body + ) + if refresh_status and "error" in refresh_status: + raise ClientException("Failed to refersh RO Account Status") + + def update_vim_account_dict(self, vim_account, vim_access, vim_config): + if vim_access["vim-type"] == "vmware": + if "admin_username" in vim_config: + vim_account["datacenter"]["admin_username"] = vim_config[ + "admin_username" + ] + if "admin_password" in vim_config: + vim_account["datacenter"]["admin_password"] = vim_config[ + "admin_password" + ] + if "nsx_manager" in vim_config: + vim_account["datacenter"]["nsx_manager"] = vim_config["nsx_manager"] + if "nsx_user" in vim_config: + vim_account["datacenter"]["nsx_user"] = vim_config["nsx_user"] + if "nsx_password" in vim_config: + vim_account["datacenter"]["nsx_password"] = vim_config["nsx_password"] + if "orgname" in vim_config: + vim_account["datacenter"]["orgname"] = vim_config["orgname"] + if "vcenter_ip" in vim_config: + vim_account["datacenter"]["vcenter_ip"] = vim_config["vcenter_ip"] + if "vcenter_user" in vim_config: + vim_account["datacenter"]["vcenter_user"] = vim_config["vcenter_user"] + if "vcenter_password" in vim_config: + vim_account["datacenter"]["vcenter_password"] = vim_config[ + "vcenter_password" + ] + if "vcenter_port" in vim_config: + vim_account["datacenter"]["vcenter_port"] = vim_config["vcenter_port"] + vim_account["datacenter"]["vim_url"] = vim_access["vim-url"] + vim_account["datacenter"]["vim_url_admin"] = vim_access["vim-url"] + vim_account["datacenter"]["description"] = vim_access["description"] + vim_account["datacenter"]["vim_username"] = vim_access["vim-username"] + vim_account["datacenter"]["vim_password"] = vim_access["vim-password"] + vim_account["datacenter"]["vim_tenant_name"] = vim_access["vim-tenant-name"] + else: + vim_account["datacenter"]["vim_url"] = vim_access["vim-url"] + vim_account["datacenter"]["vim_url_admin"] = vim_access["vim-url"] + vim_account["datacenter"]["description"] = vim_access["description"] + vim_account["datacenter"]["vim_username"] = vim_access["vim-username"] + vim_account["datacenter"]["vim_password"] = vim_access["vim-password"] + vim_account["datacenter"]["vim_tenant_name"] = vim_access["vim-tenant-name"] + return vim_account + + def delete(self, vim_name): + # first detach + self._detach(vim_name) + # detach. continue if error, + # it could be the datacenter is left without attachment + resp = self._ro_http.delete_cmd("openmano/datacenters/{}".format(vim_name)) + if "result" not in resp: + raise ClientException("failed to delete vim {} - {}".format(vim_name, resp)) + self._update_ro_accounts() + + def list(self, ro_update): + if ro_update: + self._update_ro_accounts() + # the ro_update needs to be made synchronous, for now this works around the issue + # and waits a resonable amount of time for the update to finish + time.sleep(2) + + if self._client._so_version == "v3": + resp = self._http.get_cmd( + "v1/api/operational/{}ro-account-state".format( + self._client.so_rbac_project_path + ) + ) + datacenters = [] + if not resp or "rw-ro-account:ro-account-state" not in resp: + return list() + + ro_accounts = resp["rw-ro-account:ro-account-state"] + for ro_account in ro_accounts["account"]: + if "datacenters" not in ro_account: + continue + if "datacenters" not in ro_account["datacenters"]: + continue + for datacenter in ro_account["datacenters"]["datacenters"]: + datacenters.append( + { + "name": datacenter["name"], + "uuid": datacenter["uuid"] + if "uuid" in datacenter + else None, + } + ) + + vim_accounts = datacenters + return vim_accounts + else: + # Backwards Compatibility + resp = self._http.get_cmd("v1/api/operational/datacenters") + if not resp or "rw-launchpad:datacenters" not in resp: + return list() + + datacenters = resp["rw-launchpad:datacenters"] + + vim_accounts = list() + if "ro-accounts" not in datacenters: + return vim_accounts + + tenant = self._get_ro_tenant() + if tenant is None: + return vim_accounts + + for roaccount in datacenters["ro-accounts"]: + if "datacenters" not in roaccount: + continue + for datacenter in roaccount["datacenters"]: + vim_accounts.append( + self._get_ro_datacenter(datacenter["name"], tenant["uuid"]) + ) + return vim_accounts + + def _get_ro_tenant(self, name="osm"): + resp = self._ro_http.get_cmd("openmano/tenants/{}".format(name)) + + if not resp: + return None + + if "tenant" in resp and "uuid" in resp["tenant"]: + return resp["tenant"] + else: + return None + + def _get_ro_datacenter(self, name, tenant_uuid="any"): + resp = self._ro_http.get_cmd( + "openmano/{}/datacenters/{}".format(tenant_uuid, name) + ) + if not resp: + raise NotFound("datacenter {} not found".format(name)) + + if "datacenter" in resp and "uuid" in resp["datacenter"]: + return resp["datacenter"] + else: + raise NotFound("datacenter {} not found".format(name)) + + def get(self, name): + tenant = self._get_ro_tenant() + if tenant is None: + raise NotFound("no ro tenant found") + + return self._get_ro_datacenter(name, tenant["uuid"]) + + def get_datacenter(self, name): + if self._client._so_version == "v3": + resp = self._http.get_cmd( + "v1/api/operational/{}ro-account-state".format( + self._client.so_rbac_project_path + ) + ) + if not resp: + return None, None + + if not resp or "rw-ro-account:ro-account-state" not in resp: + return None, None + + ro_accounts = resp["rw-ro-account:ro-account-state"] + for ro_account in ro_accounts["account"]: + if "datacenters" not in ro_account: + continue + if "datacenters" not in ro_account["datacenters"]: + continue + for datacenter in ro_account["datacenters"]["datacenters"]: + if datacenter["name"] == name: + return datacenter, ro_account["name"] + return None, None + else: + # Backwards Compatibility + resp = self._http.get_cmd("v1/api/operational/datacenters") + if not resp: + return None + + if not resp or "rw-launchpad:datacenters" not in resp: + return None + if "ro-accounts" not in resp["rw-launchpad:datacenters"]: + return None + for roaccount in resp["rw-launchpad:datacenters"]["ro-accounts"]: + if "datacenters" not in roaccount: + continue + for datacenter in roaccount["datacenters"]: + if datacenter["name"] == name: + return datacenter + return None + + def get_resource_orchestrator(self): + resp = self._http.get_cmd( + "v1/api/operational/{}resource-orchestrator".format( + self._client.so_rbac_project_path + ) + ) + + if not resp or "rw-launchpad:resource-orchestrator" not in resp: + return None + return resp["rw-launchpad:resource-orchestrator"] diff --git a/osmclient/v1/vnf.py b/osmclient/v1/vnf.py new file mode 100644 index 0000000..5db0240 --- /dev/null +++ b/osmclient/v1/vnf.py @@ -0,0 +1,52 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 vnf API handling +""" + +from osmclient.common.exceptions import NotFound + + +class Vnf(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + + def list(self): + resp = self._http.get_cmd( + "v1/api/operational/{}vnfr-catalog/vnfr".format( + self._client.so_rbac_project_path + ) + ) + if resp and "vnfr:vnfr" in resp: + return resp["vnfr:vnfr"] + return list() + + def get(self, vnf_name): + vnfs = self.list() + for vnf in vnfs: + if vnf_name == vnf["name"]: + return vnf + if vnf_name == vnf["id"]: + return vnf + raise NotFound("vnf {} not found".format(vnf_name)) + + def get_monitoring(self, vnf_name): + vnf = self.get(vnf_name) + if vnf and "monitoring-param" in vnf: + return vnf["monitoring-param"] + return None diff --git a/osmclient/v1/vnfd.py b/osmclient/v1/vnfd.py new file mode 100644 index 0000000..e43475d --- /dev/null +++ b/osmclient/v1/vnfd.py @@ -0,0 +1,59 @@ +# Copyright 2017 Sandvine +# +# All Rights Reserved. +# +# 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 vnfd API handling +""" + +from osmclient.common.exceptions import NotFound +from osmclient.common.exceptions import ClientException + + +class Vnfd(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + + def list(self): + resp = self._http.get_cmd( + "api/running/{}vnfd-catalog/vnfd".format(self._client.so_rbac_project_path) + ) + + if self._client._so_version == "v3": + if resp and "project-vnfd:vnfd" in resp: + return resp["project-vnfd:vnfd"] + else: + # Backwards Compatibility + if resp and "vnfd:vnfd" in resp: + return resp["vnfd:vnfd"] + + return list() + + def get(self, name): + for vnfd in self.list(): + if name == vnfd["name"]: + return vnfd + raise NotFound("vnfd {} not found".format(name)) + + def delete(self, vnfd_name): + vnfd = self.get(vnfd_name) + resp = self._http.delete_cmd( + "api/running/{}vnfd-catalog/vnfd/{}".format( + self._client.so_rbac_project_path, vnfd["id"] + ) + ) + if "success" not in resp: + raise ClientException("failed to delete vnfd {}".format(vnfd_name)) diff --git a/requirements.txt b/requirements.txt index 500b7ad..421e66d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,6 +30,8 @@ packaging==23.1 # via -r requirements.in prettytable==3.7.0 # via -r requirements.in +pycurl==7.45.2 + # via -r requirements.in python-magic==0.4.27 # via -r requirements.in pyyaml==5.4.1 diff --git a/tox.ini b/tox.ini index e1560bb..78629d5 100644 --- a/tox.ini +++ b/tox.ini @@ -66,7 +66,7 @@ deps = {[testenv]deps} -r{toxinidir}/requirements-test.txt pylint commands = - pylint -E osmclient/ + pylint -E osmclient ####################################################################################### -- 2.25.1