From 456ffc2decf7e4aafa3eb00479433f53b532a332 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Mon, 4 Aug 2025 13:58:45 +0200 Subject: [PATCH] Feature 11073. Enhanced OSM declarative modelling for applications. App as first class citizen Change-Id: I1edf6f674170f2c00d62c69cbba46a2bda401e0f Signed-off-by: garciadeblas --- osmclient/cli_commands/app.py | 330 ++++++++++++++++++++++++++++++++++ osmclient/scripts/osm.py | 8 + osmclient/sol005/app.py | 43 +++++ osmclient/sol005/client.py | 2 + 4 files changed, 383 insertions(+) create mode 100755 osmclient/cli_commands/app.py create mode 100644 osmclient/sol005/app.py diff --git a/osmclient/cli_commands/app.py b/osmclient/cli_commands/app.py new file mode 100755 index 0000000..98811fd --- /dev/null +++ b/osmclient/cli_commands/app.py @@ -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) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index b63d660..323a5b6 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -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 index 0000000..addb32e --- /dev/null +++ b/osmclient/sol005/app.py @@ -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) diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py index bbe0536..0d90727 100644 --- a/osmclient/sol005/client.py +++ b/osmclient/sol005/client.py @@ -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 -- 2.25.1