"""cloud-init's NoCloud seed: user-data, meta-data, network-config on an ISO. NoCloud rather than VMware's guestinfo datasource because it needs nothing in the guest: guestinfo is read through open-vm-tools, which a generic cloud image does not ship -- it is one of the things the user-data installs. """ from __future__ import annotations import io from typing import Any import pycdlib import yaml #: The volume label cloud-init's NoCloud datasource looks for. _LABEL = "cidata" def user_data(cloud_init_config: dict[str, Any], ssh_public_keys: list[str] | None) -> str: """The ``#cloud-config`` document, with ``ssh_public_keys`` merged in.""" config = dict(cloud_init_config) if ssh_public_keys: keys = list(config.get("ssh_authorized_keys") or []) keys += [k for k in ssh_public_keys if k not in keys] config["ssh_authorized_keys"] = keys return "#cloud-config\n" + yaml.safe_dump(config, sort_keys=False) def meta_data(hostname: str, instance_id: str) -> str: return yaml.safe_dump({"instance-id": instance_id, "local-hostname": hostname}, sort_keys=False) def nocloud_iso(user: str, meta: str, network: dict[str, Any] | None) -> bytes: """An ISO 9660 image (Rock Ridge + Joliet) holding the seed files.""" files = {"user-data": user, "meta-data": meta} if network is not None: files["network-config"] = yaml.safe_dump(network, sort_keys=False) iso = pycdlib.PyCdlib() iso.new(interchange_level=3, joliet=3, rock_ridge="1.09", vol_ident=_LABEL) for index, (name, content) in enumerate(files.items()): data = content.encode() iso.add_fp( io.BytesIO(data), len(data), f"/SEED{index}.;1", rr_name=name, joliet_path=f"/{name}", ) out = io.BytesIO() iso.write_fp(out) iso.close() return out.getvalue()