clan-lib: Always set a static private key for nixos-anywhere, to make --phases work properly
This commit is contained in:
@@ -58,6 +58,9 @@ def install_command(args: argparse.Namespace) -> None:
|
|||||||
else:
|
else:
|
||||||
target_host = machine.target_host().override(host_key_check=host_key_check)
|
target_host = machine.target_host().override(host_key_check=host_key_check)
|
||||||
|
|
||||||
|
if args.identity_file:
|
||||||
|
target_host = target_host.override(private_key=args.identity_file)
|
||||||
|
|
||||||
if machine._class_ == "darwin":
|
if machine._class_ == "darwin":
|
||||||
msg = "Installing macOS machines is not yet supported"
|
msg = "Installing macOS machines is not yet supported"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ def user_cache_dir() -> Path:
|
|||||||
return Path("~/.cache").expanduser()
|
return Path("~/.cache").expanduser()
|
||||||
|
|
||||||
|
|
||||||
|
def user_nixos_anywhere_dir() -> Path:
|
||||||
|
p = user_config_dir() / "clan" / "nixos-anywhere"
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
def user_gcroot_dir() -> Path:
|
def user_gcroot_dir() -> Path:
|
||||||
p = user_config_dir() / "clan" / "gcroots"
|
p = user_config_dir() / "clan" / "gcroots"
|
||||||
p.mkdir(parents=True, exist_ok=True)
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from clan_lib.api import API
|
|||||||
from clan_lib.cmd import Log, RunOpts, run
|
from clan_lib.cmd import Log, RunOpts, run
|
||||||
from clan_lib.machines.machines import Machine
|
from clan_lib.machines.machines import Machine
|
||||||
from clan_lib.nix import nix_shell
|
from clan_lib.nix import nix_shell
|
||||||
|
from clan_lib.ssh.create import create_nixos_anywhere_ssh_key
|
||||||
from clan_lib.ssh.remote import Remote
|
from clan_lib.ssh.remote import Remote
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
@@ -25,6 +26,7 @@ BuildOn = Literal["auto", "local", "remote"]
|
|||||||
class InstallOptions:
|
class InstallOptions:
|
||||||
machine: Machine
|
machine: Machine
|
||||||
kexec: str | None = None
|
kexec: str | None = None
|
||||||
|
anywhere_priv_key: Path | None = None
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
no_reboot: bool = False
|
no_reboot: bool = False
|
||||||
phases: str | None = None
|
phases: str | None = None
|
||||||
@@ -115,8 +117,18 @@ def run_machine_install(opts: InstallOptions, target_host: Remote) -> None:
|
|||||||
"IdentitiesOnly=yes",
|
"IdentitiesOnly=yes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Always set a nixos-anywhere private key to prevent failures when running
|
||||||
|
# 'clan install --phases kexec' followed by 'clan install --phases disko,install,reboot'.
|
||||||
|
# The kexec phase requires an authorized key, and if not specified,
|
||||||
|
# nixos-anywhere defaults to a key in a temporary directory.
|
||||||
|
if opts.anywhere_priv_key is None:
|
||||||
|
key_pair = create_nixos_anywhere_ssh_key()
|
||||||
|
opts.anywhere_priv_key = key_pair.private
|
||||||
|
cmd += ["-i", str(opts.anywhere_priv_key)]
|
||||||
|
|
||||||
|
# If we need a different private key for being able to kexec, we can specify it here.
|
||||||
if target_host.private_key:
|
if target_host.private_key:
|
||||||
cmd += ["-i", str(target_host.private_key)]
|
cmd += ["--ssh-option", f"IdentityFile={target_host.private_key}"]
|
||||||
|
|
||||||
if opts.build_on:
|
if opts.build_on:
|
||||||
cmd += ["--build-on", opts.build_on]
|
cmd += ["--build-on", opts.build_on]
|
||||||
|
|||||||
61
pkgs/clan-cli/clan_lib/ssh/create.py
Normal file
61
pkgs/clan-cli/clan_lib/ssh/create.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from clan_lib.api import API
|
||||||
|
from clan_lib.cmd import Log, RunOpts, run
|
||||||
|
from clan_lib.dirs import user_nixos_anywhere_dir
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SSHKeyPair:
|
||||||
|
private: Path
|
||||||
|
public: Path
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def create_nixos_anywhere_ssh_key() -> SSHKeyPair:
|
||||||
|
"""
|
||||||
|
Create a new SSH key pair for NixOS Anywhere.
|
||||||
|
The keys are stored in ~/.config/clan/nixos-anywhere/keys/id_ed25519 and id_ed25519.pub.
|
||||||
|
"""
|
||||||
|
private_key_dir = user_nixos_anywhere_dir()
|
||||||
|
|
||||||
|
key_pair = generate_ssh_key(private_key_dir)
|
||||||
|
|
||||||
|
return key_pair
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ssh_key(root_dir: Path) -> SSHKeyPair:
|
||||||
|
"""
|
||||||
|
Generate a new SSH key pair at root_dir/keys/id_ed25519 and id_ed25519.pub.
|
||||||
|
If the key already exists, it will not be regenerated.
|
||||||
|
"""
|
||||||
|
key_dir = root_dir / "keys"
|
||||||
|
key_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
key_dir.chmod(0o700)
|
||||||
|
priv_key = key_dir / "id_ed25519"
|
||||||
|
|
||||||
|
keypair = SSHKeyPair(
|
||||||
|
private=priv_key,
|
||||||
|
public=key_dir / "id_ed25519.pub",
|
||||||
|
)
|
||||||
|
|
||||||
|
if priv_key.exists():
|
||||||
|
return keypair
|
||||||
|
|
||||||
|
log.info(f"Generating nixos-anywhere SSH key pair at {priv_key}")
|
||||||
|
cmd = [
|
||||||
|
"ssh-keygen",
|
||||||
|
"-N",
|
||||||
|
"",
|
||||||
|
"-t",
|
||||||
|
"ed25519",
|
||||||
|
"-f",
|
||||||
|
str(priv_key),
|
||||||
|
]
|
||||||
|
run(cmd, RunOpts(log=Log.BOTH))
|
||||||
|
|
||||||
|
return keypair
|
||||||
28
pkgs/clan-cli/clan_lib/ssh/create_test.py
Normal file
28
pkgs/clan-cli/clan_lib/ssh/create_test.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from clan_lib.ssh.create import create_nixos_anywhere_ssh_key
|
||||||
|
|
||||||
|
|
||||||
|
def test_clan_generate_sshkeys(temporary_home: Path) -> None:
|
||||||
|
keypair = create_nixos_anywhere_ssh_key()
|
||||||
|
|
||||||
|
assert keypair.private.exists()
|
||||||
|
assert keypair.public.exists()
|
||||||
|
assert keypair.private.is_file()
|
||||||
|
assert keypair.public.is_file()
|
||||||
|
assert (
|
||||||
|
keypair.private.parent
|
||||||
|
== Path("~/.config/clan/nixos-anywhere/keys").expanduser()
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
keypair.public.parent == Path("~/.config/clan/nixos-anywhere/keys").expanduser()
|
||||||
|
)
|
||||||
|
assert keypair.private.name == "id_ed25519"
|
||||||
|
assert keypair.public.name == "id_ed25519.pub"
|
||||||
|
assert "PRIVATE KEY" in keypair.private.read_text()
|
||||||
|
assert "ssh-ed25519" in keypair.public.read_text()
|
||||||
|
|
||||||
|
new_keypair = create_nixos_anywhere_ssh_key()
|
||||||
|
|
||||||
|
assert new_keypair.private == keypair.private
|
||||||
|
assert new_keypair.public == keypair.public
|
||||||
Reference in New Issue
Block a user