From: garciadeblas Date: Thu, 25 Apr 2024 17:13:31 +0000 (+0200) Subject: Features 11020,11022-11026 Advanced cluster mgmt X-Git-Tag: release-v16.0-start X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=a6d1818d69ae17f5cb09864e5bd32c292837fb73;p=osm%2Fosmclient.git Features 11020,11022-11026 Advanced cluster mgmt Change-Id: I45d7fcb1f644448c99588af499fe3d805780220a Signed-off-by: garciadeblas --- diff --git a/osmclient/cli_commands/app_profile.py b/osmclient/cli_commands/app_profile.py new file mode 100755 index 0000000..e85a19b --- /dev/null +++ b/osmclient/cli_commands/app_profile.py @@ -0,0 +1,97 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 click +from osmclient.cli_commands import common +import logging + +logger = logging.getLogger("osmclient") + + +@click.command(name="app-profile-create", short_help="creates an App Profile") +@click.argument("name") +@click.option("--description", default="", help="human readable description") +@click.pass_context +def app_profile_create(ctx, name, description, **kwargs): + """creates an App Profile + + NAME: name of the App Profile + """ + logger.debug("") + kwargs = {k: v for k, v in kwargs.items() if v is not None} + app_profile = kwargs + app_profile["name"] = name + if description: + app_profile["description"] = description + ctx.obj.app_profile.create(name, content_dict=app_profile) + + +@click.command(name="app-profile-delete", short_help="deletes an App Profile") +@click.argument("name") +@click.option( + "--force", is_flag=True, help="forces the deletion from the DB (not recommended)" +) +@click.pass_context +def app_profile_delete(ctx, name, force): + """deletes an App Profile + + NAME: name or ID of the App Profile to be deleted + """ + logger.debug("") + ctx.obj.app_profile.delete(name, force=force) + + +@click.command(name="app-profile-list") +@click.option( + "--filter", + help="restricts the list to the items matching the filter", +) +@click.pass_context +def app_profile_list(ctx, filter): + """list all App Profiles""" + logger.debug("") + common.generic_list(callback=ctx.obj.app_profile.list, filter=filter) + + +@click.command( + name="app-profile-show", + short_help="shows the details of an App Profile", +) +@click.argument("name") +@click.pass_context +def app_profile_show(ctx, name): + """shows the details of an App Profile + + NAME: name or ID of the App Profile + """ + logger.debug("") + common.generic_show(callback=ctx.obj.app_profile.get, name=name) + + +@click.command(name="app-profile-update", short_help="updates an App Profile") +@click.argument("name") +@click.option("--newname", help="New name for the App Profile") +@click.option("--description", help="human readable description") +@click.pass_context +def app_profile_update(ctx, name, newname, description, **kwargs): + """updates an App Profile + + NAME: name or ID of the App Profile + """ + logger.debug("") + profile_changes = common.generic_update(newname, description, kwargs) + ctx.obj.app_profile.update(name, changes_dict=profile_changes) diff --git a/osmclient/cli_commands/cluster.py b/osmclient/cli_commands/cluster.py new file mode 100755 index 0000000..15c83bc --- /dev/null +++ b/osmclient/cli_commands/cluster.py @@ -0,0 +1,147 @@ +# Copyright ETSI Contributors and Others. +# 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 click +from osmclient.cli_commands import common +import logging + +logger = logging.getLogger("osmclient") + + +@click.command(name="cluster-create", short_help="creates a K8s cluster") +@click.argument("name") +@click.option("--node-count", "-n", prompt=True, type=int, help="number of nodes") +@click.option("--node-size", prompt=True, help="size of the worker nodes") +@click.option("--version", prompt=True, help="Kubernetes version") +@click.option( + "--vim-account", + prompt=True, + help="VIM target or cloud account where the cluster will be created", +) +@click.option("--description", default="", help="human readable description") +@click.option( + "--region-name", + default=None, + help="region name in VIM account where the cluster will be created", +) +@click.option( + "--resource-group", + default=None, + help="resource group in VIM account where the cluster will be created", +) +@click.pass_context +def cluster_create( + ctx, + name, + node_count, + node_size, + version, + vim_account, + description, + region_name, + resource_group, + **kwargs +): + """creates a K8s cluster + + NAME: name of the K8s cluster + """ + logger.debug("") + # kwargs = {k: v for k, v in kwargs.items() if v is not None} + cluster = {} + cluster["name"] = name + cluster["node_count"] = node_count + cluster["node_size"] = node_size + cluster["k8s_version"] = version + cluster["vim_account"] = vim_account + if description: + cluster["description"] = description + if region_name: + cluster["region_name"] = region_name + if resource_group: + cluster["resource_group"] = resource_group + ctx.obj.cluster.create(name, content_dict=cluster) + + +@click.command(name="cluster-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 cluster_delete(ctx, name, force): + """deletes a K8s cluster + + NAME: name or ID of the K8s cluster to be deleted + """ + logger.debug("") + ctx.obj.cluster.delete(name, force=force) + + +@click.command(name="cluster-list") +@click.option( + "--filter", + help="restricts the list to the items matching the filter", +) +@click.pass_context +def cluster_list(ctx, filter): + """list all K8s clusters""" + logger.debug("") + extras = {"Created": "created"} + common.generic_list(callback=ctx.obj.cluster.list, filter=filter, extras=extras) + + +@click.command( + name="cluster-show", + short_help="shows the details of a K8s cluster", +) +@click.argument("name") +@click.pass_context +def cluster_show(ctx, name): + """shows the details of a K8s cluster + + NAME: name or ID of the K8s cluster + """ + logger.debug("") + common.generic_show(callback=ctx.obj.cluster.get, name=name) + + +@click.command(name="cluster-update", short_help="updates a K8s cluster") +@click.argument("name") +@click.option("--newname", help="New name for the K8s cluster") +@click.option("--description", help="human readable description") +@click.pass_context +def cluster_update(ctx, name, newname, description, **kwargs): + """updates a K8s cluster + + NAME: name or ID of the K8s cluster + """ + logger.debug("") + cluster_changes = common.generic_update(newname, description, kwargs) + ctx.obj.cluster.update(name, changes_dict=cluster_changes) + + +@click.command( + name="cluster-get-credentials", short_help="get kubeconfig of a K8s cluster" +) +@click.argument("name") +@click.pass_context +def cluster_get_credentials(ctx, name, **kwargs): + """updates a K8s cluster + + NAME: name or ID of the K8s cluster + """ + logger.debug("") + ctx.obj.cluster.get_credentials(name) diff --git a/osmclient/cli_commands/common.py b/osmclient/cli_commands/common.py new file mode 100644 index 0000000..7ba9368 --- /dev/null +++ b/osmclient/cli_commands/common.py @@ -0,0 +1,108 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 logging +import json +from osmclient.common import print_output +from osmclient.common.exceptions import ClientException + + +logger = logging.getLogger("osmclient") + + +def iterator_split(iterator, separators): + """ + Splits a tuple or list into several lists whenever a separator is found + For instance, the following tuple will be separated with the separator "--vnf" as follows. + From: + ("--vnf", "A", "--cause", "cause_A", "--vdu", "vdu_A1", "--vnf", "B", "--cause", "cause_B", ... + "--vdu", "vdu_B1", "--count_index", "1", "--run-day1", "--vdu", "vdu_B1", "--count_index", "2") + To: + [ + ("--vnf", "A", "--cause", "cause_A", "--vdu", "vdu_A1"), + ("--vnf", "B", "--cause", "cause_B", "--vdu", "vdu_B1", "--count_index", "1", "--run-day1", ... + "--vdu", "vdu_B1", "--count_index", "2") + ] + + Returns as many lists as separators are found + """ + logger.debug("") + if iterator[0] not in separators: + raise ClientException(f"Expected one of {separators}. Received: {iterator[0]}.") + list_of_lists = [] + first = 0 + for i in range(len(iterator)): + if iterator[i] in separators: + if i == first: + continue + if i - first < 2: + raise ClientException( + f"Expected at least one argument after separator (possible separators: {separators})." + ) + list_of_lists.append(list(iterator[first:i])) + first = i + if (len(iterator) - first) < 2: + raise ClientException( + f"Expected at least one argument after separator (possible separators: {separators})." + ) + else: + list_of_lists.append(list(iterator[first : len(iterator)])) + # logger.debug(f"List of lists: {list_of_lists}") + return list_of_lists + + +def generic_update(newname, description, extras): + changes_dict = {k: v for k, v in extras.items() if v is not None} + if newname: + changes_dict["name"] = newname + if description: + changes_dict["description"] = description + return changes_dict + + +def generic_show(callback, name, format="table"): + logger.debug("") + resp = callback(name) + headers = ["field", "value"] + rows = [] + if format == "table" or format == "csv": + if resp: + for k, v in list(resp.items()): + rows.append([k, json.dumps(v, indent=2)]) + print_output.print_output(format, headers, rows, resp) + + +def generic_list(callback, filter, format="table", extras={}): + logger.debug("") + if filter: + filter = "&".join(filter) + resp = callback(filter) + headers = ["Name", "Id", "State", "Operating State", "Resource State"] + headers.extend(extras.keys()) + rows = [] + if format == "table" or format == "csv": + for item in resp: + row_item = [ + item["name"], + item["_id"], + item["state"], + item["operatingState"], + item["resourceState"], + ] + for v in extras.values(): + row_item.append(item.get(v, "-")) + rows.append(row_item) + print_output.print_output(format, headers, rows, resp) diff --git a/osmclient/cli_commands/infra_config_profile.py b/osmclient/cli_commands/infra_config_profile.py new file mode 100755 index 0000000..9229a6f --- /dev/null +++ b/osmclient/cli_commands/infra_config_profile.py @@ -0,0 +1,103 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 click +from osmclient.cli_commands import common +import logging + +logger = logging.getLogger("osmclient") + + +@click.command( + name="infra-config-profile-create", short_help="creates an Infra Config Profile" +) +@click.argument("name") +@click.option("--description", default="", help="human readable description") +@click.pass_context +def infra_config_profile_create(ctx, name, description, **kwargs): + """creates an Infra Config Profile + + NAME: name of the Infra Config Profile + """ + logger.debug("") + kwargs = {k: v for k, v in kwargs.items() if v is not None} + infra_config_profile = kwargs + infra_config_profile["name"] = name + if description: + infra_config_profile["description"] = description + ctx.obj.infra_config_profile.create(name, content_dict=infra_config_profile) + + +@click.command( + name="infra-config-profile-delete", short_help="deletes an Infra Config Profile" +) +@click.argument("name") +@click.option( + "--force", is_flag=True, help="forces the deletion from the DB (not recommended)" +) +@click.pass_context +def infra_config_profile_delete(ctx, name, force): + """deletes an Infra Config Profile + + NAME: name or ID of the Infra Config Profile to be deleted + """ + logger.debug("") + ctx.obj.infra_config_profile.delete(name, force=force) + + +@click.command(name="infra-config-profile-list") +@click.option( + "--filter", + help="restricts the list to the items matching the filter", +) +@click.pass_context +def infra_config_profile_list(ctx, filter): + """list all Infra Config Profiles""" + logger.debug("") + common.generic_list(callback=ctx.obj.infra_config_profile.list, filter=filter) + + +@click.command( + name="infra-config-profile-show", + short_help="shows the details of an Infra Config Profile", +) +@click.argument("name") +@click.pass_context +def infra_config_profile_show(ctx, name): + """shows the details of an Infra Config Profile + + NAME: name or ID of the Infra Config Profile + """ + logger.debug("") + common.generic_show(callback=ctx.obj.infra_config_profile.get, name=name) + + +@click.command( + name="infra-config-profile-update", short_help="updates an Infra Config Profile" +) +@click.argument("name") +@click.option("--newname", help="New name for the Infra Config Profile") +@click.option("--description", help="human readable description") +@click.pass_context +def infra_config_profile_update(ctx, name, newname, description, **kwargs): + """updates an Infra Config Profile + + NAME: name or ID of the Infra Config Profile + """ + logger.debug("") + profile_changes = common.generic_update(newname, description, kwargs) + ctx.obj.infra_config_profile.update(name, changes_dict=profile_changes) diff --git a/osmclient/cli_commands/infra_controller_profile.py b/osmclient/cli_commands/infra_controller_profile.py new file mode 100755 index 0000000..6bab0e5 --- /dev/null +++ b/osmclient/cli_commands/infra_controller_profile.py @@ -0,0 +1,106 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 click +from osmclient.cli_commands import common +import logging + +logger = logging.getLogger("osmclient") + + +@click.command( + name="infra-controller-profile-create", + short_help="creates an Infra Controller Profile", +) +@click.argument("name") +@click.option("--description", default="", help="human readable description") +@click.pass_context +def infra_controller_profile_create(ctx, name, description, **kwargs): + """creates an Infra Controller Profile + + NAME: name of the Infra Controller Profile + """ + logger.debug("") + kwargs = {k: v for k, v in kwargs.items() if v is not None} + infra_controller_profile = kwargs + infra_controller_profile["name"] = name + if description: + infra_controller_profile["description"] = description + ctx.obj.infra_controller_profile.create(name, content_dict=infra_controller_profile) + + +@click.command( + name="infra-controller-profile-delete", + short_help="deletes an Infra Controller Profile", +) +@click.argument("name") +@click.option( + "--force", is_flag=True, help="forces the deletion from the DB (not recommended)" +) +@click.pass_context +def infra_controller_profile_delete(ctx, name, force): + """deletes an Infra Controller Profile + + NAME: name or ID of the Infra Controller Profile to be deleted + """ + logger.debug("") + ctx.obj.infra_controller_profile.delete(name, force=force) + + +@click.command(name="infra-controller-profile-list") +@click.option( + "--filter", + help="restricts the list to the items matching the filter", +) +@click.pass_context +def infra_controller_profile_list(ctx, filter): + """list all Infra Controller Profiles""" + logger.debug("") + common.generic_list(callback=ctx.obj.infra_controller_profile.list, filter=filter) + + +@click.command( + name="infra-controller-profile-show", + short_help="shows the details of an Infra Controller Profile", +) +@click.argument("name") +@click.pass_context +def infra_controller_profile_show(ctx, name): + """shows the details of an Infra Controller Profile + + NAME: name or ID of the Infra Controller Profile + """ + logger.debug("") + common.generic_show(callback=ctx.obj.infra_controller_profile.get, name=name) + + +@click.command( + name="infra-controller-profile-update", + short_help="updates an Infra Controller Profile", +) +@click.argument("name") +@click.option("--newname", help="New name for the Infra Controller Profile") +@click.option("--description", help="human readable description") +@click.pass_context +def infra_controller_profile_update(ctx, name, newname, description, **kwargs): + """updates an Infra Controller Profile + + NAME: name or ID of the Infra Controller Profile + """ + logger.debug("") + profile_changes = common.generic_update(newname, description, kwargs) + ctx.obj.infra_controller_profile.update(name, changes_dict=profile_changes) diff --git a/osmclient/cli_commands/k8scluster.py b/osmclient/cli_commands/k8scluster.py index 77e1124..380d311 100755 --- a/osmclient/cli_commands/k8scluster.py +++ b/osmclient/cli_commands/k8scluster.py @@ -110,7 +110,7 @@ def k8scluster_add( cluster["namespace"] = namespace if cni: cluster["cni"] = yaml.safe_load(cni) - ctx.obj.k8scluster.create(name, cluster, wait) + ctx.obj.k8scluster.add(name, cluster, wait) @click.command(name="k8scluster-update", short_help="updates a K8s cluster") @@ -190,7 +190,7 @@ def k8scluster_delete(ctx, name, force, wait): NAME: name or ID of the K8s cluster to be deleted """ utils.check_client_version(ctx.obj, ctx.command.name) - ctx.obj.k8scluster.delete(name, force, wait) + ctx.obj.k8scluster.remove(name, force, wait) @click.command(name="k8scluster-list") diff --git a/osmclient/cli_commands/ksu.py b/osmclient/cli_commands/ksu.py new file mode 100755 index 0000000..b89fca3 --- /dev/null +++ b/osmclient/cli_commands/ksu.py @@ -0,0 +1,337 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 click +from osmclient.cli_commands import common +from osmclient.common.exceptions import ClientException +import logging +import yaml + +logger = logging.getLogger("osmclient") + + +def verify_and_update_ksu(ctx, ksu): + def get_oka_id(ctx, oka_name): + logger.debug("") + resp = ctx.obj.oka.get(oka_name) + logger.debug(f"OKA obtained: {resp}") + if "_id" in resp: + return resp["_id"] + else: + raise ClientException("Unexpected failure when reading the OKA") + + def get_profile_id(ctx, profile, profile_type): + logger.debug("") + get_function = { + "infra-controller-profile": ctx.obj.infra_controller_profile.get, + "infra-config-profile": ctx.obj.infra_config_profile.get, + "app-profile": ctx.obj.app_profile.get, + "resource-profile": ctx.obj.resource_profile.get, + } + resp = get_function[profile_type](profile) + logger.debug(f"Profile obtained: {resp}") + if "_id" in resp: + return resp["_id"] + else: + raise ClientException("Unexpected failure when reading the profile") + + logger.debug("") + if "name" not in ksu: + raise ClientException("A name must be provided for each KSU") + else: + # TODO if ctx.command.name == "ksu-update", update ksu if needed + pass + if "profile" in ksu: + ksu_profile = ksu["profile"] + ksu_profile_type = ksu_profile.get("profile_type") + if "_id" in ksu_profile: + ksu_profile["_id"] = get_profile_id( + ctx, ksu_profile["_id"], ksu_profile_type + ) + else: + raise ClientException("A profile id or name must be provided for each KSU") + else: + raise ClientException("A profile must be provided for each KSU") + if "oka" in ksu: + for oka in ksu["oka"]: + if "_id" in oka: + oka["_id"] = get_oka_id(ctx, oka["_id"]) + elif "sw_catalog_path" not in oka: + raise ClientException( + "An OKA id or name, or a SW catalog path must be provided for each OKA" + ) + else: + raise ClientException( + "At least one OKA or SW catalog path must be provided for each KSU" + ) + + +def process_common_ksu_params(ctx, ksu_dict, ksu_args): + def process_common_oka_params(ctx, oka_dict, oka_args): + logger.debug("") + i = 0 + while i < len(oka_args): + if oka_args[i] == "--oka": + if (i + 1 >= len(oka_args)) or oka_args[i + 1].startswith("--"): + raise ClientException("No OKA was provided after --oka") + oka_dict["_id"] = oka_args[i + 1] + i = i + 2 + continue + if oka_args[i] == "--sw-catalog-path": + if (i + 1 >= len(oka_args)) or oka_args[i + 1].startswith("--"): + raise ClientException( + "No path was provided after --sw-catalog-path" + ) + oka_dict["sw_catalog_path"] = oka_args[i + 1] + i = i + 2 + continue + elif oka_args[i] == "--params": + if (i + 1 >= len(oka_args)) or oka_args[i + 1].startswith("--"): + raise ClientException("No params file was provided after --params") + with open(oka_args[i + 1], "r") as pf: + oka_dict["transformation"] = yaml.safe_load(pf.read()) + i = i + 2 + continue + else: + raise ClientException(f"Unknown option for OKA: {oka_args[i]}") + + logger.debug("") + i = 0 + while i < len(ksu_args): + if ksu_args[i] == "--description": + if (i + 1 >= len(ksu_args)) or ksu_args[i + 1].startswith("--"): + raise ClientException("No description was provided after --description") + ksu_dict["description"] = ksu_args[i + 1] + i = i + 2 + continue + elif ksu_args[i] == "--profile": + if (i + 1 >= len(ksu_args)) or ksu_args[i + 1].startswith("--"): + raise ClientException( + "No profile name or ID was provided after --profile" + ) + if "profile" not in ksu_dict: + ksu_dict["profile"] = {} + ksu_dict["profile"]["_id"] = ksu_args[i + 1] + i = i + 2 + continue + elif ksu_args[i] == "--profile-type": + if (i + 1 >= len(ksu_args)) or ksu_args[i + 1].startswith("--"): + raise ClientException( + "No profile type was provided after --profile-type" + ) + if "profile" not in ksu_dict: + ksu_dict["profile"] = {} + profile_type = ksu_args[i + 1] + profile_set = ( + "infra-controller-profile", + "infra-config-profile", + "app-profile", + "resource-profile", + ) + if profile_type not in profile_set: + raise ClientException(f"Profile type must be one of: {profile_set}") + ksu_dict["profile"]["profile_type"] = ksu_args[i + 1] + i = i + 2 + continue + elif ksu_args[i] == "--oka" or ksu_args[i] == "--sw-catalog-path": + # Split the tuple by "--oka" + logger.debug(ksu_args[i:]) + okas = common.iterator_split(ksu_args[i:], ("--oka", "--sw-catalog-path")) + logger.debug(f"OKAs: {okas}") + oka_list = [] + for oka in okas: + oka_dict = {} + process_common_oka_params(ctx, oka_dict, oka) + oka_list.append(oka_dict) + ksu_dict["oka"] = oka_list + break + else: + if ksu_args[i] == "--params": + raise ClientException( + "Option --params must be specified for an OKA or sw-catalog-path" + ) + else: + raise ClientException(f"Unknown option for KSU: {ksu_args[i]}") + return + + +def process_ksu_params(ctx, param, value): + """ + Processes the params in the commands ksu-create and ksu-update + Click does not allow advanced patterns for positional options like this: + --ksu jenkins --description "Jenkins KSU" + --profile profile1 --profile-type infra-controller-profile + --oka jenkins-controller --params jenkins-controller.yaml + --oka jenkins-config --params jenkins-config.yaml + --ksu prometheus --description "Prometheus KSU" + --profile profile2 --profile-type infra-controller-profile + --sw-catalog-path infra-controllers/prometheus --params prometheus-controller.yaml + + It returns the dictionary with all the params stored in ctx.params["ksu_params"] + """ + + logger.debug("") + logger.debug(f"Args: {value}") + if param.name != "args": + raise ClientException(f"Unexpected param: {param.name}") + # Split the tuple "value" by "--ksu" + ksus = common.iterator_split(value, ["--ksu"]) + logger.debug(f"KSUs: {ksus}") + ksu_list = [] + for ksu in ksus: + ksu_dict = {} + if ksu[1].startswith("--"): + raise ClientException("Expected a KSU after --ksu") + ksu_dict["name"] = ksu[1] + process_common_ksu_params(ctx, ksu_dict, ksu[2:]) + ksu_list.append(ksu_dict) + ctx.params["ksu_params"] = ksu_list + logger.debug(f"KSU params: {ksu_list}") + return + + +@click.command( + name="ksu-create", + short_help="creates KSUs in OSM", + context_settings=dict( + ignore_unknown_options=True, + ), +) +@click.argument( + "args", + nargs=-1, + type=click.UNPROCESSED, + callback=process_ksu_params, +) +@click.pass_context +def ksu_create(ctx, args, ksu_params): + """creates one or several Kubernetes SW Units (KSU) in OSM + + \b + Options: + --ksu NAME name of the KSU to be created + --profile NAME name or ID of the profile the KSU will belong to + --profile_type TYPE type of the profile: + [infra-controller-profile|infra-config-profile|app-profile|resource-profile] + --oka OKA_ID name or ID of the OKA that will be incorporated to the KSU + (either --oka or --sw_catalog must be used) + --sw_catalog TEXT folder in the SW catalog (git repo) that will be incorporated to the KSU + (either --oka or --sw_catalog must be used) + --params FILE file with the values that parametrize the OKA or the sw_catalog + + \b + Example: + osm ksu-create --ksu jenkins --description "Jenkins KSU" + --profile profile1 --profile-type infra-controller-profile + --oka jenkins-controller --params jenkins-controller.yaml + --oka jenkins-config --params jenkins-config.yaml + --ksu prometheus --description "Prometheus KSU" + --profile profile2 --profile-type infra-controller-profile + --sw-catalog-path infra-controllers/prometheus --params prometheus-controller.yaml + """ + logger.debug("") + logger.debug(f"ksu_params:\n{yaml.safe_dump(ksu_params)}") + for ksu in ksu_params: + verify_and_update_ksu(ctx, ksu) + logger.debug(f"ksu_params:\n{yaml.safe_dump(ksu_params)}") + ctx.obj.ksu.multi_create_update(ksu_params, "create") + + +@click.command(name="ksu-delete", short_help="deletes one or several KSU") +@click.argument("ksus", type=str, nargs=-1, metavar=" [...]") +@click.option( + "--force", is_flag=True, help="forces the deletion from the DB (not recommended)" +) +@click.pass_context +def ksu_delete(ctx, ksus, force): + """deletes one or several KSUs + + KSU: name or ID of the KSU to be deleted + """ + logger.debug("") + ctx.obj.ksu.multi_delete(ksus, "ksus", force=force) + + +@click.command(name="ksu-list") +@click.option( + "--filter", + help="restricts the list to the items matching the filter", +) +@click.pass_context +def ksu_list(ctx, filter): + """list all Kubernetes SW Units (KSU)""" + logger.debug("") + common.generic_list(callback=ctx.obj.ksu.list, filter=filter) + + +@click.command(name="ksu-show", short_help="shows the details of a KSU") +@click.argument("name") +@click.pass_context +def ksu_show(ctx, name): + """shows the details of a KSU + + NAME: name or ID of the KSU + """ + logger.debug("") + common.generic_show(callback=ctx.obj.ksu.get, name=name) + + +@click.command( + name="ksu-update", + short_help="updates KSUs in OSM", + context_settings=dict( + ignore_unknown_options=True, + ), +) +@click.argument( + "args", + nargs=-1, + type=click.UNPROCESSED, + callback=process_ksu_params, +) +@click.pass_context +def ksu_update(ctx, args, ksu_params): + """updates one or several Kubernetes SW Units (KSU) in OSM + + \b + Options: + --ksu NAME name of the KSU to be udpated + --profile NAME name or ID of the profile the KSU will belong to + --profile_type TYPE type of the profile: + [infra-controller-profile|infra-config-profile|app-profile|resource-profile] + --oka OKA_ID name or ID of the OKA that will be incorporated to the KSU + (either --oka or --sw_catalog must be used) + --sw_catalog TEXT folder in the SW catalog (git repo) that will be incorporated to the KSU + (either --oka or --sw_catalog must be used) + --params FILE file with the values that parametrize the OKA or the sw_catalog + + \b + Example: + osm ksu-update --ksu jenkins --description "Jenkins KSU" + --profile profile1 --profile-type infra-controller-profile + --oka jenkins-controller --params jenkins-controller.yaml + --oka jenkins-config --params jenkins-config.yaml + --ksu prometheus --description "Prometheus KSU" + --profile profile2 --profile-type infra-controller-profile + --sw-catalog-path infra-controllers/prometheus --params prometheus-controller.yaml + """ + logger.debug("") + logger.debug(f"ksu_params:\n{yaml.safe_dump(ksu_params)}") + for ksu in ksu_params: + verify_and_update_ksu(ctx, ksu) + logger.debug(f"ksu_params:\n{yaml.safe_dump(ksu_params)}") + ctx.obj.ksu.multi_create_update(ksu_params, "create") diff --git a/osmclient/cli_commands/nslcm.py b/osmclient/cli_commands/nslcm.py index 355422c..d206db2 100755 --- a/osmclient/cli_commands/nslcm.py +++ b/osmclient/cli_commands/nslcm.py @@ -16,7 +16,7 @@ import click from osmclient.common.exceptions import ClientException from osmclient.common.utils import validate_uuid4 -from osmclient.cli_commands import utils +from osmclient.cli_commands import utils, common import yaml import logging @@ -184,47 +184,6 @@ def ns_update(ctx, ns_name, updatetype, config, timeout, wait): ctx.obj.ns.update(ns_name, op_data, wait=wait) -def iterator_split(iterator, separators): - """ - Splits a tuple or list into several lists whenever a separator is found - For instance, the following tuple will be separated with the separator "--vnf" as follows. - From: - ("--vnf", "A", "--cause", "cause_A", "--vdu", "vdu_A1", "--vnf", "B", "--cause", "cause_B", ... - "--vdu", "vdu_B1", "--count_index", "1", "--run-day1", "--vdu", "vdu_B1", "--count_index", "2") - To: - [ - ("--vnf", "A", "--cause", "cause_A", "--vdu", "vdu_A1"), - ("--vnf", "B", "--cause", "cause_B", "--vdu", "vdu_B1", "--count_index", "1", "--run-day1", ... - "--vdu", "vdu_B1", "--count_index", "2") - ] - - Returns as many lists as separators are found - """ - logger.debug("") - if iterator[0] not in separators: - raise ClientException(f"Expected one of {separators}. Received: {iterator[0]}.") - list_of_lists = [] - first = 0 - for i in range(len(iterator)): - if iterator[i] in separators: - if i == first: - continue - if i - first < 2: - raise ClientException( - f"Expected at least one argument after separator (possible separators: {separators})." - ) - list_of_lists.append(list(iterator[first:i])) - first = i - if (len(iterator) - first) < 2: - raise ClientException( - f"Expected at least one argument after separator (possible separators: {separators})." - ) - else: - list_of_lists.append(list(iterator[first : len(iterator)])) - # logger.debug(f"List of lists: {list_of_lists}") - return list_of_lists - - def process_common_heal_params(heal_vnf_dict, args): logger.debug("") current_item = "vnf" @@ -289,7 +248,7 @@ def process_ns_heal_params(ctx, param, value): if param.name != "args": raise ClientException(f"Unexpected param: {param.name}") # Split the tuple "value" by "--vnf" - vnfs = iterator_split(value, ["--vnf"]) + vnfs = common.iterator_split(value, ["--vnf"]) logger.debug(f"VNFs: {vnfs}") heal_dict = {} heal_dict["healVnfData"] = [] diff --git a/osmclient/cli_commands/oka.py b/osmclient/cli_commands/oka.py new file mode 100755 index 0000000..dce5fa5 --- /dev/null +++ b/osmclient/cli_commands/oka.py @@ -0,0 +1,121 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 click +from osmclient.cli_commands import common +import logging + +logger = logging.getLogger("osmclient") + + +@click.command(name="oka-add", short_help="adds an OSM Kubernetes Application (OKA)") +@click.argument("name") +@click.argument("path") +@click.option("--description", default="", help="human readable description") +@click.pass_context +def oka_add(ctx, name, path, description): + """adds an OKA to OSM + + \b + NAME: name of the OKA + PATH: path to a folder with the OKA or to a tar.gz file with the OKA + """ + logger.debug("") + oka = {} + oka["name"] = name + if description: + oka["description"] = description + ctx.obj.oka.create(name, content_dict=oka, filename=path) + + +@click.command( + name="oka-delete", short_help="deletes an OSM Kubernetes Application (OKA)" +) +@click.argument("name") +@click.option( + "--force", is_flag=True, help="forces the deletion from the DB (not recommended)" +) +@click.pass_context +def oka_delete(ctx, name, force): + """deletes an OkA from OSM + + NAME: name or ID of the OKA to be deleted + """ + logger.debug("") + ctx.obj.oka.delete(name, force=force) + + +@click.command(name="oka-list") +@click.option( + "--filter", + help="restricts the list to the items matching the filter", +) +@click.pass_context +def oka_list(ctx, filter): + """list all OSM Kubernetes Application (OKA)""" + logger.debug("") + common.generic_list(callback=ctx.obj.oka.list, filter=filter) + + +@click.command( + name="oka-show", + short_help="shows the details of an OKA", +) +@click.argument("name") +@click.pass_context +def oka_show(ctx, name): + """shows the details of an OKA + + NAME: name or ID of the OKA + """ + logger.debug("") + common.generic_show(callback=ctx.obj.oka.get, name=name) + + +@click.command( + name="oka-update", short_help="updates an OSM Kubernetes Application (OKA)" +) +@click.argument("name") +@click.option("--newname", help="New name for the OSM Kubernetes Application (OKA)") +@click.option("--description", help="human readable description") +@click.pass_context +def oka_update(ctx, name, newname, description, **kwargs): + """updates an OSM Kubernetes Application (OKA) + + NAME: name or ID of the OSM Kubernetes Application (OKA) + """ + logger.debug("") + oka_changes = common.generic_update(newname, description, kwargs) + ctx.obj.oka.update(name, changes_dict=oka_changes, force_multipart=True) + + +@click.command( + name="oka-update-content", + short_help="updates the content of an OKA", +) +@click.argument("name") +@click.argument("path") +@click.pass_context +def oka_update_content(ctx, name, path): + """updates the content of an OSM Kubernetes Application (OKA) + + \b + NAME: name or ID of the OSM Kubernetes Application (OKA) + PATH: path to a folder with the OKA or to a tar.gz file with the OKA + """ + logger.debug("") + ctx.obj.oka.update_content(name, path) diff --git a/osmclient/cli_commands/profiles.py b/osmclient/cli_commands/profiles.py new file mode 100755 index 0000000..93a41f3 --- /dev/null +++ b/osmclient/cli_commands/profiles.py @@ -0,0 +1,143 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 click +import logging +from osmclient.common import print_output + +logger = logging.getLogger("osmclient") + + +@click.command(name="profile-list") +@click.option( + "--filter", + help="restricts the list to the items matching the filter", +) +@click.pass_context +def profile_list(ctx, filter): + """list all Profiles""" + if filter: + filter = "&".join(filter) + + resp_infra_controller_profile = ctx.obj.infra_controller_profile.list(filter) + resp_infra_config_profile = ctx.obj.infra_config_profile.list(filter) + resp_app_profile = ctx.obj.app_profile.list(filter) + resp_resource_profile = ctx.obj.resource_profile.list(filter) + + headers = ["Name", "Id", "Profile Type", "Default"] + rows = [] + + profile_list = [ + {"type": "Infra Controller Profile", "list": resp_infra_controller_profile}, + {"type": "Infra Config Profile", "list": resp_infra_config_profile}, + {"type": "App Profile", "list": resp_app_profile}, + {"type": "Resource Profile", "list": resp_resource_profile}, + ] + for profile in profile_list: + profile_type = profile["type"] + profile_list_item = profile["list"] + for item in profile_list_item: + rows.append( + [ + item["name"], + item["_id"], + profile_type, + item["default"], + ] + ) + print_output.print_output("table", headers, rows) + + +def patch_cluster_profile(ctx, profile, cluster, profile_type, patch_string): + logger.debug("") + get_function = { + "infra-controller-profile": ctx.obj.infra_controller_profile.get, + "infra-config-profile": ctx.obj.infra_config_profile.get, + "app-profile": ctx.obj.app_profile.get, + "resource-profile": ctx.obj.resource_profile.get, + } + resp = get_function[profile_type](profile) + profile_id = resp["_id"] + resp = ctx.obj.cluster.get(cluster) + cluster_id = resp["_id"] + update_dict = {patch_string: [{"id": profile_id}]} + logger.debug( + f"Cluster_id: {cluster_id}. Profile_id: {profile_id}. Update_dict = {update_dict}" + ) + return cluster_id, update_dict + + +@click.command(name="attach-profile") +@click.argument("profile") +@click.argument("cluster") +@click.option( + "--profile_type", + type=click.Choice( + [ + "infra-controller-profile", + "infra-config-profile", + "app-profile", + "resource-profile", + ] + ), + prompt=True, + help="type of profile", +) +@click.pass_context +def attach_profile(ctx, profile, cluster, profile_type): + """attaches profile to cluster + + \b + PROFILE: name or ID of the profile + CLUSTER: name or ID of the Kubernetes cluster + """ + logger.debug("") + cluster_id, update_dict = patch_cluster_profile( + ctx, profile, cluster, profile_type, "add_profile" + ) + ctx.obj.cluster.update_profiles(cluster_id, profile_type, update_dict) + + +@click.command(name="detach-profile") +@click.argument("profile") +@click.argument("cluster") +@click.option( + "--profile_type", + type=click.Choice( + [ + "infra-controller-profile", + "infra-config-profile", + "app-profile", + "resource-profile", + ] + ), + prompt=True, + help="type of profile", +) +@click.pass_context +def detach_profile(ctx, profile, cluster, profile_type): + """detaches profile from cluster + + \b + PROFILE: name or ID of the profile + CLUSTER: name or ID of the Kubernetes cluster + """ + logger.debug("") + cluster_id, update_dict = patch_cluster_profile( + ctx, profile, cluster, profile_type, "remove_profile" + ) + ctx.obj.cluster.update_profiles(cluster_id, profile_type, update_dict) diff --git a/osmclient/cli_commands/resource_profile.py b/osmclient/cli_commands/resource_profile.py new file mode 100755 index 0000000..8efb959 --- /dev/null +++ b/osmclient/cli_commands/resource_profile.py @@ -0,0 +1,99 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 click +from osmclient.cli_commands import common +import logging + +logger = logging.getLogger("osmclient") + + +@click.command( + name="resource-profile-create", short_help="creates an Resource Profile to OSM" +) +@click.argument("name") +@click.option("--description", default=None, help="human readable description") +@click.pass_context +def resource_profile_create(ctx, name, description, **kwargs): + """creates an Resource Profile to OSM + + NAME: name of the Resource Profile + """ + logger.debug("") + kwargs = {k: v for k, v in kwargs.items() if v is not None} + resource_profile = kwargs + resource_profile["name"] = name + if description: + resource_profile["description"] = description + ctx.obj.resource_profile.create(name, content_dict=resource_profile) + + +@click.command(name="resource-profile-delete", short_help="deletes an Resource Profile") +@click.argument("name") +@click.option( + "--force", is_flag=True, help="forces the deletion from the DB (not recommended)" +) +@click.pass_context +def resource_profile_delete(ctx, name, force): + """deletes an Resource Profile + + NAME: name or ID of the Resource Profile to be deleted + """ + logger.debug("") + ctx.obj.resource_profile.delete(name, force=force) + + +@click.command(name="resource-profile-list") +@click.option( + "--filter", + help="restricts the list to the items matching the filter", +) +@click.pass_context +def resource_profile_list(ctx, filter): + """list all Resource Profiles""" + logger.debug("") + common.generic_list(callback=ctx.obj.resource_profile.list, filter=filter) + + +@click.command( + name="resource-profile-show", + short_help="shows the details of an Resource Profile", +) +@click.argument("name") +@click.pass_context +def resource_profile_show(ctx, name): + """shows the details of an Resource Profile + + NAME: name or ID of the Resource Profile + """ + logger.debug("") + common.generic_show(callback=ctx.obj.resource_profile.get, name=name) + + +@click.command(name="resource-profile-update", short_help="updates an Resource Profile") +@click.argument("name") +@click.option("--newname", help="New name for the Resource Profile") +@click.option("--description", help="human readable description") +@click.pass_context +def resource_profile_update(ctx, name, newname, description, **kwargs): + """updates an Resource Profile + + NAME: name or ID of the Resource Profile + """ + logger.debug("") + profile_changes = common.generic_update(newname, description, kwargs) + ctx.obj.resource_profile.update(name, changes_dict=profile_changes) diff --git a/osmclient/cli_commands/vim.py b/osmclient/cli_commands/vim.py index 2f7bbfc..bb7622d 100755 --- a/osmclient/cli_commands/vim.py +++ b/osmclient/cli_commands/vim.py @@ -73,9 +73,7 @@ def _check_ca_cert(vim_config: dict) -> None: "until the operation is completed, or timeout", ) @click.option("--vca", default=None, help="VCA to be used in this VIM account") -@click.option( - "--creds", default=None, help="credentials file (only applicable for GCP VIM type)" -) +@click.option("--creds", default=None, help="credentials file") @click.option( "--prometheus_url", default=None, diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index f67f550..78d3fff 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -18,7 +18,12 @@ from osmclient import client from osmclient.common.exceptions import ClientException from osmclient.cli_commands import ( alarms, + app_profile, + cluster, + infra_config_profile, + infra_controller_profile, k8scluster, + ksu, metrics, netslice_instance, netslice_ops, @@ -28,11 +33,14 @@ from osmclient.cli_commands import ( nslcm_ops, nslcm, nspkg, + oka, other, packages, pdus, + profiles, rbac, repo, + resource_profile, sdnc, subscriptions, vca, @@ -139,6 +147,53 @@ def cli(): cli_osm.add_command(k8scluster.k8scluster_show) cli_osm.add_command(k8scluster.k8scluster_update) + cli_osm.add_command(infra_controller_profile.infra_controller_profile_create) + cli_osm.add_command(infra_controller_profile.infra_controller_profile_delete) + cli_osm.add_command(infra_controller_profile.infra_controller_profile_list) + cli_osm.add_command(infra_controller_profile.infra_controller_profile_show) + cli_osm.add_command(infra_controller_profile.infra_controller_profile_update) + + cli_osm.add_command(infra_config_profile.infra_config_profile_create) + cli_osm.add_command(infra_config_profile.infra_config_profile_delete) + cli_osm.add_command(infra_config_profile.infra_config_profile_list) + cli_osm.add_command(infra_config_profile.infra_config_profile_show) + cli_osm.add_command(infra_config_profile.infra_config_profile_update) + + cli_osm.add_command(app_profile.app_profile_create) + cli_osm.add_command(app_profile.app_profile_delete) + cli_osm.add_command(app_profile.app_profile_list) + cli_osm.add_command(app_profile.app_profile_show) + cli_osm.add_command(app_profile.app_profile_update) + + cli_osm.add_command(resource_profile.resource_profile_create) + cli_osm.add_command(resource_profile.resource_profile_delete) + cli_osm.add_command(resource_profile.resource_profile_list) + cli_osm.add_command(resource_profile.resource_profile_show) + cli_osm.add_command(resource_profile.resource_profile_update) + + cli_osm.add_command(profiles.profile_list) + cli_osm.add_command(profiles.attach_profile) + cli_osm.add_command(profiles.detach_profile) + + cli_osm.add_command(oka.oka_add) + cli_osm.add_command(oka.oka_delete) + cli_osm.add_command(oka.oka_list) + cli_osm.add_command(oka.oka_show) + cli_osm.add_command(oka.oka_update) + cli_osm.add_command(oka.oka_update_content) + + cli_osm.add_command(ksu.ksu_create) + cli_osm.add_command(ksu.ksu_delete) + cli_osm.add_command(ksu.ksu_list) + cli_osm.add_command(ksu.ksu_show) + cli_osm.add_command(ksu.ksu_update) + + cli_osm.add_command(cluster.cluster_create) + cli_osm.add_command(cluster.cluster_delete) + cli_osm.add_command(cluster.cluster_list) + cli_osm.add_command(cluster.cluster_show) + cli_osm.add_command(cluster.cluster_update) + cli_osm.add_command(netslice_instance.nsi_create1) cli_osm.add_command(netslice_instance.nsi_create2) cli_osm.add_command(netslice_instance.nsi_delete1) diff --git a/osmclient/sol005/app_profile.py b/osmclient/sol005/app_profile.py new file mode 100644 index 0000000..3b5417e --- /dev/null +++ b/osmclient/sol005/app_profile.py @@ -0,0 +1,34 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 App Profile API handling +""" + +from osmclient.sol005.osm_api_object import GenericOSMAPIObject + + +class AppProfile(GenericOSMAPIObject): + def __init__(self, http=None, client=None): + super().__init__(http, client) + self._apiName = "/k8scluster" + self._apiVersion = "/v1" + self._apiResource = "/app_profiles" + self._logObjectName = "app_profile" + self._apiBase = "{}{}{}".format( + self._apiName, self._apiVersion, self._apiResource + ) diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py index e0254c3..0b7d309 100644 --- a/osmclient/sol005/client.py +++ b/osmclient/sol005/client.py @@ -40,6 +40,13 @@ from osmclient.sol005 import repo from osmclient.sol005 import osmrepo from osmclient.sol005 import subscription from osmclient.common import package_tool +from osmclient.sol005 import oka +from osmclient.sol005 import ksu +from osmclient.sol005 import infra_controller_profile +from osmclient.sol005 import infra_config_profile +from osmclient.sol005 import app_profile +from osmclient.sol005 import resource_profile +from osmclient.sol005 import cluster from osmclient.common.exceptions import ClientException import json import logging @@ -100,6 +107,19 @@ class Client(object): self.osmrepo = osmrepo.OSMRepo(self._http_client, client=self) self.package_tool = package_tool.PackageTool(client=self) self.subscription = subscription.Subscription(self._http_client, client=self) + self.ksu = ksu.KSU(self._http_client, client=self) + self.oka = oka.OKA(self._http_client, client=self) + self.infra_controller_profile = infra_controller_profile.InfraControllerProfile( + self._http_client, client=self + ) + self.infra_config_profile = infra_config_profile.InfraConfigProfile( + self._http_client, client=self + ) + self.app_profile = app_profile.AppProfile(self._http_client, client=self) + self.resource_profile = resource_profile.ResourceProfile( + self._http_client, client=self + ) + self.cluster = cluster.Cluster(self._http_client, client=self) """ self.vca = vca.Vca(http_client, client=self, **kwargs) self.utils = utils.Utils(http_client, **kwargs) @@ -117,14 +137,11 @@ class Client(object): postfields_dict["project_domain_name"] = self._project_domain_name if self._user_domain_name: postfields_dict["user_domain_name"] = self._user_domain_name - http_code, resp = self._http_client.post_cmd( + _, resp = self._http_client.post_cmd( endpoint=self._auth_endpoint, postfields_dict=postfields_dict, skip_query_admin=True, ) - # if http_code not in (200, 201, 202, 204): - # message ='Authentication error: not possible to get auth token\nresp:\n{}'.format(resp) - # raise ClientException(message) token = json.loads(resp) if resp else None if token.get("message") == "change_password" and not pwd_change: diff --git a/osmclient/sol005/cluster.py b/osmclient/sol005/cluster.py new file mode 100644 index 0000000..9aebeab --- /dev/null +++ b/osmclient/sol005/cluster.py @@ -0,0 +1,65 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 Cluster API handling +""" + +from osmclient.sol005.osm_api_object import GenericOSMAPIObject +from osmclient.common.exceptions import ClientException +import json + + +class Cluster(GenericOSMAPIObject): + def __init__(self, http=None, client=None): + super().__init__(http, client) + self._apiName = "/k8scluster" + self._apiVersion = "/v1" + self._apiResource = "/clusters" + self._logObjectName = "cluster" + self._apiBase = "{}{}{}".format( + self._apiName, self._apiVersion, self._apiResource + ) + + def update_profiles(self, cluster_id, profile_type, update_dict): + """Updates the profiles in a cluster""" + self._logger.debug("") + self._client.get_token() + profile_endpoint = { + "infra-controller-profile": "infra_controller_profiles", + "infra-config-profile": "infra_config_profiles", + "app-profile": "app_profiles", + "resource-profile": "resource_profiles", + } + http_code, resp = self._http.patch_cmd( + endpoint="{}/{}/{}".format( + self._apiBase, cluster_id, profile_endpoint[profile_type] + ), + postfields_dict=update_dict, + skip_query_admin=True, + ) + if http_code in (200, 201, 202): + if resp: + resp = json.loads(resp) + if not resp or "id" not in resp: + raise ClientException(f"unexpected response from server - {resp}") + print(resp["id"]) + elif http_code == 204: + print("Updated") + + # def get_credentials(self, cluster_id): + # # TODO: diff --git a/osmclient/sol005/http.py b/osmclient/sol005/http.py index 8dd87e7..5b140f5 100644 --- a/osmclient/sol005/http.py +++ b/osmclient/sol005/http.py @@ -111,6 +111,7 @@ class Http(http.Http): self, endpoint="", postfields_dict=None, + postdata=None, formfile=None, filename=None, put_method=False, @@ -137,13 +138,17 @@ class Http(http.Http): jsondata_log = jsondata requests_cmd.json = postfields_dict self._logger.verbose("Request POSTFIELDS: {}".format(jsondata_log)) - elif formfile is not None: - requests_cmd.files = {formfile[0]: formfile[1]} - elif filename is not None: + if postdata is not None: + self._logger.verbose("Request DATA: {}".format(postdata)) + requests_cmd.data = postdata + if formfile is not None: + self._logger.verbose("Request FILES: {}".format(formfile)) + requests_cmd.files = formfile + if filename is not None: with open(filename, "rb") as stream: - postdata = stream.read() + filedata = stream.read() self._logger.verbose("Request POSTFIELDS: Binary content") - requests_cmd.data = postdata + requests_cmd.data = filedata if put_method: self._logger.info( @@ -176,6 +181,7 @@ class Http(http.Http): self, endpoint="", postfields_dict=None, + postdata=None, formfile=None, filename=None, skip_query_admin=False, @@ -184,6 +190,7 @@ class Http(http.Http): return self.send_cmd( endpoint=endpoint, postfields_dict=postfields_dict, + postdata=postdata, formfile=formfile, filename=filename, put_method=False, @@ -195,6 +202,7 @@ class Http(http.Http): self, endpoint="", postfields_dict=None, + postdata=None, formfile=None, filename=None, skip_query_admin=False, @@ -203,6 +211,7 @@ class Http(http.Http): return self.send_cmd( endpoint=endpoint, postfields_dict=postfields_dict, + postdata=postdata, formfile=formfile, filename=filename, put_method=True, @@ -214,6 +223,7 @@ class Http(http.Http): self, endpoint="", postfields_dict=None, + postdata=None, formfile=None, filename=None, skip_query_admin=False, @@ -222,6 +232,7 @@ class Http(http.Http): return self.send_cmd( endpoint=endpoint, postfields_dict=postfields_dict, + postdata=postdata, formfile=formfile, filename=filename, put_method=False, diff --git a/osmclient/sol005/infra_config_profile.py b/osmclient/sol005/infra_config_profile.py new file mode 100644 index 0000000..b342fa8 --- /dev/null +++ b/osmclient/sol005/infra_config_profile.py @@ -0,0 +1,34 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 Infra Config Profile API handling +""" + +from osmclient.sol005.osm_api_object import GenericOSMAPIObject + + +class InfraConfigProfile(GenericOSMAPIObject): + def __init__(self, http=None, client=None): + super().__init__(http, client) + self._apiName = "/k8scluster" + self._apiVersion = "/v1" + self._apiResource = "/infra_config_profiles" + self._logObjectName = "infra_config_profile" + self._apiBase = "{}{}{}".format( + self._apiName, self._apiVersion, self._apiResource + ) diff --git a/osmclient/sol005/infra_controller_profile.py b/osmclient/sol005/infra_controller_profile.py new file mode 100644 index 0000000..d4a6c19 --- /dev/null +++ b/osmclient/sol005/infra_controller_profile.py @@ -0,0 +1,34 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 Infra Controller Profile API handling +""" + +from osmclient.sol005.osm_api_object import GenericOSMAPIObject + + +class InfraControllerProfile(GenericOSMAPIObject): + def __init__(self, http=None, client=None): + super().__init__(http, client) + self._apiName = "/k8scluster" + self._apiVersion = "/v1" + self._apiResource = "/infra_controller_profiles" + self._logObjectName = "infra_controller_profile" + self._apiBase = "{}{}{}".format( + self._apiName, self._apiVersion, self._apiResource + ) diff --git a/osmclient/sol005/k8scluster.py b/osmclient/sol005/k8scluster.py index ff6822e..88e6e2b 100644 --- a/osmclient/sol005/k8scluster.py +++ b/osmclient/sol005/k8scluster.py @@ -62,7 +62,7 @@ class K8scluster(object): deleteFlag=deleteFlag, ) - def create(self, name, k8s_cluster, wait=False): + def add(self, name, k8s_cluster, wait=False): self._client.get_token() vim_account = self._get_vim_account(k8s_cluster["vim_account"]) k8s_cluster["vim_account"] = vim_account["_id"] @@ -132,7 +132,7 @@ class K8scluster(object): return cluster["_id"] raise NotFound("K8s cluster {} not found".format(name)) - def delete(self, name, force=False, wait=False): + def remove(self, name, force=False, wait=False): self._client.get_token() cluster_id = name if not utils.validate_uuid4(name): diff --git a/osmclient/sol005/ksu.py b/osmclient/sol005/ksu.py new file mode 100644 index 0000000..c686e8d --- /dev/null +++ b/osmclient/sol005/ksu.py @@ -0,0 +1,34 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 KSU API handling +""" + +from osmclient.sol005.osm_api_object import GenericOSMAPIObject + + +class KSU(GenericOSMAPIObject): + def __init__(self, http=None, client=None): + super().__init__(http, client) + self._apiName = "/ksu" + self._apiVersion = "/v1" + self._apiResource = "/ksus" + self._logObjectName = "ksu" + self._apiBase = "{}{}{}".format( + self._apiName, self._apiVersion, self._apiResource + ) diff --git a/osmclient/sol005/oka.py b/osmclient/sol005/oka.py new file mode 100644 index 0000000..539bc40 --- /dev/null +++ b/osmclient/sol005/oka.py @@ -0,0 +1,34 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 Kubernetes Application (OKA) API handling +""" + +from osmclient.sol005.osm_api_object import GenericOSMAPIObject + + +class OKA(GenericOSMAPIObject): + def __init__(self, http=None, client=None): + super().__init__(http, client) + self._apiName = "/oka" + self._apiVersion = "/v1" + self._apiResource = "/oka_packages" + self._logObjectName = "oka_package" + self._apiBase = "{}{}{}".format( + self._apiName, self._apiVersion, self._apiResource + ) diff --git a/osmclient/sol005/osm_api_object.py b/osmclient/sol005/osm_api_object.py new file mode 100644 index 0000000..4ce94b9 --- /dev/null +++ b/osmclient/sol005/osm_api_object.py @@ -0,0 +1,344 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 Generic OSM API Object +Abstract base class used by other objects +""" + +from abc import ABC, abstractmethod +from osmclient.common.exceptions import NotFound +from osmclient.common.exceptions import ClientException +from osmclient.common import utils +import json +import logging +import magic +import os +import shutil +import tempfile + + +class GenericOSMAPIObject(ABC): + @abstractmethod + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = "/generic" + self._apiVersion = "/v1" + self._apiResource = "/objects" + self._logObjectName = "object" + self._apiBase = "{}{}{}".format( + self._apiName, self._apiVersion, self._apiResource + ) + self._logger = logging.getLogger("osmclient") + + def set_http_headers(self, filename, multipart=False): + headers = self._client._headers + if multipart: + # requests library should know which Content-Type to set + # headers["Content-Type"] = "multipart/form-data" + headers.pop("Content-Type") + return + mime_type = magic.from_file(filename, mime=True) + if mime_type is None: + raise ClientException( + "Unexpected MIME type for file {}: MIME type {}".format( + filename, mime_type + ) + ) + headers["Content-Filename"] = os.path.basename(filename) + if mime_type in ["application/yaml", "text/plain", "application/json"]: + headers["Content-Type"] = "text/plain" + elif mime_type in ["application/gzip", "application/x-gzip"]: + headers["Content-Type"] = "application/gzip" + elif mime_type in ["application/zip"]: + headers["Content-Type"] = "application/zip" + else: + raise ClientException( + "Unexpected MIME type for file {}: MIME type {}".format( + filename, mime_type + ) + ) + headers["Content-File-MD5"] = utils.md5(filename) + + def generate_package(self, name, path, basedir): + """Generate a package (tar.gz) from path""" + self._logger.debug("") + if not os.path.exists(path): + raise ClientException(f"The specified {path} does not exist") + try: + self._logger.debug(f"basename: {os.path.join(basedir, name)}") + self._logger.debug(f"root_dir: {path}") + zip_package = shutil.make_archive( + base_name=os.path.join(basedir, name), + format="gztar", + root_dir=path, + ) + self._logger.info(f"Package created: {zip_package}") + return zip_package + except Exception as exc: + raise ClientException("failure during build of zip file: {}".format(exc)) + + def list(self, filter=None): + """List all Generic OSM API Object""" + self._logger.debug("") + self._client.get_token() + filter_string = "" + if filter: + filter_string = "?{}".format(filter) + _, resp = self._http.get2_cmd("{}{}".format(self._apiBase, filter_string)) + + if resp: + return json.loads(resp) + return list() + + def get(self, name): + """ + Gets and shows an individual Generic OSM API Object + from the list of all objects of the same kind + """ + self._logger.debug("") + self._client.get_token() + if utils.validate_uuid4(name): + for item in self.list(): + if name == item["_id"]: + return item + else: + for item in self.list(): + if "_id" in item and name == item["_id"]: + return item + elif "name" in item and name == item["name"]: + return item + raise NotFound(f"{self._logObjectName} {name} not found") + + def get_individual(self, name): + """Gets and shows an individual Generic OSM API Object""" + # It is redundant, since the method getalready gets the whole item + # The only difference is that a different primitive is exercised + self._logger.debug("") + item = self.get(name) + try: + _, resp = self._http.get2_cmd("{}/{}".format(self._apiBase, item["_id"])) + if resp: + return json.loads(resp) + except NotFound: + raise NotFound(f"{self._logObjectName} '{name}' not found") + raise NotFound(f"{self._logObjectName} '{name}' not found") + + def delete(self, name, force=False): + """Delete the Generic OSM API Object specified by name""" + self._logger.debug("") + self._client.get_token() + item = self.get(name) + querystring = "" + if force: + querystring = "?FORCE=True" + http_code, resp = self._http.delete_cmd( + "{}/{}{}".format(self._apiBase, item["_id"], querystring) + ) + + if http_code == 202: + print("Deletion in progress") + elif http_code == 204: + print("Deleted") + else: + msg = resp or "" + raise ClientException( + f"failed to delete {self._logObjectName} {name} - {msg}" + ) + + def generic_post(self, endpoint, content): + return self._http.post_cmd( + endpoint=endpoint, + postfields_dict=content, + skip_query_admin=True, + ) + + def multi_create_update(self, content, endpoint_suffix): + """Create or update a bundle of Generic OSM API Object specified by content""" + self._logger.debug("") + self._client.get_token() + endpoint = f"{self._apiBase}/{endpoint_suffix}" + http_code, resp = self.generic_post(endpoint=endpoint, content=content) + if http_code in (200, 201, 202): + if not resp: + raise ClientException(f"unexpected response from server - {resp}") + resp = json.loads(resp) + print(resp) + elif http_code == 204: + print("Received") + + def multi_delete(self, name_list, prefix="", force=False): + """Delete the list of Generic OSM API Object specified by name_list""" + self._logger.debug("") + self._client.get_token() + + # Get the id for each element + item_list = [] + for name in name_list: + item = self.get(name) + item_list.append(item["_id"]) + + # Prepare delete_content + if not prefix: + delete_content = item_list + else: + delete_content = {prefix: []} + for i in item_list: + delete_content[prefix].append({"_id": i}) + + querystring = "" + if force: + querystring = "?FORCE=True" + http_code, resp = self.generic_post( + endpoint=f"{self._apiBase}/delete{querystring}", content=delete_content + ) + if http_code in (201, 202): + print("Deletion in progress") + elif http_code in (200, 204): + print("Deleted") + else: + msg = resp or "" + raise ClientException( + f"failed to create {self._logObjectName}. Http code: {http_code}. Message: {msg}" + ) + + def create(self, name, content_dict=None, filename=None): + """Creates a new Generic OSM API Object""" + self._logger.debug(f"Creating Generic OSM API Object {name}") + self._client.get_token() + # If filename is dir, generate a tar.gz file + tempdir = None + if filename: + if os.path.isdir(filename): + tempdir = tempfile.TemporaryDirectory() + basedir = tempdir.name + filename = filename.rstrip("/") + filename = self.generate_package(name, filename, basedir) + if content_dict and not filename: + # Typical case. Only a dict + self._logger.debug("Sending only a dict") + _, resp = self._http.post_cmd( + endpoint=self._apiBase, + postfields_dict=content_dict, + skip_query_admin=True, + ) + elif not content_dict and not filename: + # No content at all. Raise error + raise ClientException( + f"failed to create {self._logObjectName} {name} - No content to be sent" + ) + elif content_dict and filename: + self._logger.debug("Sending a multipart file: a dict and a file") + # It's a form and data + formfile = { + "package": ( + os.path.basename(filename), + open(filename, "rb"), + "application/gzip", + ), + } + self._logger.debug(f"formfile: {formfile}") + self.set_http_headers(filename, multipart=True) + _, resp = self._http.post_cmd( + endpoint=self._apiBase, postdata=content_dict, formfile=formfile + ) + elif not content_dict and filename: + self._logger.debug("Sending only a file") + # Only a file to be sent + self.set_http_headers(filename) + _, resp = self._http.post_cmd(endpoint=self._apiBase, filename=filename) + if resp: + resp = json.loads(resp) + self._logger.debug(f"Resp: {resp}") + if not resp or "_id" not in resp: + raise ClientException("Unexpected response from server - {}".format(resp)) + print(resp["_id"]) + if tempdir: + tempdir.cleanup() + + def update(self, name, changes_dict=None, filename=None, force_multipart=False): + """Updates a Generic OSM API Object""" + self._logger.debug("") + self._client.get_token() + item = self.get(name) + + formfile = None + tempdir = None + if filename: + # If filename is dir, generate a tar.gz file + if os.path.isdir(filename): + tempdir = tempfile.TemporaryDirectory() + basedir = tempdir.name + filename = filename.rstrip("/") + filename = self.generate_package(name, filename, basedir) + self._logger.debug("Sending a multipart file: a dict and a file") + # It's a form and data + formfile = { + "package": (filename, open(filename, "rb"), "application/gzip"), + } + self.set_http_headers(filename, multipart=True) + + if not filename and force_multipart: + self.set_http_headers(filename, multipart=True) + + http_code, resp = self._http.patch_cmd( + endpoint="{}/{}".format(self._apiBase, item["_id"]), + postfields_dict=changes_dict, + formfile=formfile, + skip_query_admin=True, + ) + if http_code in (200, 201, 202): + if resp: + resp = json.loads(resp) + if not resp or "id" not in resp: + raise ClientException(f"unexpected response from server - {resp}") + print(resp["id"]) + elif http_code == 204: + print("Updated") + + def update_content(self, name, filename): + """Updates the content of a Generic OSM API Object""" + self._logger.debug("") + self._client.get_token() + item = self.get(name) + + tempdir = None + if filename: + if os.path.isdir(filename): + tempdir = tempfile.TemporaryDirectory() + basedir = tempdir.name + filename = filename.rstrip("/") + filename = self.generate_package(name, filename, basedir) + self._logger.debug("Sending only a file") + # Only a file to be sent + self.set_http_headers(filename) + http_code, resp = self._http.put_cmd( + endpoint="{}/{}".format(self._apiBase, item["_id"]), + filename=filename, + skip_query_admin=True, + ) + if http_code in (200, 201, 202): + if resp: + resp = json.loads(resp) + if not resp or "id" not in resp: + raise ClientException(f"unexpected response from server - {resp}") + print(resp["id"]) + elif http_code == 204: + print("Updated") + if tempdir: + tempdir.cleanup() diff --git a/osmclient/sol005/resource_profile.py b/osmclient/sol005/resource_profile.py new file mode 100644 index 0000000..2cc009d --- /dev/null +++ b/osmclient/sol005/resource_profile.py @@ -0,0 +1,34 @@ +####################################################################################### +# Copyright ETSI Contributors and Others. +# +# 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 Resource Profile API handling +""" + +from osmclient.sol005.osm_api_object import GenericOSMAPIObject + + +class ResourceProfile(GenericOSMAPIObject): + def __init__(self, http=None, client=None): + super().__init__(http, client) + self._apiName = "/k8scluster" + self._apiVersion = "/v1" + self._apiResource = "/resource_profiles" + self._logObjectName = "resource_profile" + self._apiBase = "{}{}{}".format( + self._apiName, self._apiVersion, self._apiResource + )