diff --git a/meta-digi-containers/README.md b/meta-digi-containers/README.md index b3a4a5035..e9bbd698f 100644 --- a/meta-digi-containers/README.md +++ b/meta-digi-containers/README.md @@ -98,24 +98,36 @@ This generates a bundle named like: `dey-image-container-manager` includes `cc-container-mng` that has the capability to publish container statistics through the local CCCS Python API. -For generated DCPs, per-container DRM sampling is enabled -through `registration_defaults.stats_publish` in the artifact manifest. +For generated DCPs, DRM behavior is controlled through `registration_defaults.drm` +in the artifact manifest. Those manifest defaults establish the initial runtime policy on the target. After installation, mutable policy such as `autostart`, `monitor`, `restart`, and -`stats_publish` can be inspected or updated through the container manager `config` +`drm` can be inspected or updated through the container manager `config` `get` and `set` operations without regenerating the DCP. The image recipe generates the DCP automatically from the following variables: -- `CONTAINER_STATS_PUBLISH_ENABLED` -- `CONTAINER_STATS_PUBLISH_SAMPLE_INTERVAL` +- `CONTAINER_DRM_ENABLED` +- `CONTAINER_DRM_STATS_SAMPLE_INTERVAL` +- `CONTAINER_DRM_STATS_LIST_OF_METRICS` Example: ```conf -CONTAINER_STATS_PUBLISH_ENABLED = "true" -CONTAINER_STATS_PUBLISH_SAMPLE_INTERVAL = "30" +CONTAINER_DRM_ENABLED = "true" +CONTAINER_DRM_STATS_SAMPLE_INTERVAL = "30" +CONTAINER_DRM_STATS_LIST_OF_METRICS = "[\"cpu\", \"mem\"]" ``` +`CONTAINER_DRM_STATS_LIST_OF_METRICS` follows the same semantics as the target +manager: + +- `["all"]` is accepted as a shorthand input for all periodic metrics. +- `[]` publishes no periodic metrics. +- any other list publishes only the selected metrics. + +Generated manifests and target-side effective configuration use the explicit +metric list. + ## Layer Scope Main recipes: @@ -294,8 +306,9 @@ Relevant variables: - `CONTAINER_PACKAGE_ID` - `CONTAINER_ARTIFACT_VERSION` - `CONTAINER_CREATE_ARGS_PODMAN` -- `CONTAINER_STATS_PUBLISH_ENABLED` -- `CONTAINER_STATS_PUBLISH_SAMPLE_INTERVAL` +- `CONTAINER_DRM_ENABLED` +- `CONTAINER_DRM_STATS_SAMPLE_INTERVAL` +- `CONTAINER_DRM_STATS_LIST_OF_METRICS` - `CONTAINER_FIRMWARE_VERSIONS` - `CONTAINER_DEVICE_TYPES_JSON` - `CONTAINER_ARTIFACT_DESCRIPTION` diff --git a/meta-digi-containers/examples/manifest-lxc.json b/meta-digi-containers/examples/manifest-lxc.json index ab59a5a9e..472b17b60 100644 --- a/meta-digi-containers/examples/manifest-lxc.json +++ b/meta-digi-containers/examples/manifest-lxc.json @@ -5,6 +5,19 @@ "registration_defaults": { "autostart": true, "monitor": true, + "drm": { + "enabled": true, + "stats_sample_interval_s": 30, + "stats_list_of_metrics": [ + "uptime", + "cpu", + "mem", + "net/rx", + "net/tx", + "disk/read", + "disk/write" + ] + }, "restart": { "enabled": true, "max_retries": 3, diff --git a/meta-digi-containers/examples/manifest-podman.json b/meta-digi-containers/examples/manifest-podman.json index 9a8c18fcc..d312c987d 100644 --- a/meta-digi-containers/examples/manifest-podman.json +++ b/meta-digi-containers/examples/manifest-podman.json @@ -6,6 +6,19 @@ "registration_defaults": { "autostart": true, "monitor": true, + "drm": { + "enabled": true, + "stats_sample_interval_s": 30, + "stats_list_of_metrics": [ + "uptime", + "cpu", + "mem", + "net/rx", + "net/tx", + "disk/read", + "disk/write" + ] + }, "restart": { "enabled": true, "max_retries": 3, diff --git a/meta-digi-containers/recipes-core/images/dey-image-container-artifact.inc b/meta-digi-containers/recipes-core/images/dey-image-container-artifact.inc index 81de04ae9..a4f711b13 100644 --- a/meta-digi-containers/recipes-core/images/dey-image-container-artifact.inc +++ b/meta-digi-containers/recipes-core/images/dey-image-container-artifact.inc @@ -53,8 +53,9 @@ do_image_container_artifacts() { CREATE_ARGS="${create_args}" \ AUTOSTART="${CONTAINER_AUTOSTART}" \ MONITOR="${CONTAINER_MONITOR}" \ - STATS_PUBLISH_ENABLED="${CONTAINER_STATS_PUBLISH_ENABLED}" \ - STATS_PUBLISH_SAMPLE_INTERVAL="${CONTAINER_STATS_PUBLISH_SAMPLE_INTERVAL}" \ + DRM_ENABLED="${CONTAINER_DRM_ENABLED}" \ + DRM_STATS_SAMPLE_INTERVAL="${CONTAINER_DRM_STATS_SAMPLE_INTERVAL}" \ + DRM_STATS_LIST_OF_METRICS="${CONTAINER_DRM_STATS_LIST_OF_METRICS}" \ RESTART_ENABLED="${CONTAINER_RESTART_ENABLED}" \ RESTART_MAX_RETRIES="${CONTAINER_RESTART_MAX_RETRIES}" \ RESTART_WINDOW="${CONTAINER_RESTART_WINDOW}" \ @@ -66,7 +67,7 @@ do_image_container_artifacts() { LABELS_JSON="${CONTAINER_ARTIFACT_LABELS_JSON}" \ python3 -c 'import json, os, sys; \ parse_bool = lambda name: os.environ[name].strip().lower() == "true"; \ -payload = {"package_id": os.environ["PACKAGE_ID"], "version": os.environ["VERSION"], "runtime": os.environ["RUNTIME"], "registration_defaults": {"autostart": parse_bool("AUTOSTART"), "monitor": parse_bool("MONITOR"), "stats_publish": {"enabled": parse_bool("STATS_PUBLISH_ENABLED"), "sample_interval_s": int(os.environ["STATS_PUBLISH_SAMPLE_INTERVAL"])}, "restart": {"enabled": parse_bool("RESTART_ENABLED"), "max_retries": int(os.environ["RESTART_MAX_RETRIES"]), "window": int(os.environ["RESTART_WINDOW"]), "retry_delay": int(os.environ["RESTART_RETRY_DELAY"]) } }, "device_types": json.loads(os.environ["DEVICE_TYPES_JSON"]), "firmware_versions": os.environ["FIRMWARE_VERSIONS"], "build_id": os.environ["BUILD_ID"], "description": os.environ["DESCRIPTION"], "labels": json.loads(os.environ["LABELS_JSON"])}; \ +payload = {"package_id": os.environ["PACKAGE_ID"], "version": os.environ["VERSION"], "runtime": os.environ["RUNTIME"], "registration_defaults": {"autostart": parse_bool("AUTOSTART"), "monitor": parse_bool("MONITOR"), "drm": {"enabled": parse_bool("DRM_ENABLED"), "stats_sample_interval_s": int(os.environ["DRM_STATS_SAMPLE_INTERVAL"]), "stats_list_of_metrics": json.loads(os.environ["DRM_STATS_LIST_OF_METRICS"])}, "restart": {"enabled": parse_bool("RESTART_ENABLED"), "max_retries": int(os.environ["RESTART_MAX_RETRIES"]), "window": int(os.environ["RESTART_WINDOW"]), "retry_delay": int(os.environ["RESTART_RETRY_DELAY"]) } }, "device_types": json.loads(os.environ["DEVICE_TYPES_JSON"]), "firmware_versions": os.environ["FIRMWARE_VERSIONS"], "build_id": os.environ["BUILD_ID"], "description": os.environ["DESCRIPTION"], "labels": json.loads(os.environ["LABELS_JSON"])}; \ create_args = os.environ["CREATE_ARGS"].strip(); \ payload.update({"create_args": create_args} if create_args else {}); \ open(sys.argv[1], "w", encoding="utf-8").write(json.dumps(payload, indent=2) + "\n")' \ diff --git a/meta-digi-containers/recipes-core/images/dey-image-container.bb b/meta-digi-containers/recipes-core/images/dey-image-container.bb index 411372725..b330db5c7 100644 --- a/meta-digi-containers/recipes-core/images/dey-image-container.bb +++ b/meta-digi-containers/recipes-core/images/dey-image-container.bb @@ -69,8 +69,9 @@ CONTAINER_ARTIFACT_LABELS_JSON ?= "{}" # monitor and (auto/re)start configuration CONTAINER_AUTOSTART ?= "true" CONTAINER_MONITOR ?= "true" -CONTAINER_STATS_PUBLISH_ENABLED ?= "true" -CONTAINER_STATS_PUBLISH_SAMPLE_INTERVAL ?= "30" +CONTAINER_DRM_ENABLED ?= "true" +CONTAINER_DRM_STATS_SAMPLE_INTERVAL ?= "30" +CONTAINER_DRM_STATS_LIST_OF_METRICS ?= "[\"uptime\", \"cpu\", \"mem\", \"net/rx\", \"net/tx\", \"disk/read\", \"disk/write\"]" CONTAINER_RESTART_ENABLED ?= "true" CONTAINER_RESTART_MAX_RETRIES ?= "3" CONTAINER_RESTART_WINDOW ?= "60" diff --git a/meta-digi-containers/scripts/generate-dcp.py b/meta-digi-containers/scripts/generate-dcp.py index 0341c47c5..6bf277fd6 100755 --- a/meta-digi-containers/scripts/generate-dcp.py +++ b/meta-digi-containers/scripts/generate-dcp.py @@ -16,6 +16,16 @@ import tarfile import tempfile BASE36_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz" +SUPPORTED_STATS_METRICS = ( + "uptime", + "cpu", + "mem", + "net/rx", + "net/tx", + "disk/read", + "disk/write", +) +SUPPORTED_STATS_METRICS_SET = set(SUPPORTED_STATS_METRICS) def fail(message: str) -> "NoReturn": @@ -100,6 +110,34 @@ def validate_labels(value: object) -> dict: return value +def validate_string_list(value: object, *, path: str, allowed: set[str]) -> list[str]: + if value is None: + return [] + if not isinstance(value, list): + fail(f"invalid manifest: {path} must be an array") + result: list[str] = [] + seen: set[str] = set() + for item in value: + if not isinstance(item, str) or not item.strip(): + fail(f"invalid manifest: {path} entries must be non-empty strings") + metric = item.strip() + if metric not in allowed: + fail(f"invalid manifest: {path} contains unsupported metric {metric}") + if metric in seen: + continue + seen.add(metric) + result.append(metric) + if "all" in result and len(result) > 1: + fail(f"invalid manifest: {path} cannot combine all with specific metrics") + return result + + +def normalize_drm_stats_metrics(metrics: list[str]) -> list[str]: + if "all" in metrics: + return list(SUPPORTED_STATS_METRICS) + return metrics + + def validate_manifest(data: dict) -> dict: package_id = validate_string(data, "package_id", path="manifest") version = validate_string(data, "version", path="manifest") @@ -114,6 +152,9 @@ def validate_manifest(data: dict) -> dict: restart = registration_defaults.get("restart") if not isinstance(restart, dict): fail("invalid manifest: registration_defaults.restart must be an object") + drm = registration_defaults.get("drm", {}) + if not isinstance(drm, dict): + fail("invalid manifest: registration_defaults.drm must be an object") create_args = data.get("create_args") if runtime == "podman": @@ -135,7 +176,7 @@ def validate_manifest(data: dict) -> dict: if description is not None and not isinstance(description, str): fail("invalid manifest: description must be a string") - return { + validated = { "package_id": package_id, "version": version, "runtime": runtime, @@ -143,6 +184,20 @@ def validate_manifest(data: dict) -> dict: "registration_defaults": { "autostart": validate_bool(registration_defaults, "autostart", path="registration_defaults"), "monitor": validate_bool(registration_defaults, "monitor", path="registration_defaults"), + "drm": { + "enabled": validate_bool(drm, "enabled", path="registration_defaults.drm"), + "stats_sample_interval_s": validate_int( + drm, + "stats_sample_interval_s", + path="registration_defaults.drm", + minimum=1, + ), + "stats_list_of_metrics": validate_string_list( + drm.get("stats_list_of_metrics"), + path="registration_defaults.drm.stats_list_of_metrics", + allowed=SUPPORTED_STATS_METRICS_SET | {"all"}, + ), + }, "restart": { "enabled": validate_bool(restart, "enabled", path="registration_defaults.restart"), "max_retries": validate_int(restart, "max_retries", path="registration_defaults.restart", minimum=0), @@ -156,6 +211,10 @@ def validate_manifest(data: dict) -> dict: "description": description or "", "labels": validate_labels(data.get("labels")), } + validated["registration_defaults"]["drm"]["stats_list_of_metrics"] = normalize_drm_stats_metrics( + validated["registration_defaults"]["drm"]["stats_list_of_metrics"] + ) + return validated def validate_payload(path: Path, runtime: str) -> None: