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
|
2024-05-30 17:51:53 +00:00
|
|
|
from .completions import add_dynamic_completer, complete_machines
|
2024-03-07 12:30:53 +00:00
|
|
|
from .errors import ClanError
|
2024-03-23 04:05:31 +00:00
|
|
|
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,
|
2024-05-29 07:08:23 +00:00
|
|
|
extra_args: list[str] = [],
|
2024-03-07 12:30:53 +00:00
|
|
|
) -> None:
|
2024-03-23 04:05:31 +00:00
|
|
|
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)
|
2024-03-23 04:05:31 +00:00
|
|
|
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),
|
|
|
|
]
|
|
|
|
)
|
2024-05-29 07:08:23 +00:00
|
|
|
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
|
2024-05-29 07:08:23 +00:00
|
|
|
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,
|
2024-05-29 09:10:14 +00:00
|
|
|
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,
|
2024-05-29 09:10:14 +00:00
|
|
|
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())
|
2024-04-03 09:51:21 +00:00
|
|
|
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
|
|
|
|
2024-05-29 09:10:14 +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}}}}
|
2024-05-29 09:10:14 +00:00
|
|
|
}
|
|
|
|
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:
|
2024-05-30 17:51:53 +00:00
|
|
|
machines_parser = parser.add_argument(
|
2024-02-07 04:24:43 +00:00
|
|
|
"machine",
|
|
|
|
type=str,
|
|
|
|
help="machine to install",
|
|
|
|
)
|
2024-05-30 17:51:53 +00:00
|
|
|
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={},
|
|
|
|
)
|
2024-05-29 07:08:23 +00:00
|
|
|
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-05-29 07:08:23 +00:00
|
|
|
"""
|
|
|
|
)
|
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(
|
2024-05-29 09:10:14 +00:00
|
|
|
"--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)
|