Generate unique DCP package ids
https://onedigi.atlassian.net/browse/DEL-10035 https://onedigi.atlassian.net/browse/DEL-10081 Signed-off-by: Isaac Hermida <isaac.hermida@digi.com>
This commit is contained in:
parent
d743784281
commit
69286f4057
|
|
@ -68,8 +68,11 @@ The script generates exactly one runtime artifact per execution.
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- `--output-dir` is optional. If omitted, the script writes the DCP to the current directory.
|
- `--output-dir` is optional. If omitted, the script writes the DCP to the current directory.
|
||||||
- The output file name is always derived from manifest fields as:
|
- The generator appends a unique suffix to the input `package_id` using the
|
||||||
- `<package_id>_artifact_<runtime>_<device_types[0]>.tar.gz`
|
`created_at` timestamp encoded in base36 milliseconds.
|
||||||
|
- The output file name is always derived from the generated package ID and
|
||||||
|
manifest fields as:
|
||||||
|
- `<generated_package_id>_artifact_<runtime>_<device_types[0]>.tar.gz`
|
||||||
- For Podman payloads, the final internal payload name is always `payload/image.tar`
|
- For Podman payloads, the final internal payload name is always `payload/image.tar`
|
||||||
- For LXC payloads, the generator requires a Yocto-style `.tar.gz` bundle, validates its layout,
|
- For LXC payloads, the generator requires a Yocto-style `.tar.gz` bundle, validates its layout,
|
||||||
and stores it inside the DCP using the original file name and format without re-packaging it.
|
and stores it inside the DCP using the original file name and format without re-packaging it.
|
||||||
|
|
@ -87,9 +90,9 @@ python3 meta-digi-containers/scripts/generate-dcp.py \
|
||||||
--payload /tmp/image.tar
|
--payload /tmp/image.tar
|
||||||
```
|
```
|
||||||
|
|
||||||
This generates:
|
This generates a bundle named like:
|
||||||
|
|
||||||
- `./flutter-demo_artifact_podman_ccmp25-dvk.tar.gz`
|
- `./flutter-demo-<base36_created_at_ms>_artifact_podman_ccmp25-dvk.tar.gz`
|
||||||
|
|
||||||
## Digi Remote Manager metrics support
|
## Digi Remote Manager metrics support
|
||||||
|
|
||||||
|
|
@ -97,6 +100,10 @@ This generates:
|
||||||
publish container statistics through the local CCCS Python API.
|
publish container statistics through the local CCCS Python API.
|
||||||
For generated DCPs, per-container DRM sampling is enabled
|
For generated DCPs, per-container DRM sampling is enabled
|
||||||
through `registration_defaults.stats_publish` in the artifact manifest.
|
through `registration_defaults.stats_publish` 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`
|
||||||
|
`get` and `set` operations without regenerating the DCP.
|
||||||
The image recipe generates the DCP automatically from the following variables:
|
The image recipe generates the DCP automatically from the following variables:
|
||||||
|
|
||||||
- `CONTAINER_STATS_PUBLISH_ENABLED`
|
- `CONTAINER_STATS_PUBLISH_ENABLED`
|
||||||
|
|
@ -169,14 +176,16 @@ Outputs are generated in:
|
||||||
|
|
||||||
Final outputs:
|
Final outputs:
|
||||||
|
|
||||||
- `${CONTAINER_PACKAGE_ID}_artifact_podman_<device_types[0]>.tar.gz`
|
- `${CONTAINER_PACKAGE_ID}-<base36_created_at_ms>_artifact_podman_<device_types[0]>.tar.gz`
|
||||||
- `${CONTAINER_PACKAGE_ID}_artifact_lxc_<device_types[0]>.tar.gz`
|
- `${CONTAINER_PACKAGE_ID}-<base36_created_at_ms>_artifact_lxc_<device_types[0]>.tar.gz`
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- The generator script makes the DCP filename using `package_id`, `runtime`, and
|
- The generator script appends a base36-encoded millisecond timestamp suffix to
|
||||||
`device_types[0]` fields from the manifest file.
|
the input `package_id`, stores that generated value in the final
|
||||||
- In Yocto builds, `CONTAINER_PACKAGE_ID` defaults to `${CONTAINER_NAME}`
|
`manifest.json`, and uses it in the DCP file name.
|
||||||
|
- In Yocto builds, `CONTAINER_PACKAGE_ID` defaults to `${CONTAINER_NAME}` before
|
||||||
|
the generator adds the unique suffix.
|
||||||
|
|
||||||
Intermediate outputs generated during the build (LXC bundle, Podman archive, OCI/rootfs files)
|
Intermediate outputs generated during the build (LXC bundle, Podman archive, OCI/rootfs files)
|
||||||
are removed automatically at the end of artifact creation.
|
are removed automatically at the end of artifact creation.
|
||||||
|
|
|
||||||
|
|
@ -109,14 +109,14 @@ open(sys.argv[1], "w", encoding="utf-8").write(json.dumps(payload, indent=2) + "
|
||||||
changelog_arg="--changelog ${template_dir}/metadata/changelog.txt"
|
changelog_arg="--changelog ${template_dir}/metadata/changelog.txt"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 "${generator_script}" \
|
generated_output="$(python3 "${generator_script}" \
|
||||||
--manifest "${manifest_path}" \
|
--manifest "${manifest_path}" \
|
||||||
--payload "${src_payload}" \
|
--payload "${src_payload}" \
|
||||||
--output-dir "${DEPLOY_DIR_IMAGE}" \
|
--output-dir "${DEPLOY_DIR_IMAGE}" \
|
||||||
${readme_arg} \
|
${readme_arg} \
|
||||||
${changelog_arg}
|
${changelog_arg})"
|
||||||
|
|
||||||
generated_output="${DEPLOY_DIR_IMAGE}/${CONTAINER_PACKAGE_ID}_artifact_${runtime}_${primary_device_type}.tar.gz"
|
generated_output="$(printf '%s' "${generated_output}" | tail -n 1)"
|
||||||
if [ ! -s "${generated_output}" ]; then
|
if [ ! -s "${generated_output}" ]; then
|
||||||
bbfatal "Container artifact bundle not generated correctly: ${generated_output}"
|
bbfatal "Container artifact bundle not generated correctly: ${generated_output}"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,33 @@ import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
BASE36_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
|
||||||
def fail(message: str) -> "NoReturn":
|
def fail(message: str) -> "NoReturn":
|
||||||
raise SystemExit(f"error: {message}")
|
raise SystemExit(f"error: {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def to_base36(value: int) -> str:
|
||||||
|
if value < 0:
|
||||||
|
fail("cannot convert negative values to base36")
|
||||||
|
if value == 0:
|
||||||
|
return "0"
|
||||||
|
result: list[str] = []
|
||||||
|
while value:
|
||||||
|
value, remainder = divmod(value, 36)
|
||||||
|
result.append(BASE36_ALPHABET[remainder])
|
||||||
|
return "".join(reversed(result))
|
||||||
|
|
||||||
|
|
||||||
|
def format_created_at(timestamp: datetime) -> str:
|
||||||
|
return timestamp.astimezone(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
||||||
|
|
||||||
|
|
||||||
|
def build_generated_package_id(base_package_id: str, *, created_at_ms: int) -> str:
|
||||||
|
return f"{base_package_id}-{to_base36(created_at_ms)}"
|
||||||
|
|
||||||
|
|
||||||
def load_manifest(path: Path) -> dict:
|
def load_manifest(path: Path) -> dict:
|
||||||
try:
|
try:
|
||||||
data = json.loads(path.read_text(encoding="utf-8"))
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
@ -167,8 +189,8 @@ def sha256(path: Path) -> str:
|
||||||
return digest.hexdigest()
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
def build_output_name(manifest: dict) -> str:
|
def build_output_name(*, package_id: str, runtime: str, device_type: str) -> str:
|
||||||
return f"{manifest['package_id']}_artifact_{manifest['runtime']}_{manifest['device_types'][0]}.tar.gz"
|
return f"{package_id}_artifact_{runtime}_{device_type}.tar.gz"
|
||||||
|
|
||||||
|
|
||||||
def ensure_lxc_layout(payload: Path) -> tuple[str, str]:
|
def ensure_lxc_layout(payload: Path) -> tuple[str, str]:
|
||||||
|
|
@ -188,9 +210,16 @@ def ensure_lxc_layout(payload: Path) -> tuple[str, str]:
|
||||||
return dirs[0].name, dirs[0].name
|
return dirs[0].name, dirs[0].name
|
||||||
|
|
||||||
fail("invalid payload: LXC archive must contain rootfs/ and config")
|
fail("invalid payload: LXC archive must contain rootfs/ and config")
|
||||||
def write_default_readme(path: Path, manifest: dict, *, created_at: str, payload_name: str) -> None:
|
def write_default_readme(
|
||||||
|
path: Path,
|
||||||
|
manifest: dict,
|
||||||
|
*,
|
||||||
|
package_id: str,
|
||||||
|
created_at: str,
|
||||||
|
payload_name: str,
|
||||||
|
) -> None:
|
||||||
content = (
|
content = (
|
||||||
f"Package: {manifest['package_id']}\n"
|
f"Package: {package_id}\n"
|
||||||
f"Version: {manifest['version']}\n"
|
f"Version: {manifest['version']}\n"
|
||||||
f"Runtime: {manifest['runtime']}\n"
|
f"Runtime: {manifest['runtime']}\n"
|
||||||
f"Platform: {manifest['device_types'][0]}\n"
|
f"Platform: {manifest['device_types'][0]}\n"
|
||||||
|
|
@ -218,9 +247,17 @@ def write_manifest(path: Path, manifest: dict) -> None:
|
||||||
path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def build_final_manifest(base: dict, *, artifact_type: str, digest: str, size_bytes: int, created_at: str) -> dict:
|
def build_final_manifest(
|
||||||
|
base: dict,
|
||||||
|
*,
|
||||||
|
package_id: str,
|
||||||
|
artifact_type: str,
|
||||||
|
digest: str,
|
||||||
|
size_bytes: int,
|
||||||
|
created_at: str,
|
||||||
|
) -> dict:
|
||||||
output = {
|
output = {
|
||||||
"package_id": base["package_id"],
|
"package_id": package_id,
|
||||||
"version": base["version"],
|
"version": base["version"],
|
||||||
"runtime": base["runtime"],
|
"runtime": base["runtime"],
|
||||||
"artifact_type": artifact_type,
|
"artifact_type": artifact_type,
|
||||||
|
|
@ -281,9 +318,19 @@ def main() -> int:
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
fail(f"cannot create output directory {output_dir}: {exc}")
|
fail(f"cannot create output directory {output_dir}: {exc}")
|
||||||
|
|
||||||
output_name = build_output_name(manifest)
|
created_at_dt = datetime.now(timezone.utc)
|
||||||
|
created_at_ms = int(created_at_dt.timestamp() * 1000)
|
||||||
|
created_at = format_created_at(created_at_dt)
|
||||||
|
generated_package_id = build_generated_package_id(
|
||||||
|
manifest["package_id"],
|
||||||
|
created_at_ms=created_at_ms,
|
||||||
|
)
|
||||||
|
output_name = build_output_name(
|
||||||
|
package_id=generated_package_id,
|
||||||
|
runtime=manifest["runtime"],
|
||||||
|
device_type=manifest["device_types"][0],
|
||||||
|
)
|
||||||
output_path = output_dir / output_name
|
output_path = output_dir / output_name
|
||||||
created_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix="dcp-bundle-") as tmpdir:
|
with tempfile.TemporaryDirectory(prefix="dcp-bundle-") as tmpdir:
|
||||||
staging_root = Path(tmpdir)
|
staging_root = Path(tmpdir)
|
||||||
|
|
@ -311,6 +358,7 @@ def main() -> int:
|
||||||
|
|
||||||
final_manifest = build_final_manifest(
|
final_manifest = build_final_manifest(
|
||||||
manifest,
|
manifest,
|
||||||
|
package_id=generated_package_id,
|
||||||
artifact_type=artifact_type,
|
artifact_type=artifact_type,
|
||||||
digest=digest,
|
digest=digest,
|
||||||
size_bytes=size_bytes,
|
size_bytes=size_bytes,
|
||||||
|
|
@ -321,7 +369,13 @@ def main() -> int:
|
||||||
stage_metadata(
|
stage_metadata(
|
||||||
readme_path,
|
readme_path,
|
||||||
metadata_dir / "README.txt",
|
metadata_dir / "README.txt",
|
||||||
lambda dst: write_default_readme(dst, manifest, created_at=created_at, payload_name=payload_name),
|
lambda dst: write_default_readme(
|
||||||
|
dst,
|
||||||
|
manifest,
|
||||||
|
package_id=generated_package_id,
|
||||||
|
created_at=created_at,
|
||||||
|
payload_name=payload_name,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
stage_metadata(changelog_path, metadata_dir / "changelog.txt", write_default_changelog)
|
stage_metadata(changelog_path, metadata_dir / "changelog.txt", write_default_changelog)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue