Features 11020,11022-11026 Advanced cluster mgmt 29/14329/27 release-v16.0-start v16.0.0
authorgarciadeblas <gerardo.garciadeblas@telefonica.com>
Thu, 25 Apr 2024 17:13:31 +0000 (19:13 +0200)
committergarciadeblas <gerardo.garciadeblas@telefonica.com>
Tue, 20 Aug 2024 13:07:54 +0000 (15:07 +0200)
Change-Id: I45d7fcb1f644448c99588af499fe3d805780220a
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
24 files changed:
osmclient/cli_commands/app_profile.py [new file with mode: 0755]
osmclient/cli_commands/cluster.py [new file with mode: 0755]
osmclient/cli_commands/common.py [new file with mode: 0644]
osmclient/cli_commands/infra_config_profile.py [new file with mode: 0755]
osmclient/cli_commands/infra_controller_profile.py [new file with mode: 0755]
osmclient/cli_commands/k8scluster.py
osmclient/cli_commands/ksu.py [new file with mode: 0755]
osmclient/cli_commands/nslcm.py
osmclient/cli_commands/oka.py [new file with mode: 0755]
osmclient/cli_commands/profiles.py [new file with mode: 0755]
osmclient/cli_commands/resource_profile.py [new file with mode: 0755]
osmclient/cli_commands/vim.py
osmclient/scripts/osm.py
osmclient/sol005/app_profile.py [new file with mode: 0644]
osmclient/sol005/client.py
osmclient/sol005/cluster.py [new file with mode: 0644]
osmclient/sol005/http.py
osmclient/sol005/infra_config_profile.py [new file with mode: 0644]
osmclient/sol005/infra_controller_profile.py [new file with mode: 0644]
osmclient/sol005/k8scluster.py
osmclient/sol005/ksu.py [new file with mode: 0644]
osmclient/sol005/oka.py [new file with mode: 0644]
osmclient/sol005/osm_api_object.py [new file with mode: 0644]
osmclient/sol005/resource_profile.py [new file with mode: 0644]

diff --git a/osmclient/cli_commands/app_profile.py b/osmclient/cli_commands/app_profile.py
new file mode 100755 (executable)
index 0000000..e85a19b
--- /dev/null
@@ -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 (executable)
index 0000000..15c83bc
--- /dev/null
@@ -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 (file)
index 0000000..7ba9368
--- /dev/null
@@ -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 (executable)
index 0000000..9229a6f
--- /dev/null
@@ -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 (executable)
index 0000000..6bab0e5
--- /dev/null
@@ -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)
index 77e1124..380d311 100755 (executable)
@@ -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 (executable)
index 0000000..b89fca3
--- /dev/null
@@ -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="<KSU> [<KSU>...]")
+@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")
index 355422c..d206db2 100755 (executable)
@@ -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 (executable)
index 0000000..dce5fa5
--- /dev/null
@@ -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 (executable)
index 0000000..93a41f3
--- /dev/null
@@ -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 (executable)
index 0000000..8efb959
--- /dev/null
@@ -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)
index 2f7bbfc..bb7622d 100755 (executable)
@@ -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,
index f67f550..78d3fff 100755 (executable)
@@ -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 (file)
index 0000000..3b5417e
--- /dev/null
@@ -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
+        )
index e0254c3..0b7d309 100644 (file)
@@ -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 (file)
index 0000000..9aebeab
--- /dev/null
@@ -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:
index 8dd87e7..5b140f5 100644 (file)
@@ -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 (file)
index 0000000..b342fa8
--- /dev/null
@@ -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 (file)
index 0000000..d4a6c19
--- /dev/null
@@ -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
+        )
index ff6822e..88e6e2b 100644 (file)
@@ -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 (file)
index 0000000..c686e8d
--- /dev/null
@@ -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 (file)
index 0000000..539bc40
--- /dev/null
@@ -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 (file)
index 0000000..4ce94b9
--- /dev/null
@@ -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 (file)
index 0000000..2cc009d
--- /dev/null
@@ -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
+        )