Feature 11073. Enhanced OSM declarative modelling for applications. App as first... 13/15313/2
authorgarciadeblas <gerardo.garciadeblas@telefonica.com>
Mon, 4 Aug 2025 11:58:45 +0000 (13:58 +0200)
committergarciadeblas <gerardo.garciadeblas@telefonica.com>
Mon, 4 Aug 2025 12:01:20 +0000 (14:01 +0200)
Change-Id: I1edf6f674170f2c00d62c69cbba46a2bda401e0f
Signed-off-by: garciadeblas <gerardo.garciadeblas@telefonica.com>
osmclient/cli_commands/app.py [new file with mode: 0755]
osmclient/scripts/osm.py
osmclient/sol005/app.py [new file with mode: 0644]
osmclient/sol005/client.py

diff --git a/osmclient/cli_commands/app.py b/osmclient/cli_commands/app.py
new file mode 100755 (executable)
index 0000000..98811fd
--- /dev/null
@@ -0,0 +1,330 @@
+#######################################################################################
+# 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 click
+import yaml
+from osmclient.cli_commands import common, utils
+from osmclient.common.exceptions import ClientException
+from osmclient.common import print_output
+
+logger = logging.getLogger("osmclient")
+
+
+def get_oka_id(ctx, oka_name):
+    """Get the ID of an OKA by name or ID"""
+    logger.debug("")
+    resp = ctx.obj.oka.get(oka_name)
+    logger.debug("OKA obtained: %s", 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):
+    """Get the ID of a profile of a specifc profile_type by name or ID"""
+    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("Profile obtained: %s", resp)
+    if "_id" in resp:
+        return resp["_id"]
+    else:
+        raise ClientException("Unexpected failure when reading the profile")
+
+
+@click.command(name="app-create", short_help="creates an App Instance")
+@utils.require_hostname
+@click.argument("name")
+@click.option("--description", default="", help="human readable description")
+@click.option(
+    "--oka",
+    default="",
+    help="name or ID of the OKA that the App Instance will be based on",
+)
+@click.option(
+    "--sw-catalog-path",
+    default="",
+    help="folder in the SW catalog (git repo) that the App Instance will be based on",
+)
+@click.option(
+    "--profile",
+    default="",
+    help="name or ID of the profile the App Instance will belong to",
+)
+@click.option(
+    "--profile-type",
+    type=click.Choice(
+        [
+            "infra-controller-profile",
+            "infra-config-profile",
+            "app-profile",
+            "resource-profile",
+        ]
+    ),
+    default="app-profile",
+    help="type of profile. Default: app-profile",
+)
+@click.option(
+    "--model",
+    default=None,
+    help="specific yaml file with App Instance model file (to be merged with App blueprint model file)",
+)
+@click.option(
+    "--params",
+    default=None,
+    help="specific yaml file with clear params for the App Instance",
+)
+@click.option(
+    "--secret-params",
+    default=None,
+    help="specific yaml file with secret params for the App Instance",
+)
+@click.pass_context
+def app_create(
+    ctx,
+    name,
+    description,
+    oka,
+    sw_catalog_path,
+    profile,
+    profile_type,
+    model,
+    params,
+    secret_params,
+):
+    """creates an App Instance
+
+    NAME: name of the App Instance to be created
+    """
+    logger.debug("")
+    profile_type_mapping = {
+        "infra-controller-profile": "infra_controller_profiles",
+        "infra-config-profile": "infra_config_profiles",
+        "app-profile": "app_profiles",
+        "resource-profile": "resource_profiles",
+    }
+    app = {}
+    app["name"] = name
+    if description:
+        app["description"] = description
+    if oka and sw_catalog_path:
+        raise ClientException(
+            '"--oka" option is incompatible with "--sw-catalog-path" option'
+        )
+    if oka:
+        app["oka"] = get_oka_id(ctx, oka)
+    if sw_catalog_path:
+        app["sw_catalog_path"] = sw_catalog_path
+    app["profile_type"] = profile_type_mapping[profile_type]
+    app["profile"] = get_profile_id(ctx, profile, profile_type)
+    if model:
+        with open(model, "r", encoding="utf-8") as cf:
+            model_data = cf.read()
+        try:
+            app["model"] = yaml.safe_load(model_data)
+        except yaml.YAMLError as e:
+            raise ClientException(f"Error parsing YAML configuration: {e}") from e
+    if params:
+        with open(params, "r", encoding="utf-8") as cf:
+            params_data = cf.read()
+        try:
+            app["params"] = yaml.safe_load(params_data)
+        except yaml.YAMLError as e:
+            raise ClientException(f"Error parsing YAML configuration: {e}") from e
+    if secret_params:
+        with open(secret_params, "r", encoding="utf-8") as cf:
+            secret_params_data = cf.read()
+        try:
+            app["secret_params"] = yaml.safe_load(secret_params_data)
+        except yaml.YAMLError as e:
+            raise ClientException(f"Error parsing YAML configuration: {e}") from e
+    ctx.obj.app.create(name, content_dict=app)
+
+
+@click.command(name="app-delete", short_help="deletes an App Instance cluster")
+@utils.require_hostname
+@click.argument("name")
+@click.option(
+    "--force", is_flag=True, help="forces the deletion from the DB (not recommended)"
+)
+@click.pass_context
+def app_delete(ctx, name, force):
+    """deletes an App Instance
+
+    NAME: name or ID of the App Instance to be deleted
+    """
+    logger.debug("")
+    ctx.obj.app.delete(name, force=force)
+
+
+@click.command(name="app-list")
+@utils.require_hostname
+@click.option(
+    "--filter",
+    help="restricts the list to the items matching the filter",
+)
+@print_output.output_option
+@click.pass_context
+def app_list(ctx, filter, output):
+    """list all App instances"""
+    logger.debug("")
+    common.generic_list(callback=ctx.obj.app.list, filter=filter, format=output)
+
+
+@click.command(name="app-show", short_help="shows the details of an app instance")
+@utils.require_hostname
+@click.argument("name")
+@print_output.output_option
+@click.pass_context
+def app_show(ctx, name, output):
+    """shows the details of an App instance
+
+    NAME: name or ID of the App instance to be shown
+    """
+    logger.debug("")
+    common.generic_show(callback=ctx.obj.app.get, name=name, format=output)
+
+
+@click.command(
+    name="app-edit", short_help="updates name or description of an App Instance"
+)
+@utils.require_hostname
+@click.argument("name")
+@click.option("--newname", help="New name for the App Instance")
+@click.option("--description", help="human readable description")
+@click.pass_context
+def app_edit(ctx, name, newname, description, **kwargs):
+    """updates the name or description of an App Instance
+
+    NAME: name or ID of the App Instance to be updated
+    """
+    logger.debug("")
+    app_changes = common.generic_update(newname, description, kwargs)
+    ctx.obj.app.update(name, changes_dict=app_changes)
+
+
+@click.command(name="app-update", short_help="updates an App Instance")
+@utils.require_hostname
+@click.argument("name")
+@click.option("--newname", help="New name for the App Instance")
+@click.option("--description", default="", help="human readable description")
+@click.option(
+    "--oka",
+    default="",
+    help="name or ID of the OKA that the App Instance will be based on",
+)
+@click.option(
+    "--sw-catalog-path",
+    default="",
+    help="folder in the SW catalog (git repo) that the App Instance will be based on",
+)
+@click.option(
+    "--profile",
+    default="",
+    help="name or ID of the profile the App Instance will belong to",
+)
+@click.option(
+    "--profile-type",
+    type=click.Choice(
+        [
+            "infra-controller-profile",
+            "infra-config-profile",
+            "app-profile",
+            "resource-profile",
+        ]
+    ),
+    default="app-profile",
+    help="type of profile. Default: app-profile",
+)
+@click.option(
+    "--model",
+    default=None,
+    help="specific yaml file with App Instance model file (to be merged with App blueprint model file)",
+)
+@click.option(
+    "--params",
+    default=None,
+    help="specific yaml file with clear params for the App Instance",
+)
+@click.option(
+    "--secret-params",
+    default=None,
+    help="specific yaml file with secret params for the App Instance",
+)
+@click.pass_context
+def app_update(
+    ctx,
+    name,
+    oka,
+    sw_catalog_path,
+    profile,
+    profile_type,
+    model,
+    params,
+    secret_params,
+):
+    """updates an App Instance
+
+    NAME: name or ID of the App Instance to be updated
+    """
+    logger.debug("")
+    app_changes = {}
+    profile_type_mapping = {
+        "infra-controller-profile": "infra_controller_profiles",
+        "infra-config-profile": "infra_config_profiles",
+        "app-profile": "app_profiles",
+        "resource-profile": "resource_profiles",
+    }
+    if oka and sw_catalog_path:
+        raise ClientException(
+            '"--oka" option is incompatible with "--sw-catalog-path" option'
+        )
+    if oka:
+        app_changes["oka"] = get_oka_id(ctx, oka)
+    if sw_catalog_path:
+        app_changes["sw_catalog_path"] = sw_catalog_path
+    app_changes["profile_type"] = profile_type_mapping[profile_type]
+    app_changes["profile"] = get_profile_id(ctx, profile, profile_type)
+    if model:
+        with open(model, "r", encoding="utf-8") as cf:
+            model_data = cf.read()
+        try:
+            app_changes["model"] = yaml.safe_load(model_data)
+        except yaml.YAMLError as e:
+            raise ClientException(f"Error parsing YAML configuration: {e}") from e
+    if params:
+        with open(params, "r", encoding="utf-8") as cf:
+            params_data = cf.read()
+        try:
+            app_changes["params"] = yaml.safe_load(params_data)
+        except yaml.YAMLError as e:
+            raise ClientException(f"Error parsing YAML configuration: {e}") from e
+    if secret_params:
+        with open(secret_params, "r", encoding="utf-8") as cf:
+            secret_params_data = cf.read()
+        try:
+            app_changes["secret_params"] = yaml.safe_load(secret_params_data)
+        except yaml.YAMLError as e:
+            raise ClientException(f"Error parsing YAML configuration: {e}") from e
+    ctx.obj.app.fullupdate(name, app_changes)
index b63d660..323a5b6 100755 (executable)
@@ -18,6 +18,7 @@ from osmclient import client
 from osmclient.common.exceptions import ClientException, NotFound
 from osmclient.cli_commands import (
     alarms,
+    app,
     app_profile,
     cluster,
     infra_config_profile,
@@ -179,6 +180,13 @@ def cli():
         cli_osm.add_command(ksu.ksu_show)
         cli_osm.add_command(ksu.ksu_update)
 
+        cli_osm.add_command(app.app_create)
+        cli_osm.add_command(app.app_delete)
+        cli_osm.add_command(app.app_edit)
+        cli_osm.add_command(app.app_list)
+        cli_osm.add_command(app.app_show)
+        cli_osm.add_command(app.app_update)
+
         cli_osm.add_command(cluster.cluster_create)
         cli_osm.add_command(cluster.cluster_delete)
         cli_osm.add_command(cluster.cluster_list)
diff --git a/osmclient/sol005/app.py b/osmclient/sol005/app.py
new file mode 100644 (file)
index 0000000..addb32e
--- /dev/null
@@ -0,0 +1,43 @@
+#######################################################################################
+# 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 Instaance API handling
+"""
+
+from osmclient.sol005.osm_api_object import GenericOSMAPIObject
+
+
+class AppInstance(GenericOSMAPIObject):
+    def __init__(self, http=None, client=None):
+        super().__init__(http, client)
+        self._apiName = "/appinstance"
+        self._apiVersion = "/v1"
+        self._apiResource = "/appinstances"
+        self._logObjectName = "appinstance"
+        self._apiBase = "{}{}{}".format(
+            self._apiName, self._apiVersion, self._apiResource
+        )
+
+    def fullupdate(self, name, app_changes):
+        """
+        Updates an App Instance
+        """
+        self._logger.debug("")
+        item = self.get(name)
+        endpoint_suffix = f"{item['_id']}/update"
+        self.generic_operation(app_changes, endpoint_suffix=endpoint_suffix)
index bbe0536..0d90727 100644 (file)
@@ -42,6 +42,7 @@ 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 app
 from osmclient.sol005 import infra_controller_profile
 from osmclient.sol005 import infra_config_profile
 from osmclient.sol005 import app_profile
@@ -113,6 +114,7 @@ class Client(object):
         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.app = app.AppInstance(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