clan-core/pkgs/clan-cli/clan_cli/flash.py

250 lines
7.1 KiB
Python
Raw Normal View History

2024-02-07 04:24:43 +00:00
import argparse
import importlib
2024-04-23 16:55:00 +00:00
import json
2024-02-07 04:24:43 +00:00
import logging
2024-03-07 12:30:53 +00:00
import os
import shutil
2024-04-05 13:54:58 +00:00
import textwrap
2024-03-07 12:30:53 +00:00
from collections.abc import Sequence
2024-02-07 04:24:43 +00:00
from dataclasses import dataclass
from pathlib import Path
from tempfile import TemporaryDirectory
2024-03-07 12:30:53 +00:00
from typing import Any
2024-02-07 04:24:43 +00:00
2024-03-07 12:30:53 +00:00
from .cmd import Log, run
from .completions import add_dynamic_completer, complete_machines
2024-03-07 12:30:53 +00:00
from .errors import ClanError
from .facts.secret_modules import SecretStoreBase
2024-02-07 04:24:43 +00:00
from .machines.machines import Machine
2024-03-07 12:30:53 +00:00
from .nix import nix_shell
2024-02-07 04:24:43 +00:00
log = logging.getLogger(__name__)
2024-03-07 12:30:53 +00:00
def flash_machine(
2024-04-23 16:55:00 +00:00
machine: Machine,
*,
mode: str,
disks: dict[str, str],
system_config: dict[str, Any],
dry_run: bool,
2024-05-29 07:18:05 +00:00
write_efi_boot_entries: bool,
2024-04-23 16:55:00 +00:00
debug: bool,
extra_args: list[str] = [],
2024-03-07 12:30:53 +00:00
) -> None:
secret_facts_module = importlib.import_module(machine.secret_facts_module)
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
machine=machine
)
2024-02-07 04:24:43 +00:00
with TemporaryDirectory() as tmpdir_:
tmpdir = Path(tmpdir_)
2024-03-07 12:30:53 +00:00
upload_dir = machine.secrets_upload_directory
if upload_dir.startswith("/"):
local_dir = tmpdir / upload_dir[1:]
else:
local_dir = tmpdir / upload_dir
local_dir.mkdir(parents=True)
secret_facts_store.upload(local_dir)
2024-03-07 12:30:53 +00:00
disko_install = []
2024-02-07 04:24:43 +00:00
2024-03-07 12:30:53 +00:00
if os.geteuid() != 0:
if shutil.which("sudo") is None:
raise ClanError(
"sudo is required to run disko-install as a non-root user"
)
disko_install.append("sudo")
2024-02-07 04:24:43 +00:00
2024-03-07 12:30:53 +00:00
disko_install.append("disko-install")
2024-05-29 07:18:05 +00:00
if write_efi_boot_entries:
disko_install.append("--write-efi-boot-entries")
2024-03-07 12:30:53 +00:00
if dry_run:
disko_install.append("--dry-run")
if debug:
disko_install.append("--debug")
for name, device in disks.items():
disko_install.extend(["--disk", name, device])
disko_install.extend(["--extra-files", str(local_dir), upload_dir])
disko_install.extend(["--flake", str(machine.flake) + "#" + machine.name])
2024-04-05 13:54:58 +00:00
disko_install.extend(["--mode", str(mode)])
2024-04-23 16:55:00 +00:00
disko_install.extend(
[
"--system-config",
json.dumps(system_config),
]
)
disko_install.extend(["--option", "dry-run", "true"])
disko_install.extend(extra_args)
2024-03-07 12:30:53 +00:00
cmd = nix_shell(
2024-05-16 13:08:24 +00:00
["nixpkgs#disko"],
2024-03-07 12:30:53 +00:00
disko_install,
)
run(cmd, log=Log.BOTH, error_msg=f"Failed to flash {machine}")
2024-02-07 04:24:43 +00:00
@dataclass
class FlashOptions:
flake: Path
machine: str
2024-03-07 12:30:53 +00:00
disks: dict[str, str]
2024-04-23 16:55:00 +00:00
ssh_keys_path: list[Path]
2024-03-07 12:30:53 +00:00
dry_run: bool
confirm: bool
debug: bool
2024-04-05 13:54:58 +00:00
mode: str
2024-04-23 22:16:50 +00:00
language: str
keymap: str
write_efi_boot_entries: bool
nix_options: list[str]
2024-03-07 12:30:53 +00:00
class AppendDiskAction(argparse.Action):
def __init__(self, option_strings: str, dest: str, **kwargs: Any) -> None:
super().__init__(option_strings, dest, **kwargs)
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: str | Sequence[str] | None,
option_string: str | None = None,
) -> None:
disks = getattr(namespace, self.dest)
assert isinstance(values, list), "values must be a list"
disks[values[0]] = values[1]
2024-02-07 04:24:43 +00:00
def flash_command(args: argparse.Namespace) -> None:
opts = FlashOptions(
flake=args.flake,
machine=args.machine,
2024-03-07 12:30:53 +00:00
disks=args.disk,
2024-04-23 16:55:00 +00:00
ssh_keys_path=args.ssh_pubkey,
2024-03-07 12:30:53 +00:00
dry_run=args.dry_run,
confirm=not args.yes,
debug=args.debug,
2024-04-05 13:54:58 +00:00
mode=args.mode,
language=args.language,
2024-04-23 22:16:50 +00:00
keymap=args.keymap,
2024-05-29 07:18:05 +00:00
write_efi_boot_entries=args.write_efi_boot_entries,
nix_options=args.option,
2024-02-07 04:24:43 +00:00
)
2024-04-23 16:55:00 +00:00
2024-02-07 04:24:43 +00:00
machine = Machine(opts.machine, flake=opts.flake)
2024-03-07 12:30:53 +00:00
if opts.confirm and not opts.dry_run:
disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items())
msg = f"Install {machine.name}"
if disk_str != "":
msg += f" to {disk_str}"
msg += "? [y/N] "
ask = input(msg)
2024-03-07 12:30:53 +00:00
if ask != "y":
return
2024-04-23 16:55:00 +00:00
extra_config: dict[str, Any] = {}
if opts.ssh_keys_path:
root_keys = []
for key_path in opts.ssh_keys_path:
try:
root_keys.append(key_path.read_text())
except OSError as e:
raise ClanError(f"Cannot read SSH public key file: {key_path}: {e}")
extra_config["users"] = {
2024-04-23 16:55:00 +00:00
"users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}}
}
if opts.keymap:
extra_config["console"] = {"keyMap": opts.keymap}
if opts.language:
extra_config["i18n"] = {"defaultLocale": opts.language}
2024-04-23 16:55:00 +00:00
2024-04-05 13:54:58 +00:00
flash_machine(
2024-04-23 16:55:00 +00:00
machine,
mode=opts.mode,
disks=opts.disks,
2024-04-23 22:16:50 +00:00
system_config=extra_config,
2024-04-23 16:55:00 +00:00
dry_run=opts.dry_run,
debug=opts.debug,
2024-05-29 07:18:05 +00:00
write_efi_boot_entries=opts.write_efi_boot_entries,
extra_args=opts.nix_options,
2024-04-05 13:54:58 +00:00
)
2024-02-07 04:24:43 +00:00
def register_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
2024-02-07 04:24:43 +00:00
"machine",
type=str,
help="machine to install",
)
add_dynamic_completer(machines_parser, complete_machines)
2024-02-07 04:24:43 +00:00
parser.add_argument(
2024-03-07 12:30:53 +00:00
"--disk",
2024-02-07 04:24:43 +00:00
type=str,
2024-03-07 12:30:53 +00:00
nargs=2,
metavar=("name", "value"),
action=AppendDiskAction,
help="device to flash to",
default={},
)
mode_help = textwrap.dedent(
"""\
2024-04-05 13:54:58 +00:00
Specify the mode of operation. Valid modes are: format, mount."
Format will format the disk before installing.
Mount will mount the disk before installing.
Mount is useful for updating an existing system without losing data.
"""
)
2024-04-05 13:54:58 +00:00
parser.add_argument(
"--mode",
type=str,
help=mode_help,
choices=["format", "mount"],
default="format",
)
2024-04-23 16:55:00 +00:00
parser.add_argument(
"--ssh-pubkey",
type=Path,
action="append",
default=[],
help="ssh pubkey file to add to the root user. Can be used multiple times",
)
2024-04-23 22:16:50 +00:00
parser.add_argument(
"--language",
2024-04-23 22:16:50 +00:00
type=str,
help="system language",
)
parser.add_argument(
"--keymap",
type=str,
help="system keymap",
)
2024-03-07 12:30:53 +00:00
parser.add_argument(
"--yes",
action="store_true",
help="do not ask for confirmation",
default=False,
)
parser.add_argument(
"--dry-run",
help="Only build the system, don't flash it",
default=False,
action="store_true",
)
2024-05-29 07:18:05 +00:00
parser.add_argument(
"--write-efi-boot-entries",
help=textwrap.dedent(
"""
Write EFI boot entries to the NVRAM of the system for the installed system.
Specify this option if you plan to boot from this disk on the current machine,
but not if you plan to move the disk to another machine.
"""
).strip(),
default=False,
action="store_true",
)
2024-02-07 04:24:43 +00:00
parser.set_defaults(func=flash_command)