"""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 network_config(nics: list[tuple[str, bool]]) -> dict[str, Any] | None: """Netplan v2 config: DHCP on every ``(mac, dhcp)`` NIC that asks for it. ``None`` when no NIC does -- cloud-init then leaves networking alone rather than being told to configure nothing. """ ethernets = { f"nic{i}": {"match": {"macaddress": mac}, "dhcp4": True} for i, (mac, dhcp) in enumerate(nics) if dhcp } return {"version": 2, "ethernets": ethernets} if ethernets else None 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()