diff --git a/MODULE.bazel b/MODULE.bazel index ed1295ed..9ccb31e6 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -117,13 +117,14 @@ python.toolchain( python_version = PYTHON_VERSION, ) -pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True) +# Python pip dependencies +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") pip.parse( - hub_name = "pip_score_venv_test", + hub_name = "score_lifecycle_pip", python_version = PYTHON_VERSION, - requirements_lock = "//tests/integration:requirements.lock", + requirements_lock = "//:requirements_lock.txt", ) -use_repo(pip, "pip_score_venv_test") +use_repo(pip, "score_lifecycle_pip") bazel_dep(name = "score_baselibs_rust", version = "0.1.0") bazel_dep(name = "score_baselibs", version = "0.2.4") diff --git a/examples/config/gen_common_cfg.py b/defs.bzl similarity index 63% rename from examples/config/gen_common_cfg.py rename to defs.bzl index 815d7f0b..76ab3cc6 100644 --- a/examples/config/gen_common_cfg.py +++ b/defs.bzl @@ -10,10 +10,10 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -def get_process_index_range(process_count: int, process_group_index: int): - # Every ProcessGroup gets the same number of processes - # The Process Index is a globally unique increasing number - return range( - process_group_index * process_count, - (process_group_index * process_count) + process_count, - ) + +"""Unified entrypoint for lifecycle Bazel macros & rules.""" + +# --- Launch Manager Configuration --- +load("//scripts/config_mapping:config.bzl", _launch_manager_config = "launch_manager_config") + +launch_manager_config = _launch_manager_config diff --git a/examples/BUILD b/examples/BUILD new file mode 100644 index 00000000..5b76c1a1 --- /dev/null +++ b/examples/BUILD @@ -0,0 +1,49 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("//:defs.bzl", "launch_manager_config") +load(":run_examples.bzl", "run_examples") + +launch_manager_config( + name = "example_config", + config = "//examples/config:lifecycle_demo_config", +) + +filegroup( + name = "example_apps", + srcs = [ + "//examples:example_config", + "//examples/control_application:control_daemon", + "//examples/control_application:lmcontrol", + "//examples/cpp_lifecycle_app", + "//examples/cpp_supervised_app", + "//examples/rust_supervised_app", + ], +) + +filegroup( + name = "lm_binaries", + srcs = [ + "//src/control_client_lib", + "//src/launch_manager_daemon:launch_manager", + "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", + "//src/launch_manager_daemon/process_state_client_lib:process_state_client", + ], +) + +run_examples( + name = "run_examples", + deps = [ + ":example_apps", + ":lm_binaries", + ], +) diff --git a/examples/README.md b/examples/README.md index 6ffd56b6..385ff981 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,23 +2,20 @@ ## Building & Running the demo setup -1. Build launch_manager and health_monitor, in the parent folder, first. -2. Then start run.sh script in this folder: `cd demo && ./run.sh` +Execute `bazel run //examples:run_examples --config=<...>`. This will build all dependences and run the run.sh script The run.sh script will: -- Copy the required binaries to a temporary directory demo/tmp -- Compile the json configuration to flatbuffer using flatc - Build a docker image for execution with the required artifacts inside - Start the docker container that runs launch_manager ## Interacting with the Demo -### Changing ProcessGroup States +### Changing RunTargets -There is a CLI application that allows to request transition to a certain ProcessGroup State. +There is a CLI application that allows to request transition to a certain RunTarget. -Example: `lmcontrol ProcessGroup1/Startup` +Example: `lmcontrol Startup` ### Triggering Supervision Failure @@ -31,4 +28,4 @@ Example: `fail ` There is an interactive mode that walks you through two demo scenarios. This mode requires the run.sh script to be executed **in an active tmux** session. -`cd demo && ./run.sh tmux` +`bazel run //examples:run_examples --config=<...> -- tmux` diff --git a/examples/config/BUILD b/examples/config/BUILD new file mode 100644 index 00000000..16a9aefc --- /dev/null +++ b/examples/config/BUILD @@ -0,0 +1,22 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +exports_files( + ["lifecycle_demo.json"], + visibility = ["//examples:__subpackages__"], +) + +filegroup( + name = "lifecycle_demo_config", + srcs = ["lifecycle_demo.json"], + visibility = ["//examples:__subpackages__"], +) diff --git a/examples/config/gen_health_monitor_cfg.py b/examples/config/gen_health_monitor_cfg.py deleted file mode 100644 index f79f6a30..00000000 --- a/examples/config/gen_health_monitor_cfg.py +++ /dev/null @@ -1,338 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -import argparse -from pathlib import Path -import os -import json -from gen_common_cfg import get_process_index_range - - -def get_process(index: int, process_group: str): - return ( - """ -{ - "index": """ - + str(index) - + """, - "shortName": "demo_application""" - + str(index) - + """", - "identifier": "demo_app""" - + str(index) - + "_" - + process_group - + '''", - "processType": "REGULAR_PROCESS", - "refProcessGroupStates": [ - { - "identifier": "''' - + process_group - + """/Startup" - } - ], - "processExecutionErrors": [ - { - "processExecutionError": 1 - } - ] -} -""" - ) - - -def get_monitor_interfaces(index: int, process_group: str): - return ( - """ -{ - "instanceSpecifier": "demo/demo_application""" - + str(index) - + """/Port1", - "processShortName": "demo_application""" - + str(index) - + """", - "portPrototype": "Port1", - "interfacePath": "demo_application_""" - + str(index) - + "_" - + process_group - + """", - "refProcessIndex": """ - + str(index) - + """, - "permittedUid": 0 -} -""" - ) - - -def get_checkpoints(index: int): - # Every demo app has three checkpoints - return [ - """ -{ - "shortName": "Checkpoint""" - + str(index) - + "_1" - + """", - "checkpointId": 1, - "refInterfaceIndex": """ - + str(index) - + """ -}""" - ] - - -def get_alive_supervisions(index: int, process_group: str): - # Every demo app has three checkpoints and the first checkpoint is used for alive supervision - checkpointIdx = index * 1 - return ( - """ -{ - "ruleContextKey": "AliveSupervision""" - + str(index) - + """", - "refCheckPointIndex": """ - + str(checkpointIdx) - + """, - "aliveReferenceCycle": 100.0, - "minAliveIndications": 1, - "maxAliveIndications": 3, - "isMinCheckDisabled": false, - "isMaxCheckDisabled": false, - "failedSupervisionCyclesTolerance": 1, - "refProcessIndex": """ - + str(index) - + ''', - "refProcessGroupStates": [ - { - "identifier": "''' - + process_group - + """/Startup" - } - ] -} -""" - ) - - -def get_local_supervisions(index: int): - return ( - """ -{ - "ruleContextKey": "LocalSupervision""" - + str(index) - + """", - "infoRefInterfacePath": "demo_application_""" - + str(index) - + """", - "hmRefAliveSupervision": [ - { - "refAliveSupervisionIdx": """ - + str(index) - + """ - } - ] -} -""" - ) - - -def get_global_supervisions( - process_count: int, process_group_index: int, process_group: str -): - localSupervisionRefs = [] - processRefs = [] - for i in get_process_index_range(process_count, process_group_index): - localSupervisionRefs.append( - json.loads( - """ -{ - "refLocalSupervisionIndex": """ - + str(i) - + """ -}""" - ) - ) - processRefs.append( - json.loads( - """ -{ - "index": """ - + str(i) - + """ -}""" - ) - ) - - globalSupervisions = json.loads( - """ -{ - "ruleContextKey": "GlobalSupervision_""" - + process_group - + '''", - "isSeverityCritical": false, - "localSupervision": [], - "refProcesses": [], - "refProcessGroupStates": [ - { - "identifier": "''' - + process_group - + """/Startup" - } - ] -} -""" - ) - globalSupervisions["localSupervision"].extend(localSupervisionRefs) - globalSupervisions["refProcesses"].extend(processRefs) - return json.dumps(globalSupervisions) - - -def get_recovery_notifications( - process_count: int, process_group_index: int, process_group: str -): - return ( - """ -{ - "shortName" : "RecoveryNotification_""" - + process_group - + '''", - "recoveryNotificationTimeout" : 4000.0, - "processGroupMetaModelIdentifier" : "''' - + process_group - + """/Recovery", - "refGlobalSupervisionIndex" : """ - + str(process_group_index) - + """, - "instanceSpecifier" : "", - "shouldFireWatchdog" : false -} -""" - ) - - -def gen_health_monitor_cfg_for_process_group( - config, process_count: int, process_group: str, process_group_index: int -): - processes = [] - monitorInterfaces = [] - checkpoints = [] - hmAliveSupervisions = [] - hmLocalSupervisions = [] - hmGlobalSupervision = [] - hmRecoveryNotifications = [] - - for process_index in get_process_index_range(process_count, process_group_index): - print(f"process Index {process_index} for FG {process_group}") - processes.append(json.loads(get_process(process_index, process_group))) - monitorInterfaces.append( - json.loads(get_monitor_interfaces(process_index, process_group)) - ) - - for cp in get_checkpoints(process_index): - checkpoints.append(json.loads(cp)) - - hmAliveSupervisions.append( - json.loads(get_alive_supervisions(process_index, process_group)) - ) - hmLocalSupervisions.append(json.loads(get_local_supervisions(process_index))) - - hmGlobalSupervision.append( - json.loads( - get_global_supervisions(process_count, process_group_index, process_group) - ) - ) - hmRecoveryNotifications.append( - json.loads( - get_recovery_notifications( - process_count, process_group_index, process_group - ) - ) - ) - - config["process"].extend(processes) - config["hmMonitorInterface"].extend(monitorInterfaces) - config["hmSupervisionCheckpoint"].extend(checkpoints) - config["hmAliveSupervision"].extend(hmAliveSupervisions) - config["hmLocalSupervision"].extend(hmLocalSupervisions) - config["hmGlobalSupervision"].extend(hmGlobalSupervision) - config["hmRecoveryNotification"].extend(hmRecoveryNotifications) - return config - - -def gen_health_monitor_cfg(process_count: int, process_groups: list): - config = json.loads( - """ -{ - "versionMajor": 8, - "versionMinor": 0, - "process": [], - "hmMonitorInterface": [], - "hmSupervisionCheckpoint": [], - "hmAliveSupervision": [], - "hmDeadlineSupervision": [], - "hmLogicalSupervision": [], - "hmLocalSupervision": [], - "hmGlobalSupervision": [], - "hmRecoveryNotification": [] -} -""" - ) - - for i in range(0, len(process_groups)): - gen_health_monitor_cfg_for_process_group( - config, process_count, process_groups[i], i - ) - - return config - - -if __name__ == "__main__": - my_parser = argparse.ArgumentParser() - my_parser.add_argument( - "-c", - "--cppprocesses", - action="store", - type=int, - required=True, - help="Number of C++ processes", - ) - my_parser.add_argument( - "-r", - "--rustprocesses", - action="store", - type=int, - required=True, - help="Number of Rust processes", - ) - my_parser.add_argument( - "-p", - "--process_groups", - nargs="+", - help="Name of a Process Group", - required=True, - ) - my_parser.add_argument( - "-o", "--out", action="store", type=Path, required=True, help="Output directory" - ) - args = my_parser.parse_args() - - cfg_out_path = os.path.join(args.out, f"hm_demo.json") - with open(cfg_out_path, "w") as f: - json.dump( - gen_health_monitor_cfg( - args.cppprocesses + args.rustprocesses, args.process_groups - ), - f, - indent=4, - ) diff --git a/examples/config/gen_health_monitor_process_cfg.py b/examples/config/gen_health_monitor_process_cfg.py deleted file mode 100644 index 357cb7e4..00000000 --- a/examples/config/gen_health_monitor_process_cfg.py +++ /dev/null @@ -1,90 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -import argparse -from pathlib import Path -import os -from gen_common_cfg import get_process_index_range - - -def gen_health_monitor_process_cfg(index: int, process_group: str): - config = ( - """ -{ - "versionMajor": 8, - "versionMinor": 0, - "process": [], - "hmMonitorInterface": [ - { - "instanceSpecifier": "demo/demo_application""" - + str(index) - + """/Port1", - "processShortName": "demo_application""" - + str(index) - + """", - "portPrototype": "Port1", - "interfacePath": "demo_application_""" - + str(index) - + "_" - + process_group - + """", - "refProcessIndex":0 - } - ] -} -""" - ) - return config - - -if __name__ == "__main__": - my_parser = argparse.ArgumentParser() - my_parser.add_argument( - "-c", - "--cppprocesses", - action="store", - type=int, - required=True, - help="Number of C++ processes", - ) - my_parser.add_argument( - "-r", - "--rustprocesses", - action="store", - type=int, - required=True, - help="Number of Rust processes", - ) - my_parser.add_argument( - "-p", - "--process_groups", - nargs="+", - help="Name of a Process Group", - required=True, - ) - my_parser.add_argument( - "-o", "--out", action="store", type=Path, required=True, help="Output directory" - ) - args = my_parser.parse_args() - - process_count = args.cppprocesses + args.rustprocesses - for process_group_index in range(0, len(args.process_groups)): - process_group = args.process_groups[process_group_index] - for process_index in get_process_index_range( - process_count, process_group_index - ): - cfg_out_path = os.path.join( - args.out, - f"health_monitor_process_cfg_{process_index}_{process_group}.json", - ) - with open(cfg_out_path, "w") as f: - f.write(gen_health_monitor_process_cfg(process_index, process_group)) diff --git a/examples/config/gen_launch_manager_cfg.py b/examples/config/gen_launch_manager_cfg.py deleted file mode 100644 index ea1dc511..00000000 --- a/examples/config/gen_launch_manager_cfg.py +++ /dev/null @@ -1,530 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 Contributors to the Eclipse Foundation -# -# See the NOTICE file(s) distributed with this work for additional -# information regarding copyright ownership. -# -# This program and the accompanying materials are made available under the -# terms of the Apache License Version 2.0 which is available at -# https://www.apache.org/licenses/LICENSE-2.0 -# -# SPDX-License-Identifier: Apache-2.0 -# ******************************************************************************* -import argparse -import json -from pathlib import Path -import os -from gen_common_cfg import get_process_index_range - - -class LaunchManagerConfGen: - def __init__(self): - # setup generator data structures - self.machines = [] - - def generate_json(self, out_path): - # generate all configured machines - for machine in self.machines: - json_config = { - "versionMajor": 7, - "versionMinor": 0, - } - - json_config["Process"] = [] - json_config["ModeGroup"] = [] - for process_group in machine["process_groups"].keys(): - # configuring processes - for process in machine["process_groups"][process_group][ - "processes" - ].keys(): - json_config["Process"].append( - { - "identifier": f"{process}", - "uid": machine["process_groups"][process_group][ - "processes" - ][process]["uid"], - "gid": machine["process_groups"][process_group][ - "processes" - ][process]["gid"], - "path": machine["process_groups"][process_group][ - "processes" - ][process]["executable_name"], - } - ) - - if ( - machine["process_groups"][process_group]["processes"][process][ - "special_rights" - ] - != "" - ): - json_config["Process"][-1]["functionClusterAffiliation"] = ( - machine["process_groups"][process_group]["processes"][ - process - ]["special_rights"] - ) - - json_config["Process"][-1]["numberOfRestartAttempts"] = machine[ - "process_groups" - ][process_group]["processes"][process]["restart_attempts"] - - if not machine["process_groups"][process_group]["processes"][ - process - ]["native_application"]: - json_config["Process"][-1]["executable_reportingBehavior"] = ( - "ReportsExecutionState" - ) - else: - json_config["Process"][-1]["executable_reportingBehavior"] = ( - "DoesNotReportExecutionState" - ) - - json_config["Process"][-1]["sgids"] = [] - for gid in machine["process_groups"][process_group]["processes"][ - process - ]["supplementary_group_ids"]: - json_config["Process"][-1]["sgids"].append({"sgid": gid}) - - json_config["Process"][-1]["startupConfig"] = [] - for startup_config in machine["process_groups"][process_group][ - "processes" - ][process]["startup_configs"].keys(): - config = machine["process_groups"][process_group]["processes"][ - process - ]["startup_configs"][startup_config] - json_config["Process"][-1]["startupConfig"].append( - { - "executionError": f"{config['execution_error']}", - "schedulingPolicy": config["scheduling_policy"], - "schedulingPriority": f"{config['scheduling_priority']}", - "identifier": startup_config, - "enterTimeoutValue": int( - config["enter_timeout"] * 1000 - ), # convert to ms - "exitTimeoutValue": int( - config["exit_timeout"] * 1000 - ), # convert to ms - "terminationBehavior": config["termination_behavior"], - "executionDependency": [], - "processGroupStateDependency": [], - } - ) - - json_config["Process"][-1]["startupConfig"][-1][ - "executionDependency" - ] = [] - for process, state in config["depends_on"].items(): - json_config["Process"][-1]["startupConfig"][-1][ - "executionDependency" - ].append( - { - "stateName": state, - "targetProcess_identifier": f"/{process}App/{process}", - } - ) - - for state in config["use_in"]: - json_config["Process"][-1]["startupConfig"][-1][ - "processGroupStateDependency" - ].append( - { - "stateMachine_name": f"{process_group}", - "stateName": f"{process_group}/{state}", - } - ) - - json_config["Process"][-1]["startupConfig"][-1][ - "environmentVariable" - ] = [] - for key, val in config["env_variables"].items(): - json_config["Process"][-1]["startupConfig"][-1][ - "environmentVariable" - ].append({"key": key, "value": val}) - - json_config["Process"][-1]["startupConfig"][-1][ - "processArgument" - ] = [] - for arg in config["process_arguments"]: - json_config["Process"][-1]["startupConfig"][-1][ - "processArgument" - ].append({"argument": arg}) - - # configuring process groups - json_config["ModeGroup"].append( - { - "identifier": f"{process_group}", - "initialMode_name": "Off", - "recoveryMode_name": f"{process_group}/Recovery", - "modeDeclaration": [], - } - ) - for state in machine["process_group_states"][ - machine["process_groups"][process_group][ - "process_group_states_name" - ] - ]: - # replicating bug where we mix ModeDeclarationGroups (Process Group States) and ProcessGroupSet (Process Groups) - # essentially we use Process Group States declaration as Process Groups declarations - # here we should use machine["process_groups"][process_group]["process_group_states_name"] instead of process_group - # but we need to create new process group states declaration on the fly, so each process group has a unique set of states - json_config["ModeGroup"][-1]["modeDeclaration"].append( - {"identifier": f"{process_group}/{state}"} - ) - - file = open( - out_path, - "w", - ) - file.write(json.dumps(json_config, indent=2)) - file.close() - - def add_machine( - self, - name, - default_application_timeout_enter=0.5, - default_application_timeout_exit=0.5, - env_variables={"LD_LIBRARY_PATH": "/opt/lib"}, - ): - # TODO: this is only a test code, so for various reasons we only support a single machine configuration - if len(self.machines) > 0: - raise Exception( - "This version of ConfGen only support configuration of a single machine!" - ) - - for machine in self.machines: - if name == machine["machine_name"]: - raise Exception(f"Machine with {name=} cannot be redefined!") - - self.machines.append( - { - "machine_name": name, - "default_application_timeout_enter": default_application_timeout_enter, - "default_application_timeout_exit": default_application_timeout_exit, - "env_variables": env_variables, - # by default machine doesn't have any process groups - "process_groups": {}, - "process_group_states": {}, - } - ) - - # returning the freshly created machine, so it can be extended elsewhere - # machine index is also included, as it could be used later to read machine wide default values - index = len(self.machines) - 1 - return {"machine": self.machines[index], "machine_index": index} - - def machine_add_process_group(self, machine, name, states=["Off", "Verify"]): - pg_states_index = "" - - # process group states should be reused among different process groups - for key, value in machine["machine"]["process_group_states"].items(): - if value == states: - pg_states_index = key - - if "" == pg_states_index: - # TODO: at the moment this code generator only support a single machine, - # so we don't need to think about name space clashes between different machines... - # code like this should prevent this: - # pg_states_index = f"{machine['machine_name']}_{name}_States" - - # those process group states were not defined before - pg_states_index = name - machine["machine"]["process_group_states"][pg_states_index] = states - - if name not in machine["machine"]["process_groups"].keys(): - machine["machine"]["process_groups"][name] = { - "process_group_states_name": pg_states_index, - "processes": {}, - } - else: - raise Exception(f"Process Group with {name=} cannot be redefined!") - - # returning the freshly created process_group, so it can be extended elsewhere - # machine index is also included, as it could be used later to read machine wide default values - return { - "process_group": machine["machine"]["process_groups"][name], - "machine_index": machine["machine_index"], - } - - def process_group_add_process( - self, - process_group, - name, - executable_name=None, - uid=1001, - gid=1001, - supplementary_group_ids=[], - restart_attempts=0, - native_application=False, - special_rights="", - ): - if executable_name is None: - executable_name = f"/opt/apps/{name}/{name}" - - if name not in process_group["process_group"]["processes"].keys(): - process_group["process_group"]["processes"][name] = { - "executable_name": executable_name, - "uid": uid, - "gid": gid, - "supplementary_group_ids": supplementary_group_ids, - "restart_attempts": restart_attempts, - "native_application": native_application, - "special_rights": special_rights, - # by default process doesn't have any startup configs - "startup_configs": {}, - } - else: - raise Exception(f"Process with {name=} cannot be redefined!") - - # returning process config, so user can add startup configs - # machine index is also included, as it could be used later to read machine wide default values - return { - "process": process_group["process_group"]["processes"][name], - "machine_index": process_group["machine_index"], - } - - def process_add_startup_config( - self, - process, - name, - process_arguments=[], - env_variables={}, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=None, - exit_timeout=None, - execution_error=1, - depends_on={}, - use_in=[], - termination_behavior="ProcessIsNotSelfTerminating", - ): - if enter_timeout is None: - enter_timeout = self.machines[process["machine_index"]][ - "default_application_timeout_enter" - ] - - if exit_timeout is None: - exit_timeout = self.machines[process["machine_index"]][ - "default_application_timeout_exit" - ] - - # merging machine wide env variables with startup config (aka local) env variables - # step 1 --> start from empty set - merged_env_variables = {} - # step 2 --> overtake all env variables from global configuration - for key, val in self.machines[process["machine_index"]][ - "env_variables" - ].items(): - merged_env_variables[key] = val - # step 3 --> overtake all env variables from startup config - # please note that this step has to happen last, as local configuration should override global configuration - # to fulfill our requirements - for key, val in env_variables.items(): - merged_env_variables[key] = val - - if name not in process["process"]["startup_configs"].keys(): - process["process"]["startup_configs"][name] = { - "process_arguments": process_arguments, - "env_variables": merged_env_variables, - "scheduling_policy": scheduling_policy, - "scheduling_priority": scheduling_priority, - "enter_timeout": enter_timeout, - "exit_timeout": exit_timeout, - "execution_error": execution_error, - "depends_on": depends_on, - "use_in": use_in, - "termination_behavior": termination_behavior, - } - else: - raise Exception(f"Startup configuration with {name=} cannot be redefined!") - - # no need to return anything - # end of the configuration - - -def is_rust_app(process_index: int, cppprocess_count: int, rustprocess_count: int): - processes_per_process_group = cppprocess_count + rustprocess_count - process_index = process_index % processes_per_process_group - return process_index >= cppprocess_count - - -if __name__ == "__main__": - my_parser = argparse.ArgumentParser() - my_parser.add_argument( - "-c", - "--cppprocesses", - action="store", - type=int, - required=True, - help="Number of C++ demo app processes", - ) - my_parser.add_argument( - "-r", - "--rustprocesses", - action="store", - type=int, - required=True, - help="Number of Rust processes", - ) - my_parser.add_argument( - "-p", - "--process_groups", - nargs="+", - help="Name of a Process Group", - required=True, - ) - my_parser.add_argument( - "-n", - "--non-supervised-processes", - action="store", - type=int, - required=True, - help="Number of C++ non supervised demo app processes (no health manager involved)", - ) - my_parser.add_argument( - "-o", "--out", action="store", type=Path, required=True, help="Output directory" - ) - args = my_parser.parse_args() - - conf_gen = LaunchManagerConfGen() - qt_am_machine = conf_gen.add_machine( - "qt_am_machine", env_variables={"LD_LIBRARY_PATH": "/opt/lib"} - ) - - BASE_PROCESS_GROUP = "MainPG" - process_groups = args.process_groups - if BASE_PROCESS_GROUP not in process_groups: - print( - f"Process group '{BASE_PROCESS_GROUP}' must be included in the process groups list" - ) - exit(1) - - # adding function groups to TestMachine01 - pg_machine = conf_gen.machine_add_process_group( - qt_am_machine, BASE_PROCESS_GROUP, ["Off", "Startup", "Recovery"] - ) - - # adding Control application - control_process = conf_gen.process_group_add_process( - pg_machine, - "control_daemon", - executable_name="/opt/control_app/control_daemon", - uid=0, - gid=0, - special_rights="STATE_MANAGEMENT", - ) - conf_gen.process_add_startup_config( - control_process, - "control_daemon_startup_config", - # process_arguments = ["-a", "-b", "--test"], - env_variables={ - "PROCESSIDENTIFIER": "control_daemon", - }, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=1.0, - exit_timeout=1.0, - use_in=["Startup", "Recovery"], - ) - - if ( - args.cppprocesses < 0 - or args.non_supervised_processes < 0 - or args.non_supervised_processes > 10000 - or args.cppprocesses > 10000 - ): - print("Number of demo app processes must be between 0 and 1000") - exit(1) - if args.rustprocesses < 0 or args.rustprocesses > 10000: - print("Number of demo app processes must be between 0 and 1000") - exit(1) - total_process_count = args.cppprocesses + args.rustprocesses - - for process_group_index in range(0, len(process_groups)): - process_group_name = process_groups[process_group_index] - if process_group_name == BASE_PROCESS_GROUP: - pg = pg_machine - exec_dependency = {"healthmonitor": "Running"} - else: - pg = conf_gen.machine_add_process_group( - qt_am_machine, process_group_name, ["Off", "Startup", "Recovery"] - ) - exec_dependency = {} - - for i in get_process_index_range(total_process_count, process_group_index): - if not is_rust_app(i, args.cppprocesses, args.rustprocesses): - demo_executable_path = "/opt/supervision_demo/cpp_supervised_app" - print( - f"CPP Process with index {i} in process group {process_group_index}" - ) - else: - demo_executable_path = "/opt/supervision_demo/rust_supervised_app" - print( - f"Rust Process with index {i} in process group {process_group_index}" - ) - - demo_process = conf_gen.process_group_add_process( - pg, - f"demo_app{i}_{process_group_name}", - executable_name=demo_executable_path, - uid=0, - gid=0, - ) - conf_gen.process_add_startup_config( - demo_process, - f"demo_app_startup_config_{i}", - process_arguments=["-d50"], - env_variables={ - "PROCESSIDENTIFIER": f"{process_group_name}_app{i}", - "CONFIG_PATH": f"/opt/supervision_demo/etc/health_monitor_process_cfg_{i}_{process_group_name}.bin", - "IDENTIFIER": f"demo/demo_application{i}/Port1", - }, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=2.0, - exit_timeout=2.0, - depends_on=exec_dependency, - use_in=["Startup"], - ) - - for i in range(args.non_supervised_processes): - demo_process_wo_hm = conf_gen.process_group_add_process( - pg, - f"{process_group_name}_lifecycle_app{i}", - executable_name="/opt/cpp_lifecycle_app/cpp_lifecycle_app", - uid=0, - gid=0, - ) - conf_gen.process_add_startup_config( - demo_process_wo_hm, - f"lifecycle_app_startup_config_{i}_{process_group_name}_", - # uncomment one of the two following lines to inject error - # process_arguments=["-c", "2000"] if i == 1 else [], - # process_arguments=["-s"] if i == 1 else [], - env_variables={ - "PROCESSIDENTIFIER": f"{process_group_name}_lc{i}", - }, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=2.0, - exit_timeout=2.0, - use_in=["Startup"], - ) - - # One of the processes should also run in recovery state with different configuration - if i == args.non_supervised_processes - 1: - conf_gen.process_add_startup_config( - demo_process_wo_hm, - f"lifecycle_app_startup_config_{i}_{process_group_name}_recovery", - process_arguments=[f"-v"], - env_variables={ - "PROCESSIDENTIFIER": f"{process_group_name}_lc{i}", - }, - scheduling_policy="SCHED_OTHER", - scheduling_priority=0, - enter_timeout=2.0, - exit_timeout=2.0, - use_in=["Recovery"], - ) - - cfg_out_path = os.path.join(args.out, "lm_demo.json") - conf_gen.generate_json(cfg_out_path) diff --git a/examples/config/lifecycle_demo.json b/examples/config/lifecycle_demo.json new file mode 100644 index 00000000..27583cd8 --- /dev/null +++ b/examples/config/lifecycle_demo.json @@ -0,0 +1,158 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/opt", + "ready_timeout": 2.0, + "shutdown_timeout": 2.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Startup" + } + }, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1 + } + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_daemon": { + "component_properties": { + "binary_name": "control_app/control_daemon", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "control_daemon" + } + } + }, + "cpp_supervised_app": { + "component_properties": { + "binary_name": "supervision_demo/cpp_supervised_app", + "application_profile": { + "application_type": "Reporting_And_Supervised" + }, + "process_arguments": [ + "-d50" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "cpp_supervised_app", + "CONFIG_PATH": "/opt/supervision_demo/etc/hmproc_cpp_supervised_app.bin", + "IDENTIFIER": "cpp_supervised_app" + } + } + }, + "rust_supervised_app": { + "component_properties": { + "binary_name": "supervision_demo/rust_supervised_app", + "application_profile": { + "application_type": "Reporting_And_Supervised" + }, + "process_arguments": [ + "-d50" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "rust_supervised_app", + "CONFIG_PATH": "/opt/supervision_demo/etc/hmproc_rust_supervised_app.bin", + "IDENTIFIER": "rust_supervised_app" + } + } + }, + "lifecycle_app": { + "component_properties": { + "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app" + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "lifecycle_app" + } + } + }, + "fallback_app": { + "component_properties": { + "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app", + "process_arguments": [ + "-v" + ] + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "fallback_app" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_daemon" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Running": { + "depends_on": [ + "control_daemon", + "cpp_supervised_app", + "rust_supervised_app", + "lifecycle_app" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [ + "control_daemon", + "fallback_app" + ] + } +} \ No newline at end of file diff --git a/examples/control_application/control.hpp b/examples/control_application/control.hpp index 1cf72686..3e379b05 100644 --- a/examples/control_application/control.hpp +++ b/examples/control_application/control.hpp @@ -15,8 +15,8 @@ #include -struct ProcessGroupInfo { - char processGroupStatePath[1024]{}; +struct RunTargetInfo { + char runTargetName[1024]{}; }; static constexpr char const* control_socket_path = "/sm_control"; diff --git a/examples/control_application/control_app_cli.cpp b/examples/control_application/control_app_cli.cpp index d6a79557..ac93161b 100644 --- a/examples/control_application/control_app_cli.cpp +++ b/examples/control_application/control_app_cli.cpp @@ -18,19 +18,19 @@ int main(int argc, char** argv) { if(argc <= 1) { - std::cout << "Usage: " << argv[0] << " /My/ProcessGroup/State"; + std::cout << "Usage: " << argv[0] << " MyRunTargetName" << std::endl; return EXIT_FAILURE; } - ipc_dropin::Socket(sizeof(ProcessGroupInfo)), control_socket_capacity> sm_control_socket{}; + ipc_dropin::Socket(sizeof(RunTargetInfo)), control_socket_capacity> sm_control_socket{}; if (sm_control_socket.connect(control_socket_path) != ipc_dropin::ReturnCode::kOk) { std::cerr << "Could not connect to control socket" << std::endl; return EXIT_FAILURE; } - ProcessGroupInfo pg{}; - std::strncpy(pg.processGroupStatePath, argv[1], sizeof(pg.processGroupStatePath) - 1); - if(ipc_dropin::ReturnCode::kOk == sm_control_socket.trySend(pg)) { + RunTargetInfo info{}; + std::strncpy(info.runTargetName, argv[1], sizeof(info.runTargetName) - 1); + if(ipc_dropin::ReturnCode::kOk == sm_control_socket.trySend(info)) { std::cout << "Successfully sent request" << std::endl; return EXIT_SUCCESS; } else { diff --git a/examples/control_application/control_daemon.cpp b/examples/control_application/control_daemon.cpp index 5cd0533e..f0d4b87f 100644 --- a/examples/control_application/control_daemon.cpp +++ b/examples/control_application/control_daemon.cpp @@ -18,7 +18,6 @@ #include #include -#include #include "ipc_dropin/socket.hpp" #include "control.hpp" @@ -33,31 +32,26 @@ int main(int argc, char** argv) { score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); - ipc_dropin::Socket(sizeof(ProcessGroupInfo)), control_socket_capacity> sm_control_socket{}; + ipc_dropin::Socket(sizeof(RunTargetInfo)), control_socket_capacity> sm_control_socket{}; if (sm_control_socket.create(control_socket_path, 600) != ipc_dropin::ReturnCode::kOk) { std::cerr << "Could not create control socket" << std::endl; return EXIT_FAILURE; } - score::lcm::ControlClient client([](const score::lcm::ExecutionErrorEvent& event) { - std::cerr << "Undefined state callback invoked for process group id: " << event.processGroup << std::endl; - }); + score::lcm::ControlClient client; score::safecpp::Scope<> scope{}; while (!exitRequested) { - ProcessGroupInfo pgInfo{}; - if (ipc_dropin::ReturnCode::kOk == sm_control_socket.tryReceive(pgInfo)) { + RunTargetInfo info{}; + if (ipc_dropin::ReturnCode::kOk == sm_control_socket.tryReceive(info)) { - std::string targetProcessGroupState{pgInfo.processGroupStatePath}; - std::string targetProcessGroup = targetProcessGroupState.substr(0, targetProcessGroupState.find_last_of('/')); - - const score::lcm::IdentifierHash processGroup{targetProcessGroup}; - const score::lcm::IdentifierHash processGroupState{targetProcessGroupState}; - client.SetState(processGroup, processGroupState).Then({scope, [targetProcessGroupState](auto& result) noexcept { + std::string runTargetName{info.runTargetName}; + std::cout << "Activating Run Target: " << runTargetName << std::endl; + client.ActivateRunTarget(runTargetName).Then({scope, [runTargetName](auto& result) noexcept { if (!result) { - std::cerr << "Setting ProcessGroup state " << targetProcessGroupState << " failed with error: " << result.error().Message() << std::endl; + std::cerr << "Activating Run Target " << runTargetName << " failed with error: " << result.error().Message() << std::endl; } else { - std::cout << "Setting ProcessGroup state " << targetProcessGroupState << " succeeded" << std::endl; + std::cout << "Activating Run Target " << runTargetName << " succeeded" << std::endl; } }}); } diff --git a/examples/cpp_supervised_app/main.cpp b/examples/cpp_supervised_app/main.cpp index b9145ef3..0c615216 100644 --- a/examples/cpp_supervised_app/main.cpp +++ b/examples/cpp_supervised_app/main.cpp @@ -163,9 +163,9 @@ int main(int argc, char** argv) } } - if (stopReportingCheckpoints.load()) + while (stopReportingCheckpoints && !exitRequested) { - std::this_thread::sleep_for(std::chrono::milliseconds(5000)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } return EXIT_SUCCESS; diff --git a/examples/demo.sh b/examples/demo.sh index 18213fb7..1e506cab 100755 --- a/examples/demo.sh +++ b/examples/demo.sh @@ -20,9 +20,9 @@ ps -a | head -n 40 echo "$> ps -a | wc -l" ps -a | wc -l -read -p "$(echo -e ${COLOR}Next: Turning on ProcessGroup1/Startup${NC})" -echo "$> lmcontrol ProcessGroup1/Startup" -lmcontrol ProcessGroup1/Startup +read -p "$(echo -e ${COLOR}Next: Turning on RunTarget Running${NC})" +echo "$> lmcontrol Running" +lmcontrol Running read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a | wc -l" @@ -32,30 +32,34 @@ read -p "$(echo -e ${COLOR}Next: Show CPU utilization${NC})" echo "$> top" top -read -p "$(echo -e ${COLOR}Next: Turning off ProcessGroup1/Startup${NC})" -echo "$> lmcontrol ProcessGroup1/Off" -lmcontrol ProcessGroup1/Off +read -p "$(echo -e ${COLOR}Next: Turning off demo apps via RunTarget Startup${NC})" +echo "$> lmcontrol Startup" +lmcontrol Startup read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a | wc -l" ps -a | wc -l +read -p "$(echo -e ${COLOR}Next: Transition back to RunTarget Running${NC})" +echo "$> lmcontrol Running" +lmcontrol Running + read -p "$(echo -e ${COLOR}Next: Killing an application process${NC})" -echo "$> pkill -9 MainPG_lc0" -pkill -9 MainPG_lc0 +echo "$> pkill -9 cpp_supervised" +pkill -9 cpp_supervised read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a" ps -a echo "$> ps -a | wc -l" ps -a | wc -l -read -p "$(echo -e ${COLOR}Next: Moving back to MainPG/Startup${NC})" -echo "$> lmcontrol MainPG/Startup" -lmcontrol MainPG/Startup +read -p "$(echo -e ${COLOR}Next: Moving back to RunTarget Running${NC})" +echo "$> lmcontrol Running" +lmcontrol Running read -p "$(echo -e ${COLOR}Next: Trigger supervision failure${NC})" -echo "$> fail $(pgrep MainPG_app0)" -kill -s SIGUSR1 $(pgrep MainPG_app0) +echo "$> fail $(pgrep cpp_supervised)" +kill -s SIGUSR1 $(pgrep cpp_supervised) read -p "$(echo -e ${COLOR}Next: Show running processes${NC})" echo "$> ps -a" diff --git a/examples/run.sh b/examples/run.sh index dd704251..d6812609 100755 --- a/examples/run.sh +++ b/examples/run.sh @@ -23,12 +23,22 @@ file_exists() { fi } -LM_BINARY="$PWD/../bazel-bin/src/launch_manager_daemon/launch_manager" -DEMO_APP_BINARY="$PWD/../bazel-bin/examples/cpp_supervised_app/cpp_supervised_app" -DEMO_APP_WO_HM_BINARY="$PWD/../bazel-bin/examples/cpp_lifecycle_app/cpp_lifecycle_app" -RUST_APP_BINARY="$PWD/../bazel-bin/examples/rust_supervised_app/rust_supervised_app" -CONTROL_APP_BINARY="$PWD/../bazel-bin/examples/control_application/control_daemon" -CONTROL_CLI_BINARY="$PWD/../bazel-bin/examples/control_application/lmcontrol" +# When run via 'bazel run', BUILD_WORKSPACE_DIRECTORY is set to the workspace root. +# Otherwise, assume we're running from the examples/ directory. +if [ -n "$BUILD_WORKSPACE_DIRECTORY" ]; then + BAZEL_BIN="$BUILD_WORKSPACE_DIRECTORY/bazel-bin" + cd "$BUILD_WORKSPACE_DIRECTORY/examples" +else + BAZEL_BIN="$PWD/../bazel-bin" +fi + +LM_BINARY="$BAZEL_BIN/src/launch_manager_daemon/launch_manager" +DEMO_APP_BINARY="$BAZEL_BIN/examples/cpp_supervised_app/cpp_supervised_app" +DEMO_APP_WO_HM_BINARY="$BAZEL_BIN/examples/cpp_lifecycle_app/cpp_lifecycle_app" +RUST_APP_BINARY="$BAZEL_BIN/examples/rust_supervised_app/rust_supervised_app" +CONTROL_APP_BINARY="$BAZEL_BIN/examples/control_application/control_daemon" +CONTROL_CLI_BINARY="$BAZEL_BIN/examples/control_application/lmcontrol" +CFG_DIR="$BAZEL_BIN/examples/flatbuffer_out/" file_exists $LM_BINARY file_exists $DEMO_APP_BINARY @@ -37,51 +47,33 @@ file_exists $RUST_APP_BINARY file_exists $CONTROL_APP_BINARY file_exists $CONTROL_CLI_BINARY -NUMBER_OF_CPP_PROCESSES_PER_PROCESS_GROUP=1 -NUMBER_OF_RUST_PROCESSES_PER_PROCESS_GROUP=1 -NUMBER_OF_NON_SUPERVISED_CPP_PROCESSES_PER_PROCESS_GROUP=1 -PROCESS_GROUPS="--process_groups MainPG ProcessGroup1" - rm -rf tmp -rm -rf config/tmp -mkdir config/tmp -python3 config/gen_health_monitor_process_cfg.py -c "$NUMBER_OF_CPP_PROCESSES_PER_PROCESS_GROUP" -r "$NUMBER_OF_RUST_PROCESSES_PER_PROCESS_GROUP" $PROCESS_GROUPS -o config/tmp/ -../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs config/tmp/health_monitor_process_cfg_*.json - -python3 config/gen_health_monitor_cfg.py -c "$NUMBER_OF_CPP_PROCESSES_PER_PROCESS_GROUP" -r "$NUMBER_OF_RUST_PROCESSES_PER_PROCESS_GROUP" $PROCESS_GROUPS -o config/tmp/ -../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs config/tmp/hm_demo.json - -python3 config/gen_launch_manager_cfg.py -c "$NUMBER_OF_CPP_PROCESSES_PER_PROCESS_GROUP" -r "$NUMBER_OF_RUST_PROCESSES_PER_PROCESS_GROUP" -n "$NUMBER_OF_NON_SUPERVISED_CPP_PROCESSES_PER_PROCESS_GROUP" $PROCESS_GROUPS -o config/tmp/ -../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/config/lm_flatcfg.fbs config/tmp/lm_demo.json - -../bazel-bin/external/flatbuffers+/flatc --binary -o config/tmp ../src/launch_manager_daemon/health_monitor_lib/config/hmcore_flatcfg.fbs config/hmcore.json mkdir -p tmp/launch_manager/etc cp $LM_BINARY tmp/launch_manager/launch_manager -cp config/tmp/lm_demo.bin tmp/launch_manager/etc/ +cp $CFG_DIR/lm_demo.bin tmp/launch_manager/etc/ cp config/lm_logging.json tmp/launch_manager/etc/logging.json -cp config/tmp/hm_demo.bin tmp/launch_manager/etc/ -cp config/tmp/hmcore.bin tmp/launch_manager/etc/ +cp $CFG_DIR/hm_demo.bin tmp/launch_manager/etc/ +cp $CFG_DIR/hmcore.bin tmp/launch_manager/etc/ mkdir -p tmp/supervision_demo/etc cp $DEMO_APP_BINARY tmp/supervision_demo/ -cp config/tmp/health_monitor_process_cfg_*.bin tmp/supervision_demo/etc/ +cp $CFG_DIR/*_supervised_app.bin tmp/supervision_demo/etc/ mkdir -p tmp/cpp_lifecycle_app/etc cp $DEMO_APP_WO_HM_BINARY tmp/cpp_lifecycle_app/ cp $RUST_APP_BINARY tmp/supervision_demo/ -cp config/tmp/health_monitor_process_cfg_*.bin tmp/supervision_demo/etc/ mkdir -p tmp/control_app cp $CONTROL_APP_BINARY tmp/control_app/ cp $CONTROL_CLI_BINARY tmp/control_app/ mkdir -p tmp/lib -cp $PWD/../bazel-bin/src/launch_manager_daemon/process_state_client_lib/libprocess_state_client.so tmp/lib/ -cp $PWD/../bazel-bin/src/launch_manager_daemon/lifecycle_client_lib/liblifecycle_client.so tmp/lib/ -cp $PWD/../bazel-bin/src/control_client_lib/libcontrol_client_lib.so tmp/lib/ +cp $BAZEL_BIN/src/launch_manager_daemon/process_state_client_lib/libprocess_state_client.so tmp/lib/ +cp $BAZEL_BIN/src/launch_manager_daemon/lifecycle_client_lib/liblifecycle_client.so tmp/lib/ +cp $BAZEL_BIN/src/control_client_lib/libcontrol_client_lib.so tmp/lib/ docker build . -t demo diff --git a/examples/run_examples.bzl b/examples/run_examples.bzl new file mode 100644 index 00000000..05a5f8c6 --- /dev/null +++ b/examples/run_examples.bzl @@ -0,0 +1,46 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +def _impl_run_examples(ctx): + run_script = ctx.file._run_script + + launcher = ctx.actions.declare_file(ctx.label.name + "_launcher.sh") + ctx.actions.write( + output = launcher, + content = "#!/bin/bash\nexec {run_script} \"$@\"\n".format( + run_script = run_script.short_path, + ), + is_executable = True, + ) + + runfiles = ctx.runfiles(files = [run_script] + ctx.files.deps) + for dep in ctx.attr.deps: + runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles) + + return DefaultInfo( + executable = launcher, + runfiles = runfiles, + ) + +run_examples = rule( + implementation = _impl_run_examples, + executable = True, + attrs = { + "deps": attr.label_list( + default = [":example_apps"], + ), + "_run_script": attr.label( + default = Label("//examples:run.sh"), + allow_single_file = True, + ), + }, +) diff --git a/requirements.in b/requirements.in new file mode 100644 index 00000000..0e423780 --- /dev/null +++ b/requirements.in @@ -0,0 +1,5 @@ +# Python dependencies for generating lifecycle configuration files +-r ./scripts/config_mapping/requirements.txt + +-r ./tests/integration/requirements.txt + diff --git a/requirements_lock.txt b/requirements_lock.txt new file mode 100644 index 00000000..48e2b989 --- /dev/null +++ b/requirements_lock.txt @@ -0,0 +1,15 @@ +attrs==25.3.0 +exceptiongroup==1.3.1 +importlib-resources==6.4.5 +iniconfig==2.1.0 +jsonschema==4.23.0 +jsonschema-specifications==2023.12.1 +packaging==26.0 +pkgutil-resolve-name==1.3.10 +pluggy==1.5.0 +pytest==8.3.5 +referencing==0.35.1 +rpds-py==0.20.1 +tomli==2.4.0 +typing-extensions==4.13.2 +zipp==3.20.2 diff --git a/scripts/BUILD b/scripts/BUILD new file mode 100644 index 00000000..e69de29b diff --git a/scripts/config_mapping/BUILD b/scripts/config_mapping/BUILD new file mode 100644 index 00000000..074d0823 --- /dev/null +++ b/scripts/config_mapping/BUILD @@ -0,0 +1,34 @@ +load("@score_lifecycle_pip//:requirements.bzl", "requirement") +load("@score_tooling//:defs.bzl", "score_py_pytest") + +py_binary( + name = "lifecycle_config", + srcs = ["lifecycle_config.py"], + visibility = ["//visibility:public"], + deps = [requirement("jsonschema")], +) + +filegroup( + name = "integration_test_files", + srcs = glob(["tests/**/*/*.json"]), +) + +score_py_pytest( + name = "lifecycle_config_tests", + srcs = [ + "integration_tests.py", + "unit_tests.py", + ], + args = [ + ], + data = [ + ":integration_test_files", + ":lifecycle_config.py", + "//src/launch_manager_daemon/config/config_schema:s-core_launch_manager.schema.json", + ], + tags = ["manual"], + deps = [ + requirement("pytest"), + requirement("jsonschema"), + ], +) diff --git a/scripts/config_mapping/Readme.md b/scripts/config_mapping/Readme.md new file mode 100644 index 00000000..de6ad3d5 --- /dev/null +++ b/scripts/config_mapping/Readme.md @@ -0,0 +1,108 @@ +# Motivation + +We are introducing a new, simpler configuration file for the launch_manager. +To make use of the new configuration as early as possible, we are introducing a script to map the new configuration to the old configuration. +Once the source code of the launch_manager has been adapted to read in the new configuration file, the mapping script will become obsolete. + +# Usage + +Providing a json file using the new configuration format as input, the script will first validate the configuration against its schema. Then it will map the content to the old configuration file format and generate those files into the specified output_dir. + +## Bazel + +The bazel function `launch_manager_config` handles the translation of the new configuration format into the old configuration format and also does the subsequent compilation to flatbuffer files. + +```python +load("@score_lifecycle_health//:defs.bzl", "launch_manager_config") + +# This is your launch manager configuration in the new format +exports_files(["lm_config.json"]) + +# Afterwards, you can refer to the generated flatbuffer files with :example_config_gen +launch_manager_config( + name ="example_config_gen", + config="//scripts/config_mapping:lm_config.json" +) +``` + +## Python + +``` +python3 lifecycle_config.py -o --schema +``` + +If you want to **only** validate the configuration against its schema without generating any output: + +``` +python3 lifecycle_config.py --schema --validate +``` + +# Running Tests + +```bash +bazel test //scripts/config_mapping:lifecycle_config_tests +``` + +# Mapping Details + +## Mapping of RunTargets to ProcessGroups + +The LaunchManager will be configured with only a single Process Group called `MainPG`. +Each RunTarget will be mapped to a ProcessGroupState with the same name. +For example, RunTarget `Minimal` will result in a ProcessGroupState called `MainPG/Minimal`. +The ProcessGroupState will contain all the processes that would be started as part of the associated RunTarget. + +The LaunchManager will currently always startup up `MainPG/Startup` as initial state. +Therefore, we require for now that `initial_run_target` must be set to `Startup`. +This is ensured as part of the translation, configs with `initial_run_target` not equal to `Startup` are rejected. + +## Mapping of Components to Processes + +There is a 1:1 mapping from Component to Processes. + +### Mapping of Deployment Config to Startup Config + +There is a 1:1 mapping from deployment config to startup config. +Every Component can only have a single deployment config, therefore the mapped Process configuration will only have a single startup config. + +### Mapping of ReadyCondition to Execution Dependencies + +The ReadyCondition of a Component is mapped to an execution dependency between two processes. +If Component A has ReadyCondition `process_state:Running` and Component B depends on Component A. Then the ReadyCondition of Component A is mapped to `Component B depends on Component A in State Running`. + +For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that have at least one other Component depending on it. Otherwise, this ReadyCondition cannot be mapped to an execution dependency. + +## Mapping of Recovery Actions + +The only supported RecoveryAction during startup of a Component is the restart of a Component. This RecoveryAction is mapped to the `restartAttempts` parameter in the old configuration. + +For failures after Component startup the only currently supported RecoveryAction is switching to the `fallback_run_target`. +The `fallback_run_target` is mapped to a ProcessGroupState `MainPG/fallback_run_target` and this state will be configured as the recovery state (`ModeGroup/recoveryMode_name`). +This will initiate a transition to the target ProcessGroupState/RunTarget when a process crashes at runtime or a supervision fails. We assume that this transition must not fail. + +## Mapping of Alive Supervision + +For each Component with application_type `Reporting_And_Supervised` or `State_Manager`, we will create an Alive Supervision configuration. There is a 1:1 mapping from the `component_properties/application_profile/alive_supervision` parameters to the old configuration Alive Supervision structure. + +## Mapping of Watchdog Configuration + +The new configuration format allows to configure a single watchdog. There is simple 1:1 mapping to the old watchdog configuration format. + +## Known Limitations + +### Component + +* The sandbox parameters `max_memory_usage` and `max_cpu_usage` are currently not supported and will be ignored. +* For ReadyCondition `process_state:Terminated`, the mapping is only supported for Components that have at least one Component depending on it. +* The `ready_recovery_action` only supports the RecoveryAction of type `restart`. The parameter `delay_before_restart` is currently not supported and will be ignored. +* The `recovery_action` only supports `switch_run_target` with the `run_target` set to `fallback_run_target`. +* The `ready_timeout` is used as the timeout until process state Running is reached, even in case the ReadyCondition is `process_state:Terminated`. +* The parameter `deployment_config/working_dir` is currently not supported and will be ignored. + +### Run Target + +* The initial RunTarget must be named `Startup` and the `initial_run_target` must be configured to `Startup`. +* The parameter `run_targets//transition_timeout` is currently not supported and will be ignored. +* The `recovery_action` only supports `switch_run_target` with the `run_target` set to `fallback_run_target`. + + diff --git a/scripts/config_mapping/config.bzl b/scripts/config_mapping/config.bzl new file mode 100644 index 00000000..21a5a34c --- /dev/null +++ b/scripts/config_mapping/config.bzl @@ -0,0 +1,134 @@ +def _launch_manager_config_impl(ctx): + config = ctx.file.config + schema = ctx.file.schema + script = ctx.executable.script + json_out_dir = ctx.attr.json_out_dir + + # Run the mapping script to generate the json files in the old configuration format + # We need to declare an output directory, because we do not know upfront the name of the generated files nor the number of files. + gen_dir_json = ctx.actions.declare_directory(json_out_dir) + ctx.actions.run( + inputs = [config, schema], + outputs = [gen_dir_json], + tools = [script], + mnemonic = "LifecycleJsonConfigGeneration", + executable = script, + progress_message = "generating Launch Manager config from {}".format(config.short_path), + arguments = [ + config.path, + "--schema", + schema.path, + "-o", + gen_dir_json.path, + ], + ) + + flatbuffer_out_dir = ctx.attr.flatbuffer_out_dir + flatc = ctx.executable.flatc + lm_schema = ctx.file.lm_schema + hm_schema = ctx.file.hm_schema + hmcore_schema = ctx.file.hmcore_schema + + # We compile each of them via flatbuffer. + # Based on the name of each generated file, we select the corresponding schema. + gen_dir_flatbuffer = ctx.actions.declare_directory(flatbuffer_out_dir) + ctx.actions.run_shell( + inputs = [gen_dir_json, lm_schema, hm_schema, hmcore_schema], + outputs = [gen_dir_flatbuffer], + tools = [flatc], + command = """ + mkdir -p {gen_dir_flatbuffer} + # Process each file from generated directory + for file in {gen_dir_json}/*; do + if [ -f "$file" ]; then + filename=$(basename "$file") + + if [[ "$filename" == "lm_"* ]]; then + schema={lm_schema} + elif [[ "$filename" == "hmcore"* ]]; then + schema={hmcore_schema} + elif [[ "$filename" == "hm_"* ]]; then + schema={hm_schema} + elif [[ "$filename" == "hmproc_"* ]]; then + schema={hm_schema} + else + echo "Unknown file type for $filename, skipping." + continue + fi + + # Process with flatc + {flatc} -b -o {gen_dir_flatbuffer} "$schema" "$file" + fi + done + """.format( + gen_dir_flatbuffer = gen_dir_flatbuffer.path, + gen_dir_json = gen_dir_json.path, + lm_schema = lm_schema.path, + hmcore_schema = hmcore_schema.path, + hm_schema = hm_schema.path, + flatc = flatc.path, + ), + arguments = [], + mnemonic = "LaunchManagerFlatbufferConfigGeneration", + progress_message = "compiling generated Launch Manager configs in {} to flatbuffer files in {}".format(gen_dir_json.short_path, gen_dir_flatbuffer.short_path), + ) + + rf = ctx.runfiles( + files = [gen_dir_flatbuffer], + root_symlinks = { + ("_main/" + ctx.attr.flatbuffer_out_dir): gen_dir_flatbuffer, + }, + ) + + return DefaultInfo(files = depset([gen_dir_flatbuffer]), runfiles = rf) + +launch_manager_config = rule( + implementation = _launch_manager_config_impl, + attrs = { + "config": attr.label( + allow_single_file = [".json"], + mandatory = True, + doc = "Json file to convert. Note that the binary file will have the same name as the json (minus the suffix)", + ), + "schema": attr.label( + default = Label("//src/launch_manager_daemon/config/config_schema:s-core_launch_manager.schema.json"), + allow_single_file = [".json"], + doc = "Json schema file to validate the input json against", + ), + "script": attr.label( + default = Label("//scripts/config_mapping:lifecycle_config"), + executable = True, + cfg = "exec", + doc = "Python script to execute", + ), + "json_out_dir": attr.string( + default = "json_out", + doc = "Directory to copy the generated file to. Do not include a trailing '/'", + ), + "flatbuffer_out_dir": attr.string( + default = "flatbuffer_out", + doc = "Directory to copy the generated file to. Do not include a trailing '/'", + ), + "flatc": attr.label( + default = Label("@flatbuffers//:flatc"), + executable = True, + cfg = "exec", + doc = "Reference to the flatc binary", + ), + "lm_schema": attr.label( + allow_single_file = [".fbs"], + default = Label("//src/launch_manager_daemon:lm_flatcfg_fbs"), + doc = "Launch Manager fbs file to use", + ), + "hm_schema": attr.label( + allow_single_file = [".fbs"], + default = Label("//src/launch_manager_daemon/health_monitor_lib:hm_flatcfg_fbs"), + doc = "HealthMonitor fbs file to use", + ), + "hmcore_schema": attr.label( + allow_single_file = [".fbs"], + default = Label("//src/launch_manager_daemon/health_monitor_lib:hmcore_flatcfg_fbs"), + doc = "HealthMonitor core fbs file to use", + ), + }, +) diff --git a/scripts/config_mapping/integration_tests.py b/scripts/config_mapping/integration_tests.py new file mode 100644 index 00000000..56626e94 --- /dev/null +++ b/scripts/config_mapping/integration_tests.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 + +import subprocess +import shutil +from pathlib import Path +import filecmp +from scripts.config_mapping.lifecycle_config import ( + SUCCESS, + SCHEMA_VALIDATION_DEPENDENCY_ERROR, + SCHEMA_VALIDATION_FAILURE, + CUSTOM_VALIDATION_FAILURE, +) + +script_dir = Path(__file__).parent +schema_path = ( + script_dir.parent.parent + / "src" + / "launch_manager_daemon" + / "config" + / "config_schema" + / "s-core_launch_manager.schema.json" +) +tests_dir = script_dir / "tests" +lifecycle_script = script_dir / "lifecycle_config.py" + + +def run(input_file: Path, test_name: str, compare_files_only=[], exclude_files=[]): + """ + Execute the mapping script with the given input file and compare the generated output with the expected output. + Input: + - input_file: The path to the input JSON file for the mapping script + - test_name: The name of the test case, which corresponds to a subdirectory in the "tests" directory containing the expected output + """ + actual_output_dir = tests_dir / test_name / "actual_output" + expected_output_dir = tests_dir / test_name / "expected_output" + + if compare_files_only and exclude_files: + raise AssertionError( + "You may only make use of either parameters: compare_files_only or exclude_files, but not both." + ) + + # Clean and create actual output directory + if actual_output_dir.exists(): + shutil.rmtree(actual_output_dir) + actual_output_dir.mkdir(parents=True) + + # Execute lifecycle_config.py + cmd = [ + "python3", + str(lifecycle_script), + str(input_file), + "-o", + str(actual_output_dir), + "--schema", + str(schema_path), + ] + + try: + result = subprocess.run(cmd, check=True, capture_output=True, text=True) + print(f"Command executed successfully: {' '.join(cmd)}") + print(f"Output: {result.stdout}") + except subprocess.CalledProcessError as e: + print(f"Command failed: {' '.join(cmd)}") + print(f"Error: {e.stderr}") + raise + + if compare_files_only: + # Compare only specific files + if not compare_files( + actual_output_dir, expected_output_dir, compare_files_only + ): + raise AssertionError( + "Actual output files do not match expected output files." + ) + else: + # Compare the complete directory content + if not compare_directories( + actual_output_dir, expected_output_dir, exclude_files + ): + raise AssertionError("Actual output does not match expected output.") + + +def compare_directories(dir1: Path, dir2: Path, exclude_files: list) -> bool: + """ + Compare two directories recursively. Return True if they are the same, False otherwise. + """ + dcmp = filecmp.dircmp(dir1, dir2, ignore=exclude_files) + + if dcmp.left_only or dcmp.right_only or dcmp.diff_files: + print(f"Directories differ: {dir1} vs {dir2}") + print(f"Only in {dir1}: {dcmp.left_only}") + print(f"Only in {dir2}: {dcmp.right_only}") + print(f"Different files: {dcmp.diff_files}") + return False + + for common_dir in dcmp.common_dirs: + if not compare_directories(dir1 / common_dir, dir2 / common_dir): + return False + + return True + + +def compare_files(dir1: Path, dir2: Path, files: list) -> bool: + """ + Compare specific files in two directories. Return True if they are the same, False otherwise. + """ + for file in files: + file1 = dir1 / file + file2 = dir2 / file + if not filecmp.cmp(file1, file2, shallow=False): + print(f"Files differ: {file1} vs {file2}") + return False + return True + + +def test_basic(): + """ + Basic Smoketest for generating both launch manager and health monitoring configuration + """ + test_name = "basic_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name) + + +def test_health_config_mapping(): + """ + Test generation of the health monitoring configuration with + * Different application types + * Different alive supervision parameters + * Different Uid + """ + test_name = "health_config_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name, exclude_files=["lm_demo.json"]) + + +def test_empty_health_config_mapping(): + """ + Test generation of the health monitoring configuration with no supervised processes + """ + test_name = "empty_health_config_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name, exclude_files=["lm_demo.json"]) + + +def test_launch_config_mapping(): + """ + Test generation of the launch manager configuration with + * Different application types + * Different dependency configurations + * Different ready conditions + """ + test_name = "lm_config_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name, compare_files_only=["lm_demo.json"]) + + +def test_empty_launch_config_mapping(): + """ + Test generation of the launch manager configuration with no processes defined + """ + test_name = "empty_lm_config_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + run(input_file, test_name, compare_files_only=["lm_demo.json"]) + + +def test_custom_validation_failures(): + """ + Test that custom validation checks implemented in lifecycle_config.py are correctly identifying invalid configurations. + The input configuration contains the following issues: + * The run target "Minimal" has a recovery action that switches to a run target "Full" instead of "fallback_run_target" + * The mandatory "fallback_run_target" is missing from the configuration + * Reserved name "fallback_run_target" is used for a RunTarget name which is not allowed + * Initial RunTarget is not configured to "Startup" + * The "Startup" RunTarget is missing from the configuration, which is mandatory + """ + test_name = "custom_validation_failures_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + try: + run(input_file, test_name) + raise AssertionError( + "Expected an error due to custom validation failures, but the mapping script executed successfully." + ) + except subprocess.CalledProcessError as e: + assert e.returncode == CUSTOM_VALIDATION_FAILURE, ( + f"Expected exit code {CUSTOM_VALIDATION_FAILURE}, got {e.returncode}" + ) + + expected_errors = [ + 'recovery RunTarget must be set to "fallback_run_target"', + "fallback_run_target is a mandatory configuration", + 'RunTarget name "fallback_run_target" is reserved', + "initial_run_target must be configured to 'Startup'", + '"Startup" is a mandatory RunTarget', + ] + actual_error_output = e.stderr + for expected_error in expected_errors: + if expected_error not in actual_error_output: + print(f"Expected error message not found: {expected_error}") + print(f"Actual error output: {actual_error_output}") + raise AssertionError( + f"Expected error message not found: {expected_error}" + ) + + +def test_schema_validation_failures(): + """ + Test that schema validation errors are correctly raised when the input configuration does not conform to the defined JSON schema. + The input configuration contains the following issues: + * Missing required fields + """ + test_name = "schema_validation_failure_test" + input_file = tests_dir / test_name / "input" / "lm_config.json" + + try: + run(input_file, test_name) + raise AssertionError( + "Expected an error due to schema validation failures, but the mapping script executed successfully." + ) + except subprocess.CalledProcessError as e: + assert e.returncode == SCHEMA_VALIDATION_FAILURE, ( + f"Expected exit code {SCHEMA_VALIDATION_FAILURE}, got {e.returncode}" + ) diff --git a/scripts/config_mapping/lifecycle_config.py b/scripts/config_mapping/lifecycle_config.py new file mode 100644 index 00000000..8812678f --- /dev/null +++ b/scripts/config_mapping/lifecycle_config.py @@ -0,0 +1,750 @@ +#!/usr/bin/env python3 + +import argparse +from copy import deepcopy +import json +import sys +from typing import Dict, Any + +score_defaults = json.loads(""" +{ + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables": {}, + "bin_dir": "/opt", + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + }, + "sandbox": { + "uid": 0, + "gid": 0, + "supplementary_group_ids": [], + "security_policy": "", + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "REPORTING", + "is_self_terminating": false + }, + "ready_condition": { + "process_state": "Running" + } + }, + "run_target": { + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdog": {} +} +""") + + +def report_error(message): + print(f"Error: {message}", file=sys.stderr) + + +# There are various dictionaries in the config where only a single entry is allowed. +# We do not want to merge the defaults with the user specified values for these dictionaries. +not_merging_dicts = ["ready_recovery_action", "recovery_action"] + + +def load_json_file(file_path: str) -> Dict[str, Any]: + """Load and parse a JSON file.""" + with open(file_path, "r") as file: + return json.load(file) + + +def get_recovery_process_group_state(config): + # Existence has already been validated in the custom_validations function + return "MainPG/fallback_run_target" + + +def sec_to_ms(sec: float) -> int: + return int(sec * 1000) + + +def preprocess_defaults(global_defaults, config): + """ + This function takes the input configuration and fills in any missing fields with default values. + The resulting file with have no "defaults" entry anymore, but looks like if the user had specified all the fields explicitly. + """ + + def dict_merge(dict_a, dict_b): + def dict_merge_recursive(dict_a, dict_b): + for key, value in dict_b.items(): + if ( + key in dict_a + and isinstance(dict_a[key], dict) + and isinstance(value, dict) + ): + # For certain dictionaries, we do not want to merge the defaults with the user specified values + if key in not_merging_dicts: + dict_a[key] = value + else: + dict_a[key] = dict_merge(dict_a[key], value) + elif key not in dict_a: + # Value only exists in dict_b, just add it to dict_a + dict_a[key] = value + else: + # For lists, we want to overwrite the content + if isinstance(value, list): + dict_a[key] = value + # For primitive types, we want to take the one from dict_b + else: + dict_a[key] = value + return dict_a + + # We are changing the content of dict_a, so we need a deep copy + return dict_merge_recursive(deepcopy(dict_a), dict_b) + + config_defaults = config.get("defaults", {}) + # Starting with global_defaults, then applying the defaults from the config on top. + # This is to ensure that any defaults specified in the input config will override the hardcoded defaults in global_defaults. + merged_defaults = dict_merge(global_defaults, config_defaults) + + new_config = {} + new_config["components"] = {} + components = config.get("components", {}) + for component_name, component_config in components.items(): + # print("Processing component:", component_name) + new_config["components"][component_name] = {} + new_config["components"][component_name]["description"] = component_config.get( + "description", "" + ) + # Here we start with the merged defaults, then apply the component config on top, so that any fields specified in the component config will override the defaults. + new_config["components"][component_name]["component_properties"] = dict_merge( + merged_defaults["component_properties"], + component_config.get("component_properties"), + ) + new_config["components"][component_name]["deployment_config"] = dict_merge( + merged_defaults["deployment_config"], + component_config.get("deployment_config", {}), + ) + + # Special case: + # If the defaults specify alive_supervision for component, but the component config sets the type to anything other than "SUPERVISED", then we should not apply the + # alive_supervision defaults to that component, since it doesn't make sense to have alive_supervision from the defaults. + # TODO + + new_config["run_targets"] = {} + for run_target, run_target_config in config.get("run_targets", {}).items(): + new_config["run_targets"][run_target] = dict_merge( + merged_defaults["run_target"], run_target_config + ) + + new_config["alive_supervision"] = dict_merge( + merged_defaults["alive_supervision"], config.get("alive_supervision", {}) + ) + new_config["watchdog"] = dict_merge( + merged_defaults["watchdog"], config.get("watchdog", {}) + ) + + for key in ("initial_run_target", "fallback_run_target"): + if key in config: + new_config[key] = config[key] + + # print(json.dumps(new_config, indent=4)) + + return new_config + + +def gen_health_monitor_config(output_dir, config): + """ + This function generates the health monitor configuration file based on the input configuration. + Input: + output_dir: The directory where the generated files should be saved + config: The preprocessed configuration in the new format, with all defaults applied + + Output: + - A file named "hm_demo.json" containing the health monitor daemon configuration + - A optional file named "hmcore.json" containing the watchdog configuration + - For each supervised process, a file named "_.json" + """ + + def get_process_type(application_type): + if application_type == "State_Manager": + return "STM_PROCESS" + else: + return "REGULAR_PROCESS" + + def is_supervised(application_type): + return ( + application_type == "State_Manager" + or application_type == "Reporting_And_Supervised" + ) + + def get_all_process_group_states(run_targets): + process_group_states = [] + for run_target, _ in run_targets.items(): + process_group_states.append("MainPG/" + run_target) + process_group_states.append("MainPG/fallback_run_target") + return process_group_states + + def get_all_refProcessGroupStates(run_targets): + states = get_all_process_group_states(run_targets) + refProcessGroupStates = [] + for state in states: + refProcessGroupStates.append({"identifier": state}) + return refProcessGroupStates + + HM_SCHEMA_VERSION_MAJOR = 8 + HM_SCHEMA_VERSION_MINOR = 0 + hm_config = {} + hm_config["versionMajor"] = HM_SCHEMA_VERSION_MAJOR + hm_config["versionMinor"] = HM_SCHEMA_VERSION_MINOR + hm_config["process"] = [] + hm_config["hmMonitorInterface"] = [] + hm_config["hmSupervisionCheckpoint"] = [] + hm_config["hmAliveSupervision"] = [] + hm_config["hmDeadlineSupervision"] = [] + hm_config["hmLogicalSupervision"] = [] + hm_config["hmLocalSupervision"] = [] + hm_config["hmGlobalSupervision"] = [] + hm_config["hmRecoveryNotification"] = [] + index = 0 + for component_name, component_config in config["components"].items(): + if is_supervised( + component_config["component_properties"]["application_profile"][ + "application_type" + ] + ): + process = {} + process["index"] = index + process["shortName"] = component_name + process["identifier"] = component_name + process["processType"] = get_process_type( + component_config["component_properties"]["application_profile"][ + "application_type" + ] + ) + process["refProcessGroupStates"] = get_all_refProcessGroupStates( + config["run_targets"] + ) + process["processExecutionErrors"] = [ + {"processExecutionError": 1} for _ in process["refProcessGroupStates"] + ] + hm_config["process"].append(process) + + hmMonitorIf = {} + hmMonitorIf["instanceSpecifier"] = component_name + hmMonitorIf["processShortName"] = component_name + hmMonitorIf["portPrototype"] = "DefaultPort" + hmMonitorIf["interfacePath"] = "lifecycle_health_" + component_name + hmMonitorIf["refProcessIndex"] = index + hmMonitorIf["permittedUid"] = component_config["deployment_config"][ + "sandbox" + ]["uid"] + hm_config["hmMonitorInterface"].append(hmMonitorIf) + + checkpoint = {} + checkpoint["shortName"] = component_name + "_checkpoint" + checkpoint["checkpointId"] = 1 + checkpoint["refInterfaceIndex"] = index + hm_config["hmSupervisionCheckpoint"].append(checkpoint) + + alive_supervision = {} + alive_supervision["ruleContextKey"] = component_name + "_alive_supervision" + alive_supervision["refCheckPointIndex"] = index + alive_supervision["aliveReferenceCycle"] = sec_to_ms( + component_config["component_properties"]["application_profile"][ + "alive_supervision" + ]["reporting_cycle"] + ) + alive_supervision["minAliveIndications"] = component_config[ + "component_properties" + ]["application_profile"]["alive_supervision"]["min_indications"] + alive_supervision["maxAliveIndications"] = component_config[ + "component_properties" + ]["application_profile"]["alive_supervision"]["max_indications"] + alive_supervision["isMinCheckDisabled"] = ( + alive_supervision["minAliveIndications"] == 0 + ) + alive_supervision["isMaxCheckDisabled"] = ( + alive_supervision["maxAliveIndications"] == 0 + ) + alive_supervision["failedSupervisionCyclesTolerance"] = component_config[ + "component_properties" + ]["application_profile"]["alive_supervision"]["failed_cycles_tolerance"] + alive_supervision["refProcessIndex"] = index + alive_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates( + config["run_targets"] + ) + hm_config["hmAliveSupervision"].append(alive_supervision) + + local_supervision = {} + local_supervision["ruleContextKey"] = component_name + "_local_supervision" + local_supervision["infoRefInterfacePath"] = "" + local_supervision["hmRefAliveSupervision"] = [ + {"refAliveSupervisionIdx": index} + ] + hm_config["hmLocalSupervision"].append(local_supervision) + + with open( + f"{output_dir}/hmproc_{component_name}.json", "w" + ) as process_file: + process_config = {} + process_config["versionMajor"] = HM_SCHEMA_VERSION_MAJOR + process_config["versionMinor"] = HM_SCHEMA_VERSION_MINOR + process_config["process"] = [] + process_config["hmMonitorInterface"] = [] + process_config["hmMonitorInterface"].append(hmMonitorIf) + json.dump(process_config, process_file, indent=4) + + index += 1 + + indices = [i for i in range(index)] + if len(indices) > 0: + # Create one global supervision & recovery action for all processes. + global_supervision = {} + global_supervision["ruleContextKey"] = "global_supervision" + global_supervision["isSeverityCritical"] = False + global_supervision["localSupervision"] = [ + {"refLocalSupervisionIndex": idx} for idx in indices + ] + global_supervision["refProcesses"] = [{"index": idx} for idx in indices] + global_supervision["refProcessGroupStates"] = get_all_refProcessGroupStates( + config["run_targets"] + ) + hm_config["hmGlobalSupervision"].append(global_supervision) + + recovery_action = {} + recovery_action["shortName"] = f"recovery_notification" + recovery_action["recoveryNotificationTimeout"] = 5000 + recovery_action["processGroupMetaModelIdentifier"] = ( + get_recovery_process_group_state(config) + ) + recovery_action["refGlobalSupervisionIndex"] = hm_config[ + "hmGlobalSupervision" + ].index(global_supervision) + recovery_action["instanceSpecifier"] = "" + recovery_action["shouldFireWatchdog"] = False + hm_config["hmRecoveryNotification"].append(recovery_action) + + with open(f"{output_dir}/hm_demo.json", "w") as hm_file: + json.dump(hm_config, hm_file, indent=4) + + HM_CORE_SCHEMA_VERSION_MAJOR = 3 + HM_CORE_SCHEMA_VERSION_MINOR = 0 + hmcore_config = {} + hmcore_config["versionMajor"] = HM_CORE_SCHEMA_VERSION_MAJOR + hmcore_config["versionMinor"] = HM_CORE_SCHEMA_VERSION_MINOR + hmcore_config["watchdogs"] = [] + hmcore_config["config"] = [ + { + "periodicity": sec_to_ms( + config.get("alive_supervision", {}).get("evaluation_cycle", 0.01) + ) + } + ] + + if watchdog_config := config.get("watchdog", {}): + watchdog = {} + watchdog["shortName"] = "watchdog" + watchdog["deviceFilePath"] = watchdog_config["device_file_path"] + watchdog["maxTimeout"] = sec_to_ms(watchdog_config["max_timeout"]) + watchdog["deactivateOnShutdown"] = watchdog_config["deactivate_on_shutdown"] + watchdog["hasValueDeactivateOnShutdown"] = True + watchdog["requireMagicClose"] = watchdog_config["require_magic_close"] + watchdog["hasValueRequireMagicClose"] = True + hmcore_config["watchdogs"].append(watchdog) + + with open(f"{output_dir}/hmcore.json", "w") as hm_file: + json.dump(hmcore_config, hm_file, indent=4) + + +def gen_launch_manager_config(output_dir, config): + """ + This function generates the launch manager configuration file based on the input configuration. + Input: + output_dir: The directory where the generated files should be saved + config: The preprocessed configuration in the new format, with all defaults applied + + Output: + - A file named "lm_demo.json" containing the launch manager configuration + """ + + """ + Recursively get all components on which the run target depends + """ + + def format_dependency_path(path, cycle_target): + """Format a dependency resolution path for display, highlighting the cycle.""" + return " -> ".join(path + [cycle_target]) + + def get_process_dependencies( + run_target, ancestors_run_targets=None, ancestors_components=None + ): + """ + Resolve all component dependencies for the given run target. + + ancestors_run_targets and ancestors_components track the current + recursion path to detect cyclic dependencies without rejecting + legitimate diamond-shaped dependency trees. + """ + if ancestors_run_targets is None: + ancestors_run_targets = [] + if ancestors_components is None: + ancestors_components = [] + + out = [] + if "depends_on" not in run_target: + return out + + for dependency_name in run_target["depends_on"]: + if dependency_name in config["components"]: + if dependency_name in ancestors_components: + path = format_dependency_path(ancestors_components, dependency_name) + raise ValueError( + f"Cyclic dependency detected: component '{dependency_name}' " + f"has already been visited.\n Path: {path}" + ) + ancestors_components.append(dependency_name) + out.append(dependency_name) + + component_props = config["components"][dependency_name][ + "component_properties" + ] + if "depends_on" in component_props: + # All dependencies must be components, since components can't depend on run targets + for dep in component_props["depends_on"]: + if dep not in config["components"]: + raise ValueError( + f"Component '{dependency_name}' depends on unknown component '{dep}'." + ) + if dep in ancestors_components: + path = format_dependency_path(ancestors_components, dep) + raise ValueError( + f"Cyclic dependency detected: component '{dependency_name}' " + f"depends on already visited component '{dep}'.\n Path: {path}" + ) + ancestors_components.append(dep) + out.append(dep) + out += get_process_dependencies( + config["components"][dep]["component_properties"], + ancestors_run_targets=ancestors_run_targets, + ancestors_components=ancestors_components, + ) + ancestors_components.pop() + + ancestors_components.pop() + else: + # If the dependency is not a component, it must be a run target + if dependency_name not in config["run_targets"]: + raise ValueError( + f"Run target depends on unknown run target or component '{dependency_name}'." + ) + if dependency_name in ancestors_run_targets: + path = format_dependency_path( + ancestors_run_targets, dependency_name + ) + raise ValueError( + f"Cyclic dependency detected: run target '{dependency_name}' " + f"has already been visited.\n Path: {path}" + ) + ancestors_run_targets.append(dependency_name) + out += get_process_dependencies( + config["run_targets"][dependency_name], + ancestors_run_targets=ancestors_run_targets, + ancestors_components=ancestors_components, + ) + ancestors_run_targets.pop() + return list(set(out)) # Remove duplicates + + def get_terminating_behavior(component_config): + if component_config["component_properties"]["application_profile"][ + "is_self_terminating" + ]: + return "ProcessIsSelfTerminating" + else: + return "ProcessIsNotSelfTerminating" + + lm_config = {} + lm_config["versionMajor"] = 7 + lm_config["versionMinor"] = 0 + lm_config["Process"] = [] + lm_config["ModeGroup"] = [ + { + "identifier": "MainPG", + "initialMode_name": "not-used", + "recoveryMode_name": get_recovery_process_group_state(config), + "modeDeclaration": [], + } + ] + + process_group_states = {} + + # For each component, store which run targets depends on it + for pgstate, values in config["run_targets"].items(): + state_name = "MainPG/" + pgstate + lm_config["ModeGroup"][0]["modeDeclaration"].append({"identifier": state_name}) + components = get_process_dependencies(values) + for component in components: + if component not in process_group_states: + process_group_states[component] = [] + process_group_states[component].append(state_name) + + if fallback := config.get("fallback_run_target", {}): + lm_config["ModeGroup"][0]["modeDeclaration"].append( + {"identifier": "MainPG/fallback_run_target"} + ) + fallback_components = get_process_dependencies(fallback) + for component in fallback_components: + if component not in process_group_states: + process_group_states[component] = [] + process_group_states[component].append("MainPG/fallback_run_target") + + for component_name, component_config in config["components"].items(): + process = {} + process["identifier"] = component_name + process["path"] = ( + f"{component_config['deployment_config']['bin_dir']}/{component_config['component_properties']['binary_name']}" + ) + process["uid"] = component_config["deployment_config"]["sandbox"]["uid"] + process["gid"] = component_config["deployment_config"]["sandbox"]["gid"] + process["sgids"] = [ + {"sgid": sgid} + for sgid in component_config["deployment_config"]["sandbox"][ + "supplementary_group_ids" + ] + ] + process["securityPolicyDetails"] = component_config["deployment_config"][ + "sandbox" + ]["security_policy"] + process["numberOfRestartAttempts"] = component_config["deployment_config"][ + "ready_recovery_action" + ]["restart"]["number_of_attempts"] + + match component_config["component_properties"]["application_profile"][ + "application_type" + ]: + case "Native": + process["executable_reportingBehavior"] = "DoesNotReportExecutionState" + case "State_Manager": + process["executable_reportingBehavior"] = "ReportsExecutionState" + process["functionClusterAffiliation"] = "STATE_MANAGEMENT" + case "Reporting" | "Reporting_And_Supervised": + process["executable_reportingBehavior"] = "ReportsExecutionState" + + process["startupConfig"] = [{}] + process["startupConfig"][0]["executionError"] = "1" + process["startupConfig"][0]["identifier"] = f"{component_name}_startup_config" + process["startupConfig"][0]["enterTimeoutValue"] = sec_to_ms( + component_config["deployment_config"]["ready_timeout"] + ) + process["startupConfig"][0]["exitTimeoutValue"] = sec_to_ms( + component_config["deployment_config"]["shutdown_timeout"] + ) + process["startupConfig"][0]["schedulingPolicy"] = component_config[ + "deployment_config" + ]["sandbox"]["scheduling_policy"] + process["startupConfig"][0]["schedulingPriority"] = str( + component_config["deployment_config"]["sandbox"]["scheduling_priority"] + ) + process["startupConfig"][0]["terminationBehavior"] = get_terminating_behavior( + component_config + ) + process["startupConfig"][0]["processGroupStateDependency"] = [] + process["startupConfig"][0]["environmentVariable"] = [] + for env_var, value in ( + component_config["deployment_config"] + .get("environmental_variables", {}) + .items() + ): + process["startupConfig"][0]["environmentVariable"].append( + {"key": env_var, "value": value} + ) + + if arguments := component_config["component_properties"].get( + "process_arguments", [] + ): + arguments = [{"argument": arg} for arg in arguments] + process["startupConfig"][0]["processArgument"] = arguments + + if component_name in process_group_states: + for pgstate in process_group_states[component_name]: + process["startupConfig"][0]["processGroupStateDependency"].append( + {"stateMachine_name": "MainPG", "stateName": pgstate} + ) + + lm_config["Process"].append(process) + + # Execution dependencies. Assumption: Components can never depend on run targets + for process in lm_config["Process"]: + process["startupConfig"][0]["executionDependency"] = [] + for dependency in config["components"][process["identifier"]][ + "component_properties" + ].get("depends_on", []): + dep_entry = config["components"][dependency] + ready_condition = dep_entry["component_properties"]["ready_condition"][ + "process_state" + ] + process["startupConfig"][0]["executionDependency"].append( + {"stateName": ready_condition, "targetProcess_identifier": dependency} + ) + + with open(f"{output_dir}/lm_demo.json", "w") as lm_file: + json.dump(lm_config, lm_file, indent=4) + + +def custom_validations(config): + success = True + + if config.get("initial_run_target") != "Startup": + report_error( + "initial_run_target must be configured to 'Startup'. Other values are not yet supported yet." + ) + success = False + + if "Startup" not in config["run_targets"]: + report_error( + '"Startup" is a mandatory RunTarget and must be defined in the configuration.' + ) + success = False + + if "fallback_run_target" in config["run_targets"]: + report_error( + 'RunTarget name "fallback_run_target" is reserved, please choose a different name.' + ) + success = False + + # Check that for any switch_run_target recovery action, the run_target is set to "fallback_run_target" + for _, run_target in config["run_targets"].items(): + recovery_target_name = ( + run_target.get("recovery_action", {}) + .get("switch_run_target", {}) + .get("run_target", "fallback_run_target") + ) + if recovery_target_name != "fallback_run_target": + report_error( + 'For any switch_run_target recovery action, the recovery RunTarget must be set to "fallback_run_target".' + ) + success = False + + if "fallback_run_target" not in config: + report_error( + "fallback_run_target is a mandatory configuration but was not found in the config." + ) + success = False + + return success + + +def check_validation_dependency(): + try: + import jsonschema + except ImportError: + print( + 'jsonschema library is not installed. Please install it with "pip install jsonschema" to enable schema validation.' + ) + return False + return True + + +def schema_validation(json_input, schema, config_path=None, schema_path=None): + try: + from jsonschema import validate, ValidationError + + validate(json_input, schema) + print("Schema Validation successful") + return True + except ValidationError as err: + path = " -> ".join(str(p) for p in err.absolute_path) + location = f" (at: {path})" if path else "" + config_info = f"Validated Config: {config_path}" if config_path else "" + schema_info = f"Schema File: {schema_path}" if schema_path else "" + print( + f"Error: Schema validation failed{location}: {err.message}\n{config_info}\n{schema_info}", + file=sys.stderr, + ) + return False + + +# Possible exit codes returned from this script +SUCCESS = 0 +SCHEMA_VALIDATION_DEPENDENCY_ERROR = 1 +SCHEMA_VALIDATION_FAILURE = 2 +CUSTOM_VALIDATION_FAILURE = 3 + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("filename", help="The launch_manager configuration file") + parser.add_argument( + "--output-dir", "-o", default=".", help="Output directory for generated files" + ) + parser.add_argument( + "--validate", + default=False, + action="store_true", + help="Only validate the provided configuration file against the schema and exit.", + ) + parser.add_argument("--schema", help="Path to the JSON schema file for validation") + args = parser.parse_args() + + input_config = load_json_file(args.filename) + + if args.schema: + # User asked for validation explicitly, but the dependency is not installed, we should exit with an error + if args.validate and not check_validation_dependency(): + exit(SCHEMA_VALIDATION_DEPENDENCY_ERROR) + + # User asked not explicitly for validation, but the dependency is not installed, we should print a warning and continue without validation + if not check_validation_dependency(): + print( + 'lifecycle_config.py:jsonschema library is not installed. Please install it with "pip install jsonschema" to enable schema validation.' + ) + print("Schema validation will be skipped.") + else: + json_schema = load_json_file(args.schema) + validation_successful = schema_validation( + input_config, + json_schema, + config_path=args.filename, + schema_path=args.schema, + ) + if not validation_successful: + exit(SCHEMA_VALIDATION_FAILURE) + + if args.validate: + exit(SUCCESS) + else: + print( + 'No schema provided, skipping validation. Provide the path to the json schema with "--schema " to enable validation.' + ) + + preprocessed_config = preprocess_defaults(score_defaults, input_config) + if not custom_validations(preprocessed_config): + exit(CUSTOM_VALIDATION_FAILURE) + + try: + gen_health_monitor_config(args.output_dir, preprocessed_config) + gen_launch_manager_config(args.output_dir, preprocessed_config) + except ValueError as e: + print(f"Error during configuration generation: {e}", file=sys.stderr) + exit(CUSTOM_VALIDATION_FAILURE) + + return SUCCESS + + +if __name__ == "__main__": + exit(main()) diff --git a/scripts/config_mapping/requirements.txt b/scripts/config_mapping/requirements.txt new file mode 100644 index 00000000..8d45edf7 --- /dev/null +++ b/scripts/config_mapping/requirements.txt @@ -0,0 +1,2 @@ +pytest +jsonschema diff --git a/scripts/config_mapping/tests/.gitignore b/scripts/config_mapping/tests/.gitignore new file mode 100644 index 00000000..d1a80c32 --- /dev/null +++ b/scripts/config_mapping/tests/.gitignore @@ -0,0 +1 @@ +*/actual_output \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json new file mode 100644 index 00000000..4186ddd9 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/hm_demo.json @@ -0,0 +1,281 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [ + { + "index": 0, + "shortName": "someip-daemon", + "identifier": "someip-daemon", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + }, + { + "index": 1, + "shortName": "test_app1", + "identifier": "test_app1", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + }, + { + "index": 2, + "shortName": "state_manager", + "identifier": "state_manager", + "processType": "STM_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + } + ], + "hmMonitorInterface": [ + { + "instanceSpecifier": "someip-daemon", + "processShortName": "someip-daemon", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_someip-daemon", + "refProcessIndex": 0, + "permittedUid": 1000 + }, + { + "instanceSpecifier": "test_app1", + "processShortName": "test_app1", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app1", + "refProcessIndex": 1, + "permittedUid": 1000 + }, + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 2, + "permittedUid": 1000 + } + ], + "hmSupervisionCheckpoint": [ + { + "shortName": "someip-daemon_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 0 + }, + { + "shortName": "test_app1_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 1 + }, + { + "shortName": "state_manager_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 2 + } + ], + "hmAliveSupervision": [ + { + "ruleContextKey": "someip-daemon_alive_supervision", + "refCheckPointIndex": 0, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 0, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + }, + { + "ruleContextKey": "test_app1_alive_supervision", + "refCheckPointIndex": 1, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 1, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + }, + { + "ruleContextKey": "state_manager_alive_supervision", + "refCheckPointIndex": 2, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 2, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ], + "hmDeadlineSupervision": [], + "hmLogicalSupervision": [], + "hmLocalSupervision": [ + { + "ruleContextKey": "someip-daemon_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 0 + } + ] + }, + { + "ruleContextKey": "test_app1_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 1 + } + ] + }, + { + "ruleContextKey": "state_manager_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 2 + } + ] + } + ], + "hmGlobalSupervision": [ + { + "ruleContextKey": "global_supervision", + "isSeverityCritical": false, + "localSupervision": [ + { + "refLocalSupervisionIndex": 0 + }, + { + "refLocalSupervisionIndex": 1 + }, + { + "refLocalSupervisionIndex": 2 + } + ], + "refProcesses": [ + { + "index": 0 + }, + { + "index": 1 + }, + { + "index": 2 + } + ], + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ], + "hmRecoveryNotification": [ + { + "shortName": "recovery_notification", + "recoveryNotificationTimeout": 5000, + "processGroupMetaModelIdentifier": "MainPG/fallback_run_target", + "refGlobalSupervisionIndex": 0, + "instanceSpecifier": "", + "shouldFireWatchdog": false + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json b/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json new file mode 100644 index 00000000..f06adf40 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/hmcore.json @@ -0,0 +1,20 @@ +{ + "versionMajor": 3, + "versionMinor": 0, + "watchdogs": [ + { + "shortName": "watchdog", + "deviceFilePath": "/dev/watchdog", + "maxTimeout": 2000, + "deactivateOnShutdown": true, + "hasValueDeactivateOnShutdown": true, + "requireMagicClose": false, + "hasValueRequireMagicClose": true + } + ], + "config": [ + { + "periodicity": 500 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hmproc_someip-daemon.json b/scripts/config_mapping/tests/basic_test/expected_output/hmproc_someip-daemon.json new file mode 100644 index 00000000..724551f0 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/hmproc_someip-daemon.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "someip-daemon", + "processShortName": "someip-daemon", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_someip-daemon", + "refProcessIndex": 0, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hmproc_state_manager.json b/scripts/config_mapping/tests/basic_test/expected_output/hmproc_state_manager.json new file mode 100644 index 00000000..dd4dfdfe --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/hmproc_state_manager.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 2, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/hmproc_test_app1.json b/scripts/config_mapping/tests/basic_test/expected_output/hmproc_test_app1.json new file mode 100644 index 00000000..2de62915 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/hmproc_test_app1.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "test_app1", + "processShortName": "test_app1", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app1", + "refProcessIndex": 1, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json new file mode 100644 index 00000000..36303aac --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/expected_output/lm_demo.json @@ -0,0 +1,364 @@ +{ + "versionMajor": 7, + "versionMinor": 0, + "Process": [ + { + "identifier": "setup_filesystem_sh", + "path": "/opt/scripts/bin/setup_filesystem.sh", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "DoesNotReportExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "setup_filesystem_sh_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Startup" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + } + ], + "executionDependency": [] + } + ] + }, + { + "identifier": "dlt-daemon", + "path": "/opt/apps/dlt-daemon/dltd", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "DoesNotReportExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "dlt-daemon_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Terminated", + "targetProcess_identifier": "setup_filesystem_sh" + } + ] + } + ] + }, + { + "identifier": "someip-daemon", + "path": "/opt/apps/someip/someipd", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "someip-daemon_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [] + } + ] + }, + { + "identifier": "test_app1", + "path": "/opt/apps/test_app1/test_app1", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "test_app1_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Running", + "targetProcess_identifier": "dlt-daemon" + }, + { + "stateName": "Running", + "targetProcess_identifier": "someip-daemon" + } + ] + } + ] + }, + { + "identifier": "state_manager", + "path": "/opt/apps/state_manager/sm", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "functionClusterAffiliation": "STATE_MANAGEMENT", + "startupConfig": [ + { + "executionError": "1", + "identifier": "state_manager_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Startup" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Terminated", + "targetProcess_identifier": "setup_filesystem_sh" + } + ] + } + ] + } + ], + "ModeGroup": [ + { + "identifier": "MainPG", + "initialMode_name": "not-used", + "recoveryMode_name": "MainPG/fallback_run_target", + "modeDeclaration": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/basic_test/input/lm_config.json b/scripts/config_mapping/tests/basic_test/input/lm_config.json new file mode 100644 index 00000000..b35597f9 --- /dev/null +++ b/scripts/config_mapping/tests/basic_test/input/lm_config.json @@ -0,0 +1,168 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib", + "GLOBAL_ENV_VAR": "abc", + "EMPTY_GLOBAL_ENV_VAR": "" + }, + "bin_dir": "/opt", + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + }, + "sandbox": { + "uid": 1000, + "gid": 1000, + "supplementary_group_ids": [500, 600, 700], + "security_policy": "policy_name", + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0, + "max_memory_usage": 1024, + "max_cpu_usage": 75 + } + }, + "component_properties": { + "binary_name": "test_app1", + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + }, + "depends_on": ["test_app2", "test_app3"], + "process_arguments": ["-a", "-b", "--xyz"], + "ready_condition": { + "process_state": "Running" + } + }, + "run_target": { + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + } + }, + "components": { + "setup_filesystem_sh": { + "description": "Script to mount partitions at the right directories", + "component_properties": { + "binary_name": "bin/setup_filesystem.sh", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "depends_on": [], + "process_arguments": ["-a", "-b"], + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts" + } + }, + "dlt-daemon": { + "description": "Logging application", + "component_properties": { + "binary_name": "dltd", + "application_profile": { + "application_type": "Native" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/dlt-daemon" + } + }, + "someip-daemon": { + "description": "SOME/IP application", + "component_properties": { + "binary_name": "someipd", + "depends_on": [] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/someip" + } + }, + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1", + "depends_on": ["dlt-daemon", "someip-daemon"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + }, + "state_manager": { + "description": "Application that manages life cycle of the ECU", + "component_properties": { + "binary_name": "sm", + "application_profile": { + "application_type": "State_Manager" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/state_manager" + } + } + }, + "run_targets": { + "Startup": { + "description": "Minimal functionality of the system", + "depends_on": ["state_manager"], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["test_app1", "Startup"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/custom_validation_failures_test/input/lm_config.json b/scripts/config_mapping/tests/custom_validation_failures_test/input/lm_config.json new file mode 100644 index 00000000..66a51152 --- /dev/null +++ b/scripts/config_mapping/tests/custom_validation_failures_test/input/lm_config.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "defaults": {}, + "components": { + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1" + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + } + }, + "run_targets": { + "Minimal": { + "description": "Minimal functionality of the system", + "depends_on": ["test_app1"], + "recovery_action": { + "switch_run_target": { + "run_target": "Fallback" + } + } + }, + "Fallback": { + "description": "Nothing running", + "depends_on": [], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "fallback_run_target": { + "description": "Fallback run target", + "depends_on": [], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Minimal" +} \ No newline at end of file diff --git a/tests/integration/smoke/hm_demo.json b/scripts/config_mapping/tests/empty_health_config_test/expected_output/hm_demo.json similarity index 100% rename from tests/integration/smoke/hm_demo.json rename to scripts/config_mapping/tests/empty_health_config_test/expected_output/hm_demo.json diff --git a/examples/config/hmcore.json b/scripts/config_mapping/tests/empty_health_config_test/expected_output/hmcore.json similarity index 58% rename from examples/config/hmcore.json rename to scripts/config_mapping/tests/empty_health_config_test/expected_output/hmcore.json index d769a798..b2c87312 100644 --- a/examples/config/hmcore.json +++ b/scripts/config_mapping/tests/empty_health_config_test/expected_output/hmcore.json @@ -4,8 +4,7 @@ "watchdogs": [], "config": [ { - "periodicity": 50, - "bufferSizeGlobalSupervision": "512" + "periodicity": 123 } ] } \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json b/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json new file mode 100644 index 00000000..217c9d66 --- /dev/null +++ b/scripts/config_mapping/tests/empty_health_config_test/input/lm_config.json @@ -0,0 +1,74 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + } + }, + "components": { + "non_supervised_comp": { + "description": "Non-supervised component", + "component_properties": { + "binary_name": "bin/comp", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts", + "sandbox": { + "uid": 3 + } + } + } + }, + "run_targets": { + "Startup": { + "description": "Minimal functionality of the system", + "depends_on": ["non_supervised_comp"], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["non_supervised_comp"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.123 + }, + "watchdog": {} +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hm_demo.json new file mode 100644 index 00000000..8e559853 --- /dev/null +++ b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hm_demo.json @@ -0,0 +1,13 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [], + "hmSupervisionCheckpoint": [], + "hmAliveSupervision": [], + "hmDeadlineSupervision": [], + "hmLogicalSupervision": [], + "hmLocalSupervision": [], + "hmGlobalSupervision": [], + "hmRecoveryNotification": [] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json new file mode 100644 index 00000000..f06adf40 --- /dev/null +++ b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/hmcore.json @@ -0,0 +1,20 @@ +{ + "versionMajor": 3, + "versionMinor": 0, + "watchdogs": [ + { + "shortName": "watchdog", + "deviceFilePath": "/dev/watchdog", + "maxTimeout": 2000, + "deactivateOnShutdown": true, + "hasValueDeactivateOnShutdown": true, + "requireMagicClose": false, + "hasValueRequireMagicClose": true + } + ], + "config": [ + { + "periodicity": 500 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json new file mode 100644 index 00000000..e51df1ee --- /dev/null +++ b/scripts/config_mapping/tests/empty_lm_config_test/expected_output/lm_demo.json @@ -0,0 +1,20 @@ +{ + "versionMajor": 7, + "versionMinor": 0, + "Process": [], + "ModeGroup": [ + { + "identifier": "MainPG", + "initialMode_name": "not-used", + "recoveryMode_name": "MainPG/fallback_run_target", + "modeDeclaration": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json new file mode 100644 index 00000000..b880105e --- /dev/null +++ b/scripts/config_mapping/tests/empty_lm_config_test/input/lm_config.json @@ -0,0 +1,92 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib", + "GLOBAL_ENV_VAR": "abc", + "EMPTY_GLOBAL_ENV_VAR": "" + }, + "bin_dir": "/opt", + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + }, + "sandbox": { + "uid": 1000, + "gid": 1000, + "supplementary_group_ids": [500, 600, 700], + "security_policy": "policy_name", + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0, + "max_memory_usage": 1024, + "max_cpu_usage": 75 + } + }, + "component_properties": { + "binary_name": "test_app1", + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + }, + "depends_on": ["test_app2", "test_app3"], + "process_arguments": ["-a", "-b", "--xyz"], + "ready_condition": { + "process_state": "Running" + } + }, + "run_target": { + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + } + }, + "run_targets": { + "Startup": { + "description": "Empty", + "depends_on": [], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json new file mode 100644 index 00000000..589bf217 --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hm_demo.json @@ -0,0 +1,281 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [ + { + "index": 0, + "shortName": "state_manager", + "identifier": "state_manager", + "processType": "STM_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + }, + { + "index": 1, + "shortName": "reporting_supervised_component", + "identifier": "reporting_supervised_component", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + }, + { + "index": 2, + "shortName": "reporting_supervised_component_with_no_max_indications", + "identifier": "reporting_supervised_component_with_no_max_indications", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + } + ], + "hmMonitorInterface": [ + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 0, + "permittedUid": 4 + }, + { + "instanceSpecifier": "reporting_supervised_component", + "processShortName": "reporting_supervised_component", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_reporting_supervised_component", + "refProcessIndex": 1, + "permittedUid": 5 + }, + { + "instanceSpecifier": "reporting_supervised_component_with_no_max_indications", + "processShortName": "reporting_supervised_component_with_no_max_indications", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_reporting_supervised_component_with_no_max_indications", + "refProcessIndex": 2, + "permittedUid": 5 + } + ], + "hmSupervisionCheckpoint": [ + { + "shortName": "state_manager_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 0 + }, + { + "shortName": "reporting_supervised_component_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 1 + }, + { + "shortName": "reporting_supervised_component_with_no_max_indications_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 2 + } + ], + "hmAliveSupervision": [ + { + "ruleContextKey": "state_manager_alive_supervision", + "refCheckPointIndex": 0, + "aliveReferenceCycle": 100, + "minAliveIndications": 0, + "maxAliveIndications": 2, + "isMinCheckDisabled": true, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 0, + "refProcessIndex": 0, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + }, + { + "ruleContextKey": "reporting_supervised_component_alive_supervision", + "refCheckPointIndex": 1, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 1, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + }, + { + "ruleContextKey": "reporting_supervised_component_with_no_max_indications_alive_supervision", + "refCheckPointIndex": 2, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 0, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": true, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 2, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ], + "hmDeadlineSupervision": [], + "hmLogicalSupervision": [], + "hmLocalSupervision": [ + { + "ruleContextKey": "state_manager_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 0 + } + ] + }, + { + "ruleContextKey": "reporting_supervised_component_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 1 + } + ] + }, + { + "ruleContextKey": "reporting_supervised_component_with_no_max_indications_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 2 + } + ] + } + ], + "hmGlobalSupervision": [ + { + "ruleContextKey": "global_supervision", + "isSeverityCritical": false, + "localSupervision": [ + { + "refLocalSupervisionIndex": 0 + }, + { + "refLocalSupervisionIndex": 1 + }, + { + "refLocalSupervisionIndex": 2 + } + ], + "refProcesses": [ + { + "index": 0 + }, + { + "index": 1 + }, + { + "index": 2 + } + ], + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ], + "hmRecoveryNotification": [ + { + "shortName": "recovery_notification", + "recoveryNotificationTimeout": 5000, + "processGroupMetaModelIdentifier": "MainPG/fallback_run_target", + "refGlobalSupervisionIndex": 0, + "instanceSpecifier": "", + "shouldFireWatchdog": false + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json new file mode 100644 index 00000000..cad238fb --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hmcore.json @@ -0,0 +1,20 @@ +{ + "versionMajor": 3, + "versionMinor": 0, + "watchdogs": [ + { + "shortName": "watchdog", + "deviceFilePath": "/dev/watchdog", + "maxTimeout": 2000, + "deactivateOnShutdown": true, + "hasValueDeactivateOnShutdown": true, + "requireMagicClose": false, + "hasValueRequireMagicClose": true + } + ], + "config": [ + { + "periodicity": 123 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component.json new file mode 100644 index 00000000..f77030dd --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "reporting_supervised_component", + "processShortName": "reporting_supervised_component", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_reporting_supervised_component", + "refProcessIndex": 1, + "permittedUid": 5 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component_with_no_max_indications.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component_with_no_max_indications.json new file mode 100644 index 00000000..3846498c --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_reporting_supervised_component_with_no_max_indications.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "reporting_supervised_component_with_no_max_indications", + "processShortName": "reporting_supervised_component_with_no_max_indications", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_reporting_supervised_component_with_no_max_indications", + "refProcessIndex": 2, + "permittedUid": 5 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_state_manager.json b/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_state_manager.json new file mode 100644 index 00000000..fb4d46dd --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/expected_output/hmproc_state_manager.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 0, + "permittedUid": 4 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/health_config_test/input/lm_config.json b/scripts/config_mapping/tests/health_config_test/input/lm_config.json new file mode 100644 index 00000000..f86abac2 --- /dev/null +++ b/scripts/config_mapping/tests/health_config_test/input/lm_config.json @@ -0,0 +1,137 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + } + }, + "components": { + "non_supervised_comp": { + "description": "Non-supervised component", + "component_properties": { + "binary_name": "bin/comp", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts", + "sandbox": { + "uid": 3 + } + } + }, + "state_manager": { + "description": "StateManager with min_indications set to 0", + "component_properties": { + "binary_name": "sm", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "reporting_cycle": 0.1, + "failed_cycles_tolerance": 0, + "min_indications": 0, + "max_indications": 2 + } + } + }, + "deployment_config": { + "bin_dir" : "/opt/apps/state_manager", + "sandbox": { + "uid": 4 + } + } + }, + "reporting_supervised_component": { + "description": "Component reporting Running state and supervised", + "component_properties": { + "binary_name": "bin/comp", + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": true + }, + "process_arguments": ["-a", "-b"] + }, + "deployment_config": { + "bin_dir": "/opt/scripts", + "sandbox": { + "uid": 5 + } + } + }, + "reporting_supervised_component_with_no_max_indications": { + "description": "Component reporting Running state and supervised with max_indications=0", + "component_properties": { + "binary_name": "bin/comp", + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": true, + "alive_supervision": { + "max_indications": 0 + } + }, + "process_arguments": ["-a", "-b"] + }, + "deployment_config": { + "bin_dir": "/opt/scripts", + "sandbox": { + "uid": 5 + } + } + } + }, + "run_targets": { + "Startup": { + "description": "Minimal functionality of the system", + "depends_on": ["state_manager"], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["reporting_supervised_component", "reporting_supervised_component_with_no_max_indications", "Startup"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.123 + }, + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json new file mode 100644 index 00000000..619e3082 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hm_demo.json @@ -0,0 +1,359 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [ + { + "index": 0, + "shortName": "test_app2", + "identifier": "test_app2", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + }, + { + "index": 1, + "shortName": "someip-daemon", + "identifier": "someip-daemon", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + }, + { + "index": 2, + "shortName": "test_app1", + "identifier": "test_app1", + "processType": "REGULAR_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + }, + { + "index": 3, + "shortName": "state_manager", + "identifier": "state_manager", + "processType": "STM_PROCESS", + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ], + "processExecutionErrors": [ + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + }, + { + "processExecutionError": 1 + } + ] + } + ], + "hmMonitorInterface": [ + { + "instanceSpecifier": "test_app2", + "processShortName": "test_app2", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app2", + "refProcessIndex": 0, + "permittedUid": 2000 + }, + { + "instanceSpecifier": "someip-daemon", + "processShortName": "someip-daemon", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_someip-daemon", + "refProcessIndex": 1, + "permittedUid": 1000 + }, + { + "instanceSpecifier": "test_app1", + "processShortName": "test_app1", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app1", + "refProcessIndex": 2, + "permittedUid": 1000 + }, + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 3, + "permittedUid": 1000 + } + ], + "hmSupervisionCheckpoint": [ + { + "shortName": "test_app2_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 0 + }, + { + "shortName": "someip-daemon_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 1 + }, + { + "shortName": "test_app1_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 2 + }, + { + "shortName": "state_manager_checkpoint", + "checkpointId": 1, + "refInterfaceIndex": 3 + } + ], + "hmAliveSupervision": [ + { + "ruleContextKey": "test_app2_alive_supervision", + "refCheckPointIndex": 0, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 0, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + }, + { + "ruleContextKey": "someip-daemon_alive_supervision", + "refCheckPointIndex": 1, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 1, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + }, + { + "ruleContextKey": "test_app1_alive_supervision", + "refCheckPointIndex": 2, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 2, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + }, + { + "ruleContextKey": "state_manager_alive_supervision", + "refCheckPointIndex": 3, + "aliveReferenceCycle": 500, + "minAliveIndications": 1, + "maxAliveIndications": 3, + "isMinCheckDisabled": false, + "isMaxCheckDisabled": false, + "failedSupervisionCyclesTolerance": 2, + "refProcessIndex": 3, + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ], + "hmDeadlineSupervision": [], + "hmLogicalSupervision": [], + "hmLocalSupervision": [ + { + "ruleContextKey": "test_app2_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 0 + } + ] + }, + { + "ruleContextKey": "someip-daemon_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 1 + } + ] + }, + { + "ruleContextKey": "test_app1_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 2 + } + ] + }, + { + "ruleContextKey": "state_manager_local_supervision", + "infoRefInterfacePath": "", + "hmRefAliveSupervision": [ + { + "refAliveSupervisionIdx": 3 + } + ] + } + ], + "hmGlobalSupervision": [ + { + "ruleContextKey": "global_supervision", + "isSeverityCritical": false, + "localSupervision": [ + { + "refLocalSupervisionIndex": 0 + }, + { + "refLocalSupervisionIndex": 1 + }, + { + "refLocalSupervisionIndex": 2 + }, + { + "refLocalSupervisionIndex": 3 + } + ], + "refProcesses": [ + { + "index": 0 + }, + { + "index": 1 + }, + { + "index": 2 + }, + { + "index": 3 + } + ], + "refProcessGroupStates": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ], + "hmRecoveryNotification": [ + { + "shortName": "recovery_notification", + "recoveryNotificationTimeout": 5000, + "processGroupMetaModelIdentifier": "MainPG/fallback_run_target", + "refGlobalSupervisionIndex": 0, + "instanceSpecifier": "", + "shouldFireWatchdog": false + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json new file mode 100644 index 00000000..f06adf40 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hmcore.json @@ -0,0 +1,20 @@ +{ + "versionMajor": 3, + "versionMinor": 0, + "watchdogs": [ + { + "shortName": "watchdog", + "deviceFilePath": "/dev/watchdog", + "maxTimeout": 2000, + "deactivateOnShutdown": true, + "hasValueDeactivateOnShutdown": true, + "requireMagicClose": false, + "hasValueRequireMagicClose": true + } + ], + "config": [ + { + "periodicity": 500 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_someip-daemon.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_someip-daemon.json new file mode 100644 index 00000000..bb905848 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_someip-daemon.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "someip-daemon", + "processShortName": "someip-daemon", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_someip-daemon", + "refProcessIndex": 1, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_state_manager.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_state_manager.json new file mode 100644 index 00000000..d06eca18 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_state_manager.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "state_manager", + "processShortName": "state_manager", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_state_manager", + "refProcessIndex": 3, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app1.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app1.json new file mode 100644 index 00000000..c1ee2466 --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app1.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "test_app1", + "processShortName": "test_app1", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app1", + "refProcessIndex": 2, + "permittedUid": 1000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app2.json b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app2.json new file mode 100644 index 00000000..f06af1ec --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/hmproc_test_app2.json @@ -0,0 +1,15 @@ +{ + "versionMajor": 8, + "versionMinor": 0, + "process": [], + "hmMonitorInterface": [ + { + "instanceSpecifier": "test_app2", + "processShortName": "test_app2", + "portPrototype": "DefaultPort", + "interfacePath": "lifecycle_health_test_app2", + "refProcessIndex": 0, + "permittedUid": 2000 + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json new file mode 100644 index 00000000..ee75721c --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/expected_output/lm_demo.json @@ -0,0 +1,437 @@ +{ + "versionMajor": 7, + "versionMinor": 0, + "Process": [ + { + "identifier": "test_app2", + "path": "/opt/apps/test_app2/test_app2", + "uid": 2000, + "gid": 2000, + "sgids": [ + { + "sgid": 800 + }, + { + "sgid": 900 + } + ], + "securityPolicyDetails": "policy_name_2", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "test_app2_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_FIFO", + "schedulingPriority": "99", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Startup" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "overridden_value" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + }, + { + "key": "APP_SPECIFIC_ENV_VAR", + "value": "def" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [] + } + ] + }, + { + "identifier": "setup_filesystem_sh", + "path": "/opt/scripts/bin/setup_filesystem.sh", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "DoesNotReportExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "setup_filesystem_sh_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Startup" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + } + ], + "executionDependency": [ + { + "stateName": "Running", + "targetProcess_identifier": "test_app2" + } + ] + } + ] + }, + { + "identifier": "dlt-daemon", + "path": "/opt/apps/dlt-daemon/dltd", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "DoesNotReportExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "dlt-daemon_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Terminated", + "targetProcess_identifier": "setup_filesystem_sh" + } + ] + } + ] + }, + { + "identifier": "someip-daemon", + "path": "/opt/apps/someip/someipd", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "someip-daemon_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [] + } + ] + }, + { + "identifier": "test_app1", + "path": "/opt/apps/test_app1/test_app1", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "startupConfig": [ + { + "executionError": "1", + "identifier": "test_app1_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Running", + "targetProcess_identifier": "dlt-daemon" + }, + { + "stateName": "Running", + "targetProcess_identifier": "someip-daemon" + } + ] + } + ] + }, + { + "identifier": "state_manager", + "path": "/opt/apps/state_manager/sm", + "uid": 1000, + "gid": 1000, + "sgids": [ + { + "sgid": 500 + }, + { + "sgid": 600 + }, + { + "sgid": 700 + } + ], + "securityPolicyDetails": "policy_name", + "numberOfRestartAttempts": 1, + "executable_reportingBehavior": "ReportsExecutionState", + "functionClusterAffiliation": "STATE_MANAGEMENT", + "startupConfig": [ + { + "executionError": "1", + "identifier": "state_manager_startup_config", + "enterTimeoutValue": 500, + "exitTimeoutValue": 500, + "schedulingPolicy": "SCHED_OTHER", + "schedulingPriority": "0", + "terminationBehavior": "ProcessIsNotSelfTerminating", + "processGroupStateDependency": [ + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Startup" + }, + { + "stateMachine_name": "MainPG", + "stateName": "MainPG/Full" + } + ], + "environmentVariable": [ + { + "key": "LD_LIBRARY_PATH", + "value": "/opt/lib" + }, + { + "key": "GLOBAL_ENV_VAR", + "value": "abc" + }, + { + "key": "EMPTY_GLOBAL_ENV_VAR", + "value": "" + } + ], + "processArgument": [ + { + "argument": "-a" + }, + { + "argument": "-b" + }, + { + "argument": "--xyz" + } + ], + "executionDependency": [ + { + "stateName": "Terminated", + "targetProcess_identifier": "setup_filesystem_sh" + } + ] + } + ] + } + ], + "ModeGroup": [ + { + "identifier": "MainPG", + "initialMode_name": "not-used", + "recoveryMode_name": "MainPG/fallback_run_target", + "modeDeclaration": [ + { + "identifier": "MainPG/Startup" + }, + { + "identifier": "MainPG/Full" + }, + { + "identifier": "MainPG/fallback_run_target" + } + ] + } + ] +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/lm_config_test/input/lm_config.json b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json new file mode 100644 index 00000000..f825876f --- /dev/null +++ b/scripts/config_mapping/tests/lm_config_test/input/lm_config.json @@ -0,0 +1,191 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib", + "GLOBAL_ENV_VAR": "abc", + "EMPTY_GLOBAL_ENV_VAR": "" + }, + "bin_dir": "/opt", + "working_dir": "/tmp", + "ready_recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + }, + "sandbox": { + "uid": 1000, + "gid": 1000, + "supplementary_group_ids": [500, 600, 700], + "security_policy": "policy_name", + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0, + "max_memory_usage": 1024, + "max_cpu_usage": 75 + } + }, + "component_properties": { + "binary_name": "test_app1", + "application_profile": { + "application_type": "Reporting_And_Supervised", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.5, + "failed_cycles_tolerance": 2, + "min_indications": 1, + "max_indications": 3 + } + }, + "depends_on": ["test_app2"], + "process_arguments": ["-a", "-b", "--xyz"], + "ready_condition": { + "process_state": "Running" + } + }, + "run_target": { + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + } + }, + "components": { + "test_app2": { + "description": "Another simple test application", + "component_properties": { + "binary_name": "test_app2", + "depends_on": [] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app2", + "environmental_variables": { + "GLOBAL_ENV_VAR": "overridden_value", + "APP_SPECIFIC_ENV_VAR": "def" + }, + "sandbox": { + "uid": 2000, + "gid": 2000, + "supplementary_group_ids": [800, 900], + "security_policy": "policy_name_2", + "scheduling_policy": "SCHED_FIFO", + "scheduling_priority": 99, + "max_memory_usage": 2048, + "max_cpu_usage": 50 + } + } + }, + "setup_filesystem_sh": { + "description": "Script to mount partitions at the right directories", + "component_properties": { + "binary_name": "bin/setup_filesystem.sh", + "application_profile": { + "application_type": "Native", + "is_self_terminating": true + }, + "process_arguments": ["-a", "-b"], + "ready_condition": { + "process_state": "Terminated" + } + }, + "deployment_config": { + "bin_dir": "/opt/scripts" + } + }, + "dlt-daemon": { + "description": "Logging application", + "component_properties": { + "binary_name": "dltd", + "application_profile": { + "application_type": "Native" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/dlt-daemon" + } + }, + "someip-daemon": { + "description": "SOME/IP application", + "component_properties": { + "binary_name": "someipd", + "depends_on": [] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/someip" + } + }, + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1", + "depends_on": ["dlt-daemon", "someip-daemon"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + }, + "state_manager": { + "description": "Application that manages life cycle of the ECU", + "component_properties": { + "binary_name": "sm", + "application_profile": { + "application_type": "State_Manager" + }, + "depends_on": ["setup_filesystem_sh"] + }, + "deployment_config": { + "bin_dir" : "/opt/apps/state_manager" + } + } + }, + "run_targets": { + "Startup": { + "description": "Minimal functionality of the system", + "depends_on": ["state_manager"], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Full": { + "description": "Everything running", + "depends_on": ["test_app1", "Startup"], + "transition_timeout": 5, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + } + }, + "initial_run_target": "Startup", + "fallback_run_target": { + "description": "Switching off everything", + "depends_on": [], + "transition_timeout": 1.5 + }, + "alive_supervision" : { + "evaluation_cycle": 0.5 + }, + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } +} \ No newline at end of file diff --git a/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json b/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json new file mode 100644 index 00000000..9fd06e96 --- /dev/null +++ b/scripts/config_mapping/tests/schema_validation_failure_test/input/lm_config.json @@ -0,0 +1,21 @@ +{ + "schema_version": 1, + "defaults": {}, + "components": { + "test_app1": { + "description": "Simple test application", + "component_properties": { + "binary_name": "test_app1" + }, + "deployment_config": { + "bin_dir" : "/opt/apps/test_app1" + } + } + }, + "run_targets": { + "Minimal": { + "description": "Run Target with missing recovery action", + "depends_on": [] + } + } +} \ No newline at end of file diff --git a/scripts/config_mapping/unit_tests.py b/scripts/config_mapping/unit_tests.py new file mode 100644 index 00000000..33635d2e --- /dev/null +++ b/scripts/config_mapping/unit_tests.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +from scripts.config_mapping.lifecycle_config import preprocess_defaults +import json + + +def test_preprocessing_basic(): + """ + Basic smoketest for the preprocess_defaults function, to ensure that defaults are being applied and overridden correctly. + """ + + global_defaults = json.loads(""" + { + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 0.5, + "environmental_variables" : { + "global_default1": "global_default_value1", + "global_default2": "global_default_value2" + }, + "sandbox": { + "uid": 0, + "supplementary_group_ids": [100] + } + }, + "component_properties": { + "application_profile": { + "application_type": "REPORTING", + "is_self_terminating": false + } + }, + "alive_supervision": { + "evaluation_cycle": 0.5 + }, + "watchdog": {} + }""") + + config = json.loads("""{ + "defaults": { + "deployment_config": { + "shutdown_timeout": 1.0, + "environmental_variables" : { + "global_default2": "config_default_overwritten_value2", + "config_default3": "config_default_value3", + "config_default4": "config_default_value4" + }, + "recovery_action": { + "restart": { + "number_of_attempts": 1, + "delay_before_restart": 0.5 + } + } + }, + "component_properties": { + + } + }, + "components": { + "test_comp": { + "description": "Test component", + "component_properties": { + + }, + "deployment_config": { + "environmental_variables": { + "config_default3": "config_overwritten_value3" + }, + "sandbox": { + "uid": 0, + "gid": 1, + "supplementary_group_ids": [101] + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + } + } + }, + "run_targets": {}, + "alive_supervision": { + "evaluation_cycle": 0.1 + }, + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + }""") + + preprocessed_config = preprocess_defaults(global_defaults, config) + + expected_config = json.loads("""{ + "components": { + "test_comp": { + "description": "Test component", + "component_properties": { + "application_profile": { + "application_type": "REPORTING", + "is_self_terminating": false + } + }, + "deployment_config": { + "ready_timeout": 0.5, + "shutdown_timeout": 1.0, + "environmental_variables" : { + "global_default1": "global_default_value1", + "global_default2": "config_default_overwritten_value2", + "config_default3": "config_overwritten_value3", + "config_default4": "config_default_value4" + }, + "sandbox": { + "uid": 0, + "gid":1, + "supplementary_group_ids": [101] + }, + "recovery_action": { + "switch_run_target": { + "run_target": "Off" + } + } + } + } + }, + "run_targets": {}, + "alive_supervision": { + "evaluation_cycle": 0.1 + }, + "watchdog": { + "device_file_path": "/dev/watchdog", + "max_timeout": 2, + "deactivate_on_shutdown": true, + "require_magic_close": false + } + }""") + + print("Dumping preprocessed configuration:") + print(json.dumps(preprocessed_config, indent=4)) + + assert preprocessed_config == expected_config, ( + "Preprocessed config does not match expected config." + ) diff --git a/src/control_client_lib/BUILD b/src/control_client_lib/BUILD index 3d776e5a..d119de27 100644 --- a/src/control_client_lib/BUILD +++ b/src/control_client_lib/BUILD @@ -17,10 +17,10 @@ cc_library( "src/control_client.cpp", "src/control_client_impl.cpp", "src/control_client_impl.hpp", + "src/execution_error_event.h", ], hdrs = [ "include/score/lcm/control_client.h", - "include/score/lcm/execution_error_event.h", ], includes = ["include"], visibility = ["//visibility:public"], diff --git a/src/control_client_lib/include/score/lcm/control_client.h b/src/control_client_lib/include/score/lcm/control_client.h index f7a22df0..020ed5f9 100644 --- a/src/control_client_lib/include/score/lcm/control_client.h +++ b/src/control_client_lib/include/score/lcm/control_client.h @@ -13,14 +13,12 @@ #ifndef CONTROL_CLIENT_H_ #define CONTROL_CLIENT_H_ -#include -#include - #include "score/concurrency/future/interruptible_future.h" #include "score/concurrency/future/interruptible_promise.h" #include "score/result/result.h" +#include +#include #include "score/lcm/exec_error_domain.h" -#include "score/lcm/execution_error_event.h" namespace score { @@ -28,22 +26,14 @@ namespace lcm { class ControlClientImpl; -/// @brief Class representing connection to Launch Manager that is used to request Process Group state transitions (or other operations). +/// @brief Class representing connection to Launch Manager that is used to request Run Target activation (or other operations). /// @note ControlClient opens communication channel to Launch Manager (e.g. POSIX FIFO). Each Process that intends to perform state management, shall create an instance of this class and it shall have rights to use it. /// class ControlClient final { public: /// @brief Constructor that creates Control Client instance. /// - /// Registers given callback which is called in case a Process Group changes its state unexpectedly to an Undefined - /// Process Group State. - /// - /// @param[in] undefinedStateCallback callback to be invoked by ControlClient library if a ProcessGroup changes its - /// state unexpectedly to an Undefined Process Group State, i.e. without - /// previous request by SetState(). The affected ProcessGroup and ExecutionError - /// is provided as an argument to the callback in form of ExecutionErrorEvent. - /// - explicit ControlClient(std::function undefinedStateCallback) noexcept; + explicit ControlClient() noexcept; /// @brief Destructor of the Control Client instance. /// @param None. @@ -70,60 +60,27 @@ class ControlClient final { /// @returns the new reference ControlClient& operator=(ControlClient&& rval) noexcept; - /// @brief Method to request state transition for a single Process Group. - /// - /// This method will request Launch Manager to perform state transition and return immediately. - /// Returned InterruptibleFuture can be used to determine result of requested transition. - /// - /// @param[in] pg_name representing meta-model definition of a specific Process Group - /// @param[in] pg_state representing meta-model definition of a state. Launch Manager will perform state transition from the current state to the state identified by this parameter. - /// @returns void if requested transition is successful, otherwise it returns ExecErrorDomain error. - /// @error score::lcm::ExecErrc::kCancelled if transition to the requested Process Group state was cancelled by a newer request - /// @error score::lcm::ExecErrc::kFailed if transition to the requested Process Group state failed - /// @error score::lcm::ExecErrc::kFailedUnexpectedTerminationOnExit if Unexpected Termination in Process of previous Process Group State happened. - /// @error score::lcm::ExecErrc::kFailedUnexpectedTerminationOnEnter if Unexpected Termination in Process of target Process Group State happened. - /// @error score::lcm::ExecErrc::kInvalidArguments if arguments passed doesn't appear to be valid (e.g. after a software update, given processGroup doesn't exist anymore) - /// @error score::lcm::ExecErrc::kCommunicationError if ControlClient can't communicate with Launch Manager (e.g. IPC link is down) - /// @error score::lcm::ExecErrc::kAlreadyInState if the ProcessGroup is already in the requested state - /// @error score::lcm::ExecErrc::kInTransitionToSameState if a transition to the requested state is already ongoing - /// @error score::lcm::ExecErrc::kInvalidTransition if transition to the requested state is prohibited (e.g. Off state for MainPG) - /// @error score::lcm::ExecErrc::kGeneralError if any other error occurs. - /// - /// @threadsafety{thread-safe} - /// - score::concurrency::InterruptibleFuture SetState(const IdentifierHash& pg_name, const IdentifierHash& pg_state) const noexcept; - - /// @brief Method to retrieve result of Machine State initial transition to Startup state. + /// @brief Method to request activation of a specific Run Target. /// - /// This method allows State Management to retrieve result of a transition. - /// Please note that this transition happens once per machine life cycle, thus result delivered by this method shall not change (unless machine is started again). + /// This method will request Launch Manager to activate a Run Target and return immediately. + /// Returned InterruptibleFuture can be used to determine result of the activation request. /// - /// @param None. - /// @returns void if requested transition is successful, otherwise it returns ExecErrorDomain error. - /// @error score::lcm::ExecErrc::kCancelled if transition to the requested Process Group state was cancelled by a newer request - /// @error score::lcm::ExecErrc::kFailed if transition to the requested Process Group state failed + /// @param[in] runTargetName name of the Run Target that should be activated. Launch Manager will deactivate the currently active Run Target and activate Run Target identified by this parameter. + /// @returns void if activation requested is successful, otherwise it returns ExecErrorDomain error. + /// @error score::lcm::ExecErrc::kCancelled if activation requested was cancelled by a newer request + /// @error score::lcm::ExecErrc::kFailed if activation requested failed + /// @error score::lcm::ExecErrc::kFailedUnexpectedTerminationOnExit if Unexpected Termination of a Process assigned to the previously active Run Target happened. + /// @error score::lcm::ExecErrc::kFailedUnexpectedTerminationOnEnter if Unexpected Termination of a Process assigned the requested Run Target happened. + /// @error score::lcm::ExecErrc::kInvalidArguments if argument passed doesn't appear to be valid (e.g. after a software update, given Run Target doesn't exist anymore) /// @error score::lcm::ExecErrc::kCommunicationError if ControlClient can't communicate with Launch Manager (e.g. IPC link is down) + /// @error score::lcm::ExecErrc::kAlreadyInState if the requested Run Target is already active + /// @error score::lcm::ExecErrc::kInTransitionToSameState if there is already an ongoing request to activate requested Run Target + /// @error score::lcm::ExecErrc::kInvalidTransition if activation of the requested Run Target is prohibited (e.g. Off Run Target) /// @error score::lcm::ExecErrc::kGeneralError if any other error occurs. /// /// @threadsafety{thread-safe} /// - score::concurrency::InterruptibleFuture GetInitialMachineStateTransitionResult() const noexcept; - - /// @brief Returns the execution error which changed the given Process Group to an Undefined Process Group State. - /// - /// This function will return with error and will not return an ExecutionErrorEvent object, if the given - /// Process Group is in a defined Process Group state again. - /// - /// @param[in] processGroup Process Group of interest. - /// - /// @returns The execution error which changed the given Process Group to an Undefined Process Group State. - /// @error score::lcm::ExecErrc::kFailed Given Process Group is not in an Undefined Process Group State. - /// @error score::lcm::ExecErrc::kCommunicationError if ControlClient can't communicate with Launch Manager (e.g. IPC link is down) - /// - /// @threadsafety{thread-safe} - /// - score::Result GetExecutionError( - const IdentifierHash& processGroup) noexcept; + score::concurrency::InterruptibleFuture ActivateRunTarget(std::string_view runTargetName) const noexcept; private: /// @brief Pointer to implementation (Pimpl), we use this pattern to provide ABI compatibility. diff --git a/src/control_client_lib/src/control_client.cpp b/src/control_client_lib/src/control_client.cpp index 179a51c3..c20d9f42 100644 --- a/src/control_client_lib/src/control_client.cpp +++ b/src/control_client_lib/src/control_client.cpp @@ -30,7 +30,8 @@ inline score::concurrency::InterruptibleFuture GetErrorFuture(score::lcm:: return tmp_.GetInterruptibleFuture().value(); } -ControlClient::ControlClient(std::function undefinedStateCallback) noexcept { +ControlClient::ControlClient() noexcept { + static std::function undefinedStateCallback = [](const score::lcm::ExecutionErrorEvent& event) {}; try { control_client_impl_ = std::make_unique(undefinedStateCallback); } catch (...) { @@ -50,12 +51,14 @@ ControlClient::ControlClient(ControlClient&& rval) noexcept { ControlClient& ControlClient::operator=(ControlClient&& rval) noexcept = default; -score::concurrency::InterruptibleFuture ControlClient::SetState( const IdentifierHash& pg_name, const IdentifierHash& pg_state ) const noexcept +score::concurrency::InterruptibleFuture ControlClient::ActivateRunTarget(std::string_view runTargetName) const noexcept { score::concurrency::InterruptibleFuture retVal_ {}; if( control_client_impl_ != nullptr ) { + static IdentifierHash pg_name{"MainPG"}; + IdentifierHash pg_state{"MainPG/" + std::string(runTargetName)}; retVal_ = control_client_impl_->SetState(pg_name, pg_state); } else @@ -66,34 +69,6 @@ score::concurrency::InterruptibleFuture ControlClient::SetState( const Ide return retVal_; } -score::concurrency::InterruptibleFuture ControlClient::GetInitialMachineStateTransitionResult() const noexcept { - score::concurrency::InterruptibleFuture retVal_{}; - - if( control_client_impl_ != nullptr ) - { - retVal_ = control_client_impl_->GetInitialMachineStateTransitionResult(); - } - else - { - retVal_ = GetErrorFuture(ExecErrc::kCommunicationError); - } - - return retVal_; -} - -score::Result ControlClient::GetExecutionError( - const score::lcm::IdentifierHash& processGroup ) noexcept -{ - score::Result retVal_ {score::MakeUnexpected(score::lcm::ExecErrc::kCommunicationError) }; - - if (control_client_impl_ != nullptr) { - retVal_ = control_client_impl_->GetExecutionError(processGroup); - } - // else not needed as kCommunicationError is the default return value - - return retVal_; -} - } // namespace lcm } // namespace score diff --git a/src/control_client_lib/src/control_client_impl.cpp b/src/control_client_lib/src/control_client_impl.cpp index 41fd6c80..804857ca 100644 --- a/src/control_client_lib/src/control_client_impl.cpp +++ b/src/control_client_lib/src/control_client_impl.cpp @@ -21,7 +21,6 @@ #include "score/concurrency/future/interruptible_future.h" #include "score/concurrency/future/interruptible_promise.h" -#include #include #include diff --git a/src/control_client_lib/src/control_client_impl.hpp b/src/control_client_lib/src/control_client_impl.hpp index 5a0e0080..d0a89b45 100644 --- a/src/control_client_lib/src/control_client_impl.hpp +++ b/src/control_client_lib/src/control_client_impl.hpp @@ -24,6 +24,7 @@ #include #include #include +#include "execution_error_event.h" namespace score { diff --git a/src/control_client_lib/include/score/lcm/execution_error_event.h b/src/control_client_lib/src/execution_error_event.h similarity index 100% rename from src/control_client_lib/include/score/lcm/execution_error_event.h rename to src/control_client_lib/src/execution_error_event.h diff --git a/src/launch_manager_daemon/BUILD b/src/launch_manager_daemon/BUILD index 26da0e66..b6b2e6f4 100644 --- a/src/launch_manager_daemon/BUILD +++ b/src/launch_manager_daemon/BUILD @@ -16,13 +16,13 @@ package(default_visibility = ["//tests:__subpackages__"]) filegroup( name = "lm_flatcfg_fbs", - srcs = ["config/lm_flatcfg.fbs"], + srcs = ["//src/launch_manager_daemon/config:lm_flatcfg.fbs"], visibility = ["//visibility:public"], ) cc_library( name = "config", - hdrs = ["config/lm_flatcfg_generated.h"], + hdrs = ["//src/launch_manager_daemon/config:lm_flatcfg_generated.h"], includes = ["config"], visibility = ["//src:__pkg__"], ) diff --git a/src/launch_manager_daemon/config/BUILD b/src/launch_manager_daemon/config/BUILD new file mode 100644 index 00000000..67f38aa8 --- /dev/null +++ b/src/launch_manager_daemon/config/BUILD @@ -0,0 +1,16 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +exports_files([ + "lm_flatcfg.fbs", + "lm_flatcfg_generated.h", +]) diff --git a/src/launch_manager_daemon/config/config_schema/BUILD b/src/launch_manager_daemon/config/config_schema/BUILD new file mode 100644 index 00000000..6b2338ca --- /dev/null +++ b/src/launch_manager_daemon/config/config_schema/BUILD @@ -0,0 +1,13 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +exports_files(["s-core_launch_manager.schema.json"]) diff --git a/src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs b/src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs index 12ac79ca..ba3f5acd 100644 --- a/src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs +++ b/src/launch_manager_daemon/health_monitor_lib/config/hm_flatcfg.fbs @@ -149,7 +149,7 @@ table HmRefProcessGroupStatesGlobal { } table RecoveryNotification { - shortName: string (id:0); + shortName: string (id:0, required); recoveryNotificationTimeout: double (id:1); processGroupMetaModelIdentifier: string (id:2); refGlobalSupervisionIndex: uint32 (id:3); diff --git a/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/recovery/Notification.cpp b/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/recovery/Notification.cpp index c13fc8ef..62269859 100644 --- a/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/recovery/Notification.cpp +++ b/src/launch_manager_daemon/health_monitor_lib/src/score/lcm/saf/recovery/Notification.cpp @@ -155,12 +155,22 @@ void Notification::verifyRecoveryHandlerResponse(void) const auto result = recoveryStateFutureOutput.Get(score::cpp::stop_token{}); if (!result.has_value()) { - logger_r.LogWarn() << messageHeader << "Recovery state request returned with error:" << result.error().Message(); - logger_r.LogDebug() << messageHeader << "Incorrect response received from the Recovery state request call"; + // We may be already in the requested state, due to LM executing recovery on its own. + // This is e.g. the case if the process crashed and LM transitions to its recovery state before supervisions expire. + if (*result.error() == static_cast(score::lcm::ExecErrc::kAlreadyInState)) + { + logger_r.LogWarn() << messageHeader + << "Recovery state request returned:" << result.error().Message(); + } + else { + logger_r.LogWarn() << messageHeader + << "Recovery state request returned with error:" << result.error().Message(); + logger_r.LogDebug() << messageHeader << "Incorrect response received from the Recovery state request call"; - startTimestamp = 0U; - setFinalTimeoutState(); - return; + startTimestamp = 0U; + setFinalTimeoutState(); + return; + } } logger_r.LogDebug() << messageHeader << "Correct response received from the Recovery state request call"; diff --git a/src/launch_manager_daemon/src/configuration_manager/configurationmanager.cpp b/src/launch_manager_daemon/src/configuration_manager/configurationmanager.cpp index 0d79443d..cb0420e2 100644 --- a/src/launch_manager_daemon/src/configuration_manager/configurationmanager.cpp +++ b/src/launch_manager_daemon/src/configuration_manager/configurationmanager.cpp @@ -304,14 +304,14 @@ bool ConfigurationManager::parseMachineConfigurations(const ModeGroup* node, con process_group_data.name_ = getStringViewFromFlatBuffer(node->identifier()); process_group_data.sw_cluster_ = cluster; LM_LOG_DEBUG() << "FlatBufferParser::getModeGroupPgName:" << getStringFromFlatBuffer(node->identifier()) - << "( IdentifierHash:" << process_group_data.name_ << ")"; + << "( IdentifierHash:" << process_group_data.name_.data() << ")"; if (process_group_data.name_ != score::lcm::IdentifierHash(std::string_view(""))) { // Add process group name to the PG name list process_group_names_.push_back(process_group_data.name_); result = parseModeGroups(node, process_group_data); } else { - LM_LOG_WARN() << "parseMachineConfigurations: Process group name is empty furz"; + LM_LOG_WARN() << "parseMachineConfigurations: Process group name is empty"; } } return result; @@ -339,7 +339,7 @@ bool ConfigurationManager::parseModeGroups(const ModeGroup* node, ProcessGroup& pg_state.name_ = getStringViewFromFlatBuffer(flatbuffer_string); LM_LOG_DEBUG() << "FlatBufferParser::getModeGroupPgStateName:" << mode_declaration_node->identifier()->c_str() - << "( IdentifierHash:" << pg_state.name_ << ")"; + << "( IdentifierHash:" << pg_state.name_.data() << ")"; process_group_data.states_.push_back(pg_state); // Is this the "Off" state, i.e. does it end with "/Off" ? auto str_len = string_name.length(); diff --git a/src/launch_manager_daemon/src/process_group_manager/processgroupmanager.cpp b/src/launch_manager_daemon/src/process_group_manager/processgroupmanager.cpp index a4c94f1c..fdcef4c9 100644 --- a/src/launch_manager_daemon/src/process_group_manager/processgroupmanager.cpp +++ b/src/launch_manager_daemon/src/process_group_manager/processgroupmanager.cpp @@ -708,7 +708,7 @@ inline void ProcessGroupManager::processGroupHandler(Graph& pg) } } - if (GraphState::kUndefinedState == graph_state) + if (GraphState::kUndefinedState == pg.getState()) { // at the moment graph is not running... // i.e. it is not in kInTransition, kAborting or kCancelled state diff --git a/tests/integration/BUILD b/tests/integration/BUILD index b2ba66a2..4fa07631 100644 --- a/tests/integration/BUILD +++ b/tests/integration/BUILD @@ -1,3 +1,5 @@ +load("@rules_python//python:pip.bzl", "compile_pip_requirements") + # ******************************************************************************* # Copyright (c) 2026 Contributors to the Eclipse Foundation # @@ -10,8 +12,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@pip_score_venv_test//:requirements.bzl", "all_requirements") -load("@rules_python//python:pip.bzl", "compile_pip_requirements") +load("@score_lifecycle_pip//:requirements.bzl", "all_requirements") load("@score_tooling//python_basics:defs.bzl", "score_py_pytest", "score_virtualenv") # In order to update the requirements, change the `requirements.txt` file and run: diff --git a/tests/integration/smoke/BUILD b/tests/integration/smoke/BUILD index 1deb9a9f..2727804a 100644 --- a/tests/integration/smoke/BUILD +++ b/tests/integration/smoke/BUILD @@ -10,29 +10,26 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@pip_score_venv_test//:requirements.bzl", "all_requirements") +load("@score_lifecycle_pip//:requirements.bzl", "all_requirements") load("@score_tooling//:defs.bzl", "score_py_pytest") +load("//:defs.bzl", "launch_manager_config") load("//config:flatbuffers_rules.bzl", "flatbuffer_json_to_bin") -flatbuffer_json_to_bin( - name = "test_lm_cfg", - json = "lm_demo.json", - schema = "//src/launch_manager_daemon:config/lm_flatcfg.fbs", +exports_files( + ["lm_demo.json"], + visibility = ["//examples:__subpackages__"], ) -flatbuffer_json_to_bin( - name = "test_hm_cfg", - json = "hm_demo.json", - schema = "//src/launch_manager_daemon/health_monitor_lib:config/hm_flatcfg.fbs", +launch_manager_config( + name = "lm_smoketest_config", + config = "//tests/integration/smoke:lifecycle_smoketest.json", + flatbuffer_out_dir = "etc", ) cc_binary( name = "control_daemon_mock", srcs = ["control_daemon_mock.cpp"], - data = [ - ":test_hm_cfg", - ":test_lm_cfg", - ], + data = [], deps = [ "//src/control_client_lib", "//src/launch_manager_daemon/lifecycle_client_lib:lifecycle_client", @@ -62,6 +59,7 @@ score_py_pytest( data = [ ":control_daemon_mock", ":gtest_process", + ":lm_smoketest_config", "//src/launch_manager_daemon:launch_manager", ], tags = [ diff --git a/tests/integration/smoke/control_daemon_mock.cpp b/tests/integration/smoke/control_daemon_mock.cpp index aeea0ae4..95f46126 100644 --- a/tests/integration/smoke/control_daemon_mock.cpp +++ b/tests/integration/smoke/control_daemon_mock.cpp @@ -20,37 +20,26 @@ #include #include "tests/integration/test_helper.hpp" -score::lcm::ControlClient client([](const score::lcm::ExecutionErrorEvent& event) { - std::cerr << "Undefined state callback invoked for process group id: " << event.processGroup.data() << std::endl; -}); - -// create DefaultPG -const score::lcm::IdentifierHash defaultpg {"DefaultPG"}; -const score::lcm::IdentifierHash defaultpgOn {"DefaultPG/On"}; -const score::lcm::IdentifierHash defaultpgOff {"DefaultPG/Off"}; -// MainPG -const score::lcm::IdentifierHash mainpg {"MainPG"}; -const score::lcm::IdentifierHash mainpgOff {"MainPG/Off"}; +score::lcm::ControlClient client; TEST(Smoke, Daemon) { TEST_STEP("Control daemon report kRunning") { // report kRunning auto result = score::lcm::LifecycleClient{}.ReportExecutionState(score::lcm::ExecutionState::kRunning); - ASSERT_TRUE(result.has_value()) << "client.ReportExecutionState() failed"; } - TEST_STEP("Turn default PG on") { + TEST_STEP("Activate RunTarget Running") { score::cpp::stop_token stop_token; - auto result = client.SetState(defaultpg, defaultpgOn).Get(stop_token); + auto result = client.ActivateRunTarget("Running").Get(stop_token); EXPECT_TRUE(result.has_value()); } - TEST_STEP("Turn default PG off") { + TEST_STEP("Activate RunTarget Startup") { score::cpp::stop_token stop_token; - auto result = client.SetState(defaultpg, defaultpgOff).Get(stop_token); + auto result = client.ActivateRunTarget("Startup").Get(stop_token); EXPECT_TRUE(result.has_value()); } - TEST_STEP("Turn main PG off") { - client.SetState(mainpg, mainpgOff); + TEST_STEP("Activate RunTarget Off") { + client.ActivateRunTarget("Off"); } } diff --git a/tests/integration/smoke/gtest_process.cpp b/tests/integration/smoke/gtest_process.cpp index 702e51b9..5c13582f 100644 --- a/tests/integration/smoke/gtest_process.cpp +++ b/tests/integration/smoke/gtest_process.cpp @@ -12,7 +12,6 @@ ********************************************************************************/ #include -#include #include #include diff --git a/tests/integration/smoke/lifecycle_smoketest.json b/tests/integration/smoke/lifecycle_smoketest.json new file mode 100644 index 00000000..1da59b6e --- /dev/null +++ b/tests/integration/smoke/lifecycle_smoketest.json @@ -0,0 +1,116 @@ +{ + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/tests/integration/smoke", + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "ready_recovery_action": { + "restart": { + "number_of_attempts": 0 + } + }, + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + }, + "environmental_variables": { + "LD_LIBRARY_PATH": "/opt/lib" + }, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0 + } + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": false, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1 + } + }, + "ready_condition": { + "process_state": "Running" + } + } + }, + "components": { + "control_daemon": { + "component_properties": { + "binary_name": "control_daemon_mock", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0 + } + } + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": { + "PROCESSIDENTIFIER": "control_daemon" + } + } + }, + "gtest_process": { + "component_properties": { + "binary_name": "gtest_process", + "application_profile": { + "application_type": "Reporting" + } + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "DefaultPG_app0" + } + } + } + }, + "run_targets": { + "Startup": { + "depends_on": [ + "control_daemon" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Running": { + "depends_on": [ + "control_daemon", + "gtest_process" + ], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + } + } + }, + "Off": { + "depends_on": [], + "recovery_action": { + "switch_run_target": { + "run_target": "fallback_run_target" + + } + } + } + }, + "initial_run_target": "Startup", + "alive_supervision": { + "evaluation_cycle": 0.05 + }, + "fallback_run_target": { + "depends_on": [] + } +} \ No newline at end of file diff --git a/tests/integration/smoke/lm_demo.json b/tests/integration/smoke/lm_demo.json deleted file mode 100644 index 10185fc8..00000000 --- a/tests/integration/smoke/lm_demo.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "versionMajor": 7, - "versionMinor": 0, - "Process": [ - { - "identifier": "control_daemon", - "uid": 0, - "gid": 0, - "path": "/tests/integration/smoke/control_daemon_mock", - "functionClusterAffiliation": "STATE_MANAGEMENT", - "numberOfRestartAttempts": 0, - "executable_reportingBehavior": "ReportsExecutionState", - "sgids": [], - "startupConfig": [ - { - "executionError": "1", - "schedulingPolicy": "SCHED_OTHER", - "schedulingPriority": "0", - "identifier": "control_daemon_startup_config", - "enterTimeoutValue": 1000, - "exitTimeoutValue": 1000, - "terminationBehavior": "ProcessIsNotSelfTerminating", - "executionDependency": [], - "processGroupStateDependency": [ - { - "stateMachine_name": "MainPG", - "stateName": "MainPG/Startup" - }, - { - "stateMachine_name": "MainPG", - "stateName": "MainPG/Recovery" - } - ], - "environmentVariable": [ - { - "key": "LD_LIBRARY_PATH", - "value": "/opt/lib" - }, - { - "key": "PROCESSIDENTIFIER", - "value": "control_daemon" - } - ], - "processArgument": [] - } - ] - }, - { - "identifier": "demo_app0_DefaultPG", - "uid": 0, - "gid": 0, - "path": "/tests/integration/smoke/gtest_process", - "numberOfRestartAttempts": 0, - "executable_reportingBehavior": "ReportsExecutionState", - "sgids": [], - "startupConfig": [ - { - "executionError": "1", - "schedulingPolicy": "SCHED_OTHER", - "schedulingPriority": "0", - "identifier": "demo_app_startup_config_0", - "enterTimeoutValue": 2000, - "exitTimeoutValue": 2000, - "terminationBehavior": "ProcessIsNotSelfTerminating", - "processGroupStateDependency": [ - { - "stateMachine_name": "DefaultPG", - "stateName": "DefaultPG/On" - } - ], - "environmentVariable": [ - { - "key": "LD_LIBRARY_PATH", - "value": "/opt/lib" - }, - { - "key": "PROCESSIDENTIFIER", - "value": "DefaultPG_app0" - }, - { - "key": "CONFIG_PATH", - "value": "/opt/supervision_demo/etc/health_monitor_process_cfg_0_MainPG.bin" - } - ] - } - ] - } - ], - "ModeGroup": [ - { - "identifier": "MainPG", - "initialMode_name": "Off", - "recoveryMode_name": "MainPG/Recovery", - "modeDeclaration": [ - { - "identifier": "MainPG/Off" - }, - { - "identifier": "MainPG/Startup" - }, - { - "identifier": "MainPG/Recovery" - } - ] - }, - { - "identifier": "DefaultPG", - "initialMode_name": "Off", - "recoveryMode_name": "DefaultPG/Recovery", - "modeDeclaration": [ - { - "identifier": "DefaultPG/Off" - }, - { - "identifier": "DefaultPG/On" - }, - { - "identifier": "DefaultPG/Recovery" - } - ] - } - ] -} \ No newline at end of file diff --git a/tests/scripts/gen_lifecycle_config.py b/tests/scripts/gen_lifecycle_config.py new file mode 100644 index 00000000..526692a0 --- /dev/null +++ b/tests/scripts/gen_lifecycle_config.py @@ -0,0 +1,223 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +""" +Generates a unified lifecycle configuration file in the S-CORE Launch Manager schema format. + +This replaces the previous gen_health_monitor_cfg.py, gen_health_monitor_process_cfg.py, +and gen_launch_manager_cfg.py scripts. The generated configuration can be converted to +the old format by running: + + python3 scripts/config_mapping/lifecycle_config.py -o +""" + +import argparse +import json +import os +from pathlib import Path + + +def gen_lifecycle_config( + cppprocesses: int, + rustprocesses: int, + non_supervised_processes: int, +): + total_process_count = cppprocesses + rustprocesses + + config = { + "schema_version": 1, + "defaults": { + "deployment_config": { + "bin_dir": "/opt", + "ready_timeout": 2.0, + "shutdown_timeout": 2.0, + "ready_recovery_action": {"restart": {"number_of_attempts": 0}}, + "recovery_action": {"switch_run_target": {"run_target": "Startup"}}, + "environmental_variables": {"LD_LIBRARY_PATH": "/opt/lib"}, + "sandbox": { + "uid": 0, + "gid": 0, + "scheduling_policy": "SCHED_OTHER", + "scheduling_priority": 0, + }, + }, + "component_properties": { + "application_profile": { + "application_type": "Reporting", + "is_self_terminating": False, + "alive_supervision": { + "reporting_cycle": 0.1, + "min_indications": 1, + "max_indications": 3, + "failed_cycles_tolerance": 1, + }, + }, + "ready_condition": {"process_state": "Running"}, + }, + }, + "components": {}, + "run_targets": {}, + "initial_run_target": "Startup", + "alive_supervision": {"evaluation_cycle": 0.05}, + } + + running_deps = [] + + # --- Control daemon --- + config["components"]["control_daemon"] = { + "component_properties": { + "binary_name": "control_app/control_daemon", + "application_profile": { + "application_type": "State_Manager", + "alive_supervision": { + "min_indications": 0, + }, + }, + }, + "deployment_config": { + "ready_timeout": 1.0, + "shutdown_timeout": 1.0, + "environmental_variables": {"PROCESSIDENTIFIER": "control_daemon"}, + }, + } + running_deps.append("control_daemon") + + # --- Supervised demo processes --- + for i in range(total_process_count): + if i >= cppprocesses: + binary = "supervision_demo/rust_supervised_app" + else: + binary = "supervision_demo/cpp_supervised_app" + + comp_name = f"demo_app{i}" + config["components"][comp_name] = { + "component_properties": { + "binary_name": binary, + "application_profile": { + "application_type": "Reporting_And_Supervised", + }, + "process_arguments": ["-d50"], + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": comp_name, + "CONFIG_PATH": f"/opt/supervision_demo/etc/{comp_name}.bin", + "IDENTIFIER": comp_name, + }, + }, + } + running_deps.append(comp_name) + + # --- Non-supervised lifecycle processes --- + for i in range(non_supervised_processes): + comp_name = f"lifecycle_app{i}" + config["components"][comp_name] = { + "component_properties": { + "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app", + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": comp_name, + }, + }, + } + running_deps.append(comp_name) + + # --- Fallback verbose lifecycle app (runs during recovery) --- + config["components"]["fallback_app"] = { + "component_properties": { + "binary_name": "cpp_lifecycle_app/cpp_lifecycle_app", + "process_arguments": ["-v"], + }, + "deployment_config": { + "environmental_variables": { + "PROCESSIDENTIFIER": "fallback_app", + }, + }, + } + + # --- Run targets --- + config["run_targets"]["Startup"] = { + "depends_on": ["control_daemon"], + "recovery_action": {"switch_run_target": {"run_target": "Startup"}}, + } + + config["run_targets"]["Running"] = { + "depends_on": running_deps, + "recovery_action": {"switch_run_target": {"run_target": "Startup"}}, + } + + # Fallback run target: control daemon + verbose app run during recovery + config["fallback_run_target"] = {"depends_on": ["control_daemon", "fallback_app"]} + + return config + + +if __name__ == "__main__": + my_parser = argparse.ArgumentParser( + description="Generate unified lifecycle configuration in the S-CORE Launch Manager schema format." + ) + my_parser.add_argument( + "-c", + "--cppprocesses", + action="store", + type=int, + required=True, + help="Number of C++ supervised processes", + ) + my_parser.add_argument( + "-r", + "--rustprocesses", + action="store", + type=int, + required=True, + help="Number of Rust supervised processes", + ) + my_parser.add_argument( + "-n", + "--non-supervised-processes", + action="store", + type=int, + required=True, + help="Number of C++ non-supervised (lifecycle) processes", + ) + my_parser.add_argument( + "-o", + "--out", + action="store", + type=Path, + required=True, + help="Output directory", + ) + args = my_parser.parse_args() + + if args.cppprocesses < 0 or args.cppprocesses > 10000: + print("Number of C++ processes must be between 0 and 10000") + exit(1) + if args.rustprocesses < 0 or args.rustprocesses > 10000: + print("Number of Rust processes must be between 0 and 10000") + exit(1) + if args.non_supervised_processes < 0 or args.non_supervised_processes > 10000: + print("Number of non-supervised processes must be between 0 and 10000") + exit(1) + + cfg = gen_lifecycle_config( + args.cppprocesses, + args.rustprocesses, + args.non_supervised_processes, + ) + + cfg_out_path = os.path.join(args.out, "lifecycle_demo.json") + with open(cfg_out_path, "w") as f: + json.dump(cfg, f, indent=4) + + print(f"Generated lifecycle configuration: {cfg_out_path}")