"""
## Safe - Fedora-CoreOS on Raspberry PI
### config
- safe_dns_names: defaults to ["*.safe" for each authority.ca_config["ca_permitted_domains_list"]]
- identifiers["safe"]["storage"]: production storage device serial numbers
- tang_url
- safe_showcase_compose: *true, if false, dont include compose showcase
- safe_showcaae_nspawn: *true, if false, dont include nspawn showcase
- safe_showcase_unittest: *false, if true, dont spawn vm but finish with vm config
### host
- host_environment
- host_config
- host_machine
- host_update
### provider
- postgresql.Provider: pg_server
"""
import os
import sys
import pulumi
import pulumi_postgresql as postgresql
import pulumi_random
import yaml
from infra.authority import (
config,
stack_name,
ca_config,
create_client_cert,
create_host_cert,
exported_ca_cert,
ssh_factory,
)
from infra.os import (
ButaneTranspiler,
SystemConfigUpdate,
LibvirtIgniteFcos,
TangFingerprint,
FcosImageDownloader,
RemoteDownloadIgnitionConfig,
)
from infra.tools import (
ServePrepare,
ServeOnce,
WaitForHostReady,
write_removable,
public_local_export,
)
from infra.os import build_raspberry_extras
this_dir = os.path.dirname(os.path.normpath(__file__))
files_basedir = os.path.join(this_dir)
# configure hostnames
shortname = "safe"
dns_names = config.get_object("{}_dns_names".format(shortname)) or [
"{}.{}".format(name, domain)
for name in [shortname, "*." + shortname]
for domain in ca_config["ca_permitted_domains_list"]
]
hostname = dns_names[0]
# create tls host certificate
tls = create_host_cert(hostname, hostname, dns_names)
# get tang config for storage unlock on boot
tang_url = config.get("tang_url")
tang_fingerprint = TangFingerprint(tang_url).result if tang_url else None
# create local postgres master password
pg_postgres_password = pulumi_random.RandomPassword(
"{}_POSTGRES_PASSWORD".format(shortname), special=False, length=24
)
# create a postgres master client_cert
pg_postgres_client_cert = create_client_cert(
"postgres@{}_POSTGRESQL_CLIENTCERT".format(shortname),
"postgres@{}".format(hostname),
dns_names=["postgres@{}".format(name) for name in dns_names],
)
pulumi.export(f"{hostname}_postgres_client_cert", pg_postgres_client_cert)
# jinja environment for butane config
host_environment = {
# install mc on sim, prod should use toolbox
"RPM_OSTREE_INSTALL": ["mc", "strace"] if stack_name.endswith("sim") else [],
"SHOWCASE_COMPOSE": config.get(shortname + "_showcase_compose")
in (None, True, "true", "True"),
"SHOWCASE_NSPAWN": config.get(shortname + "_showcase_nspawn")
in (None, True, "true", "True"),
"SHOWCASE_UNITTEST": config.get(shortname + "_showcase_unittest")
in (True, "true", "True"),
"AUTHORIZED_KEYS": ssh_factory.authorized_keys,
"POSTGRESQL_PASSWORD": pg_postgres_password.result,
"DNS_RESOLVER": {}
if not config.get_object("dns_resolver")
else {key.upper(): value for key, value in config.get_object("dns_resolver").items()},
"LOCAL_DNS_SERVER": {"ENABLED": True},
"LOCAL_ACME_SERVER": {"ENABLED": True},
"FRONTEND": {
# enable debug dashboard
"DASHBOARD": "traefik.{}".format(hostname),
# enable tls for tang at port 9443
"PUBLISHPORTS": ["9443:9443"],
"ENTRYPOINTS": {
"tang-mtls-nosni": {"address": ":9443"},
"internal-tang-http": {"address": "localhost:8081"},
},
"EXTRA": 'accessLog:\n format: "common"',
},
}
# modify environment config depending stack for storage and credentials
if stack_name.endswith("sim"):
# for simulation: add qemu-guest-agent, debug=True, and 1234 as disk passphrase
host_environment["RPM_OSTREE_INSTALL"].append("qemu-guest-agent")
host_environment.update({"DEBUG_CONSOLE_AUTOLOGIN": True})
luks_root_passphrase = pulumi.Output.concat("1234")
luks_var_passphrase = pulumi.Output.concat("1234")
identifiers = yaml.safe_load(
"""
storage:
- name: boot
device: /dev/vda
size: {size_8g}
- name: usb1
device: /dev/vdb
size: {size_8g}
- name: usb2
device: /dev/vdc
size: {size_8g}
""".format(size_8g=8 * pow(2, 30))
)
else:
# for production: generate strong random passwords, get storage identifiers from config
luks_root_passphrase = pulumi_random.RandomPassword(
"{}_luks_root_passphrase".format(shortname), special=False, length=24
).result
luks_var_passphrase = pulumi_random.RandomPassword(
"{}_luks_var_passphrase".format(shortname), special=False, length=24
).result
identifiers = config.get_object("identifiers")[shortname]
# update environment to include storage id's, passphrases and tang setup
host_environment.update(
{
"boot_device": next(
s["device"] for s in identifiers["storage"] if s["name"] == "boot"
),
"usb1_device": next(
s["device"] for s in identifiers["storage"] if s["name"] == "usb1"
),
"usb2_device": next(
s["device"] for s in identifiers["storage"] if s["name"] == "usb2"
),
"luks_root_passphrase": luks_root_passphrase,
"luks_var_passphrase": luks_var_passphrase,
"tang_url": tang_url,
"tang_fingerprint": tang_fingerprint,
}
)
# write the butane target specification, everything else is included from files_basedir/*.bu
butane_yaml = pulumi.Output.from_input(
"""
variant: fcos
version: 1.6.0
"""
)
# translate butane into ignition and saltstack
host_config = ButaneTranspiler(
shortname, hostname, tls, butane_yaml, files_basedir, host_environment
)
pulumi.export("{}_butane".format(shortname), host_config)
# configure the later used remote url for remote controlled setup with encrypted config
serve_config = ServePrepare(
shortname,
serve_interface="virbr0" if stack_name.endswith("sim") else "",
request_header={
"Verification-Hash": host_config.ignition_config_hash,
},
)
# create public ignition config pointing to https retrieval of host_config served by ServeOnce
public_config = RemoteDownloadIgnitionConfig(
"{}_public_ignition".format(shortname),
hostname,
remote_url=serve_config.config.apply(lambda x: x["remote_url"]),
remote_hash=host_config.ignition_config_hash,
opts=pulumi.ResourceOptions(ignore_changes=["stdin"]),
)
# only simulate SystemConfigUpdate, skip rest if unittest
if host_environment["SHOWCASE_UNITTEST"]:
host_update = SystemConfigUpdate(
shortname,
hostname,
host_config,
simulate=True,
)
pulumi.export("{}_host_update".format(shortname), host_update)
else:
# serve secret part of ignition config via ServeOnce
serve_data = ServeOnce(
shortname,
config=serve_config.config,
payload=host_config.result,
opts=pulumi.ResourceOptions(ignore_changes=["stdin"]),
)
pulumi.export("{}_served_once".format(shortname), serve_data)
if stack_name.endswith("sim"):
# create libvirt machine simulation:
# download suitable image, create similar virtual machine, same memsize, different arch
host_machine = LibvirtIgniteFcos(
shortname,
public_config.result,
volumes=identifiers["storage"],
memory=4096,
)
# write out ip of simulated host as target
target = host_machine.vm.network_interfaces[0]["addresses"][0]
opts = pulumi.ResourceOptions(depends_on=[host_machine, serve_data])
else:
# download metal version of ARM64 os image (Raspberry PI compatible)
image = FcosImageDownloader(
architecture="aarch64", platform="metal", image_format="raw.xz"
)
# download bios and other extras for Raspberry PI for customization
extras = build_raspberry_extras()
uboot_image_filename = os.path.join(
extras.config["grains"]["tmp_dir"], "uboot/boot/efi/u-boot.bin"
)
# export public config to be copied to the removable storage device
public_config_file = public_local_export(
shortname, "{}_public.ign".format(shortname), public_config.result
)
# write customized image to removable storage device, include uboot image and ignition config
host_boot_media = write_removable(
shortname,
image=image.imagepath,
serial=host_environment["boot_device"].strip("/dev/disk/by-uuid/"),
patches=[
(uboot_image_filename, "EFI-SYSTEM/boot/efi/u-boot.bin"),
(public_config_file.filename, "boot/ignite.json"),
],
)
# target is metal, write out real dns name
target = hostname
opts = pulumi.ResourceOptions(depends_on=[host_boot_media, serve_data])
# wait until host is ready
host_ready = WaitForHostReady(
shortname,
target,
user=host_config.this_env.apply(lambda env: env["UPDATE_USER"]),
private_key=ssh_factory.provision_key.private_key_openssh,
# isready checks for running unbound, which means after rpm-ostree has run and the machine rebooted and started
isready_cmd="/usr/sbin/systemctl is-active unbound",
opts=opts,
)
# update host to newest config, should be a no-op (zero changes) on machine creation
host_update = SystemConfigUpdate(
shortname,
target,
host_config,
simulate=False,
opts=pulumi.ResourceOptions(depends_on=[host_ready]),
)
pulumi.export("{}_host_update".format(shortname), host_update)
# make host postgresql.Provider pg_server available
# print(f"exported_ca_cert.filename: {exported_ca_cert.filename}", file=sys.stderr)
pg_server = postgresql.Provider(
"{}_POSTGRESQL_HOST".format(shortname),
host=hostname,
username="postgres",
password=pg_postgres_password.result,
# XXX currently either mtls or password can be configured and used, therefore password is activated.
# XXX parsing the sslrootcert in this provider from file currently doesnt work if client cert is activated (for whatever reason)
# clientcert=postgresql.ProviderClientcertArgs(
# cert=pg_postgres_client_cert.chain,
# key=pg_postgres_client_cert.key.private_key_pem,
# sslinline=True,
# ),
superuser=True,
sslrootcert=exported_ca_cert.filename,
sslmode="verify-ca",
opts=pulumi.ResourceOptions(depends_on=[host_update]),
)
pulumi.export("{}_pg_server".format(shortname), pg_server)