machines update: support local build

Now the user can pass `--build-host local`, to select the local machine as a build host, in which case no ssh is used.

This means the admin machine does not necessarily have ssh set up to itself, which was confusing for many users.

Also this makes it easier to re-use a well configured nix remote build setup which is only available on the local machine. Eg if `--build-host local` nix' defaults for remote builds on that machine will be utilized.
This commit is contained in:
DavHau
2025-07-29 18:44:25 +07:00
committed by Jörg Thalheim
parent b74193514d
commit af7ce9b8ed
3 changed files with 121 additions and 34 deletions

View File

@@ -35,6 +35,13 @@
services.openssh.enable = true; services.openssh.enable = true;
services.openssh.settings.PasswordAuthentication = false; services.openssh.settings.PasswordAuthentication = false;
users.users.root.openssh.authorizedKeys.keys = [ (builtins.readFile ../assets/ssh/pubkey) ]; users.users.root.openssh.authorizedKeys.keys = [ (builtins.readFile ../assets/ssh/pubkey) ];
services.openssh.knownHosts.localhost.publicKeyFile = ../assets/ssh/pubkey;
services.openssh.hostKeys = [
{
path = ../assets/ssh/privkey;
type = "ed25519";
}
];
security.sudo.wheelNeedsPassword = false; security.sudo.wheelNeedsPassword = false;
boot.consoleLogLevel = lib.mkForce 100; boot.consoleLogLevel = lib.mkForce 100;
@@ -99,7 +106,8 @@
let let
closureInfo = pkgs.closureInfo { closureInfo = pkgs.closureInfo {
rootPaths = [ rootPaths = [
self.checks.x86_64-linux.clan-core-for-checks self.packages.${pkgs.system}.clan-cli
self.checks.${pkgs.system}.clan-core-for-checks
self.clanInternals.machines.${pkgs.hostPlatform.system}.test-update-machine.config.system.build.toplevel self.clanInternals.machines.${pkgs.hostPlatform.system}.test-update-machine.config.system.build.toplevel
pkgs.stdenv.drvPath pkgs.stdenv.drvPath
pkgs.bash.drvPath pkgs.bash.drvPath
@@ -151,14 +159,6 @@
machine_config_path = os.path.join(flake_dir, "machines", "test-update-machine", "configuration.nix") machine_config_path = os.path.join(flake_dir, "machines", "test-update-machine", "configuration.nix")
os.makedirs(os.path.dirname(machine_config_path), exist_ok=True) os.makedirs(os.path.dirname(machine_config_path), exist_ok=True)
with open(machine_config_path, "w") as f:
f.write("""
{
environment.etc."update-successful".text = "ok";
}
""")
# Run clan update command
# Note: update command doesn't accept -i flag, SSH key must be in ssh-agent # Note: update command doesn't accept -i flag, SSH key must be in ssh-agent
# Start ssh-agent and add the key # Start ssh-agent and add the key
agent_output = subprocess.check_output(["${pkgs.openssh}/bin/ssh-agent", "-s"], text=True) agent_output = subprocess.check_output(["${pkgs.openssh}/bin/ssh-agent", "-s"], text=True)
@@ -172,6 +172,90 @@
subprocess.run(["${pkgs.openssh}/bin/ssh-add", ssh_conn.ssh_key], check=True) subprocess.run(["${pkgs.openssh}/bin/ssh-add", ssh_conn.ssh_key], check=True)
##############
print("TEST: update with --build-host local")
with open(machine_config_path, "w") as f:
f.write("""
{
environment.etc."update-build-local-successful".text = "ok";
}
""")
# rsync the flake into the container
os.environ["PATH"] = f"{os.environ['PATH']}:${pkgs.openssh}/bin"
subprocess.run(
[
"${pkgs.rsync}/bin/rsync",
"-a",
"--delete",
"-e",
"ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no",
f"{str(flake_dir)}/",
f"root@192.168.1.1:/flake",
],
check=True
)
# allow machine to ssh into itself
subprocess.run([
"ssh",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
f"root@192.168.1.1",
"mkdir -p /root/.ssh && chmod 700 /root/.ssh && echo \"$(cat \"${../assets/ssh/privkey}\")\" > /root/.ssh/id_ed25519 && chmod 600 /root/.ssh/id_ed25519",
], check=True)
# install the clan-cli package into the container's Nix store
subprocess.run(
[
"${pkgs.nix}/bin/nix",
"copy",
"--to",
"ssh://root@192.168.1.1",
"--no-check-sigs",
f"${self.packages.${pkgs.system}.clan-cli}",
"--extra-experimental-features", "nix-command flakes",
"--from", f"{os.environ["TMPDIR"]}/store"
],
check=True,
env={
**os.environ,
"NIX_SSHOPTS": "-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no",
},
)
# Run ssh on the host to run the clan update command via --build-host local
subprocess.run([
"ssh",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "StrictHostKeyChecking=no",
f"root@192.168.1.1",
"${self.packages.${pkgs.system}.clan-cli}/bin/clan",
"machines",
"update",
"--debug",
"--flake", "/flake",
"--host-key-check", "none",
"--fetch-local", # Use local store instead of fetching from network
"--build-host", "local",
"test-update-machine",
"--target-host", f"root@localhost",
], check=True)
# Verify the update was successful
machine.succeed("test -f /etc/update-build-local-successful")
##############
print("TEST: update with --fetch-local")
with open(machine_config_path, "w") as f:
f.write("""
{
environment.etc."update-fetch-local-successful".text = "ok";
}
""")
# Run clan update command # Run clan update command
subprocess.run([ subprocess.run([
"${self.packages.${pkgs.system}.clan-cli-full}/bin/clan", "${self.packages.${pkgs.system}.clan-cli-full}/bin/clan",
@@ -186,10 +270,12 @@
], check=True) ], check=True)
# Verify the update was successful # Verify the update was successful
machine.succeed("test -f /etc/update-successful") machine.succeed("test -f /etc/update-fetch-local-successful")
# Test update with --build-host
# Update configuration again to test build-host functionality ##############
print("TEST: update with --build-host 192.168.1.1")
# Update configuration again
with open(machine_config_path, "w") as f: with open(machine_config_path, "w") as f:
f.write(""" f.write("""
{ {
@@ -213,23 +299,6 @@
# Verify the second update was successful # Verify the second update was successful
machine.succeed("test -f /etc/build-host-update-successful") machine.succeed("test -f /etc/build-host-update-successful")
# Run clan update command with --build-host
subprocess.run([
"${self.packages.${pkgs.system}.clan-cli-full}/bin/clan",
"machines",
"update",
"--debug",
"--flake", flake_dir,
"--host-key-check", "none",
"--fetch-local", # Use local store instead of fetching from network
"--build-host", f"root@192.168.1.1:{ssh_conn.host_port}",
"test-update-machine",
"--target-host", f"root@192.168.1.1:{ssh_conn.host_port}",
], check=True)
# Verify the second update was successful
machine.succeed("test -f /etc/build-host-update-successful")
''; '';
} { inherit pkgs self; }; } { inherit pkgs self; };
}; };

View File

@@ -13,7 +13,9 @@ from clan_lib.machines.machines import Machine
from clan_lib.machines.suggestions import validate_machine_names from clan_lib.machines.suggestions import validate_machine_names
from clan_lib.machines.update import run_machine_update from clan_lib.machines.update import run_machine_update
from clan_lib.nix import nix_config from clan_lib.nix import nix_config
from clan_lib.ssh.host import Host
from clan_lib.ssh.host_key import HostKeyCheck from clan_lib.ssh.host_key import HostKeyCheck
from clan_lib.ssh.localhost import LocalHost
from clan_lib.ssh.remote import Remote from clan_lib.ssh.remote import Remote
from clan_cli.completions import ( from clan_cli.completions import (
@@ -128,6 +130,19 @@ def update_command(args: argparse.Namespace) -> None:
host_key_check = args.host_key_check host_key_check = args.host_key_check
with AsyncRuntime() as runtime: with AsyncRuntime() as runtime:
for machine in machines_to_update: for machine in machines_to_update:
# figure out on which machine to build on
build_host: Host | None = None
if args.build_host:
if args.build_host == "local":
build_host = LocalHost()
else:
build_host = Remote.from_ssh_uri(
machine_name=machine.name,
address=args.build_host,
).override(host_key_check=host_key_check)
else:
build_host = machine.build_host()
# Figure out the target host
if args.target_host: if args.target_host:
target_host = Remote.from_ssh_uri( target_host = Remote.from_ssh_uri(
machine_name=machine.name, machine_name=machine.name,
@@ -137,6 +152,7 @@ def update_command(args: argparse.Namespace) -> None:
target_host = machine.target_host().override( target_host = machine.target_host().override(
host_key_check=host_key_check host_key_check=host_key_check
) )
# run the update
runtime.async_run( runtime.async_run(
AsyncOpts( AsyncOpts(
tid=machine.name, tid=machine.name,
@@ -145,7 +161,7 @@ def update_command(args: argparse.Namespace) -> None:
run_machine_update, run_machine_update,
machine=machine, machine=machine,
target_host=target_host, target_host=target_host,
build_host=machine.build_host(), build_host=build_host,
force_fetch_local=args.fetch_local, force_fetch_local=args.fetch_local,
) )
runtime.join_all() runtime.join_all()
@@ -189,7 +205,10 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
"--build-host", "--build-host",
type=str, type=str,
help="Address of the machine to build the flake, in the format of user@host:1234.", help=(
"The machine on which to build the machine configuration.\n"
"Pass 'local' to build on the local machine, or an ssh address like user@host:1234\n"
),
) )
parser.add_argument( parser.add_argument(
"--fetch-local", "--fetch-local",

View File

@@ -18,7 +18,6 @@ from clan_lib.errors import ClanError
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_command, nix_metadata from clan_lib.nix import nix_command, nix_metadata
from clan_lib.ssh.host import Host from clan_lib.ssh.host import Host
from clan_lib.ssh.remote import Remote
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -133,7 +132,7 @@ def run_machine_update(
if build_host is None: if build_host is None:
build_host = target_host build_host = target_host
else: else:
stack.enter_context(build_host.host_connection()) build_host = stack.enter_context(build_host.host_connection())
# Some operations require root privileges on the target host. # Some operations require root privileges on the target host.
target_host_root = stack.enter_context(target_host.become_root()) target_host_root = stack.enter_context(target_host.become_root())