Merge branch 'main' into machine-id-option

This commit is contained in:
pinpox
2025-06-24 09:41:42 +00:00
17 changed files with 523 additions and 333 deletions

View File

@@ -28,10 +28,8 @@
"@storybook/addon-a11y": "^9.0.8",
"@storybook/addon-docs": "^9.0.8",
"@storybook/addon-links": "^9.0.8",
"@storybook/addon-onboarding": "^9.0.8",
"@storybook/addon-viewport": "^9.0.8",
"@storybook/addon-vitest": "^9.0.8",
"@tailwindcss/typography": "^0.5.13",
"@types/node": "^22.15.19",
"@types/three": "^0.176.0",
"@typescript-eslint/parser": "^8.32.1",
@@ -68,7 +66,6 @@
"@modular-forms/solid": "^0.25.1",
"@solid-primitives/storage": "^4.3.2",
"@solidjs/router": "^0.15.3",
"@solidjs/testing-library": "^0.8.10",
"@tanstack/eslint-plugin-query": "^5.51.12",
"@tanstack/solid-query": "^5.76.0",
"solid-js": "^1.9.7",

View File

@@ -1,4 +1,4 @@
span.tag-status {
span.machine-status {
@apply flex items-center gap-1;
.indicator {

View File

@@ -1,10 +1,10 @@
import {
MachineStatus,
TagStatusProps,
MachineStatusProps,
} from "@/src/components/v2/MachineStatus/MachineStatus";
import { Meta, StoryObj } from "@kachurun/storybook-solid";
const meta: Meta<TagStatusProps> = {
const meta: Meta<MachineStatusProps> = {
title: "Components/MachineStatus",
component: MachineStatus,
decorators: [
@@ -18,7 +18,7 @@ const meta: Meta<TagStatusProps> = {
export default meta;
type Story = StoryObj<TagStatusProps>;
type Story = StoryObj<MachineStatusProps>;
export const Online: Story = {
args: {

View File

@@ -12,14 +12,14 @@ export type MachineStatus =
| "Installed"
| "Not Installed";
export interface TagStatusProps {
export interface MachineStatusProps {
label?: boolean;
status: MachineStatus;
}
export const MachineStatus = (props: TagStatusProps) => (
export const MachineStatus = (props: MachineStatusProps) => (
<Badge
class={cx("tag-status", {
class={cx("machine-status", {
online: props.status == "Online",
offline: props.status == "Offline",
installed: props.status == "Installed",

View File

@@ -0,0 +1,37 @@
span.tag {
@apply flex items-center gap-1 w-fit px-2 py-1 rounded-full;
@apply bg-def-4;
&:focus-visible {
@apply bg-def-acc-3 outline-none;
box-shadow:
0 0 0 0.0625rem theme(colors.off.white),
0 0 0 0.125rem theme(colors.border.semantic.info.1);
}
&.active {
@apply bg-def-acc-4;
}
&.inverted {
@apply bg-inv-1;
}
&.has-action {
@apply pr-1.5;
&:hover {
@apply bg-def-acc-3;
}
&.inverted:hover {
@apply bg-inv-acc-3;
}
}
& > .icon {
&:hover {
@apply cursor-pointer;
}
}
}

View File

@@ -0,0 +1,47 @@
import { Tag, TagProps } from "@/src/components/v2/Tag/Tag";
import { Meta, type StoryContext, StoryObj } from "@kachurun/storybook-solid";
import { expect, fn } from "storybook/test";
const meta: Meta<TagProps> = {
title: "Components/Tag",
component: Tag,
};
export default meta;
type Story = StoryObj<TagProps>;
export const Default: Story = {
args: {
label: "Label",
},
};
export const WithAction: Story = {
args: {
...Default.args,
action: {
icon: "Close",
onClick: fn(),
},
},
play: async ({ canvas, step, userEvent, args }: StoryContext) => {
await userEvent.click(canvas.getByRole("button"));
await expect(args.action.onClick).toHaveBeenCalled();
},
};
export const Inverted: Story = {
args: {
label: "Label",
inverted: true,
},
};
export const InvertedWithAction: Story = {
args: {
...WithAction.args,
inverted: true,
},
play: WithAction.play,
};

View File

@@ -0,0 +1,54 @@
import "./Tag.css";
import cx from "classnames";
import { Typography } from "@/src/components/v2/Typography/Typography";
import { createSignal, Show } from "solid-js";
import Icon, { IconVariant } from "../Icon/Icon";
export interface TagAction {
icon: IconVariant;
onClick: () => void;
}
export interface TagProps {
label: string;
action?: TagAction;
inverted?: boolean;
}
export const Tag = (props: TagProps) => {
const inverted = () => props.inverted || false;
const [isActive, setIsActive] = createSignal(false);
const handleActionClick = () => {
setIsActive(true);
props.action?.onClick();
setTimeout(() => setIsActive(false), 150);
};
return (
<span
class={cx("tag", {
inverted: inverted(),
active: isActive(),
"has-action": props.action,
})}
aria-label={props.label}
aria-readonly={!props.action}
>
<Typography hierarchy="label" size="xs" inverted={inverted()}>
{props.label}
</Typography>
<Show when={props.action}>
<Icon
role="button"
icon={props.action!.icon}
size="0.5rem"
inverted={inverted()}
onClick={handleActionClick}
/>
</Show>
</span>
);
};

View File

@@ -0,0 +1,3 @@
div.tag-group {
@apply flex flex-wrap gap-x-1.5 gap-y-2;
}

View File

@@ -0,0 +1,43 @@
import { TagGroup, TagGroupProps } from "@/src/components/v2/TagGroup/TagGroup";
import { Meta, StoryObj } from "@kachurun/storybook-solid";
const meta: Meta<TagGroupProps> = {
title: "Components/TagGroup",
component: TagGroup,
decorators: [
(Story: StoryObj) => (
/* for some reason w-x from tailwind was not working */
<div style="width: 196px">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<TagGroupProps>;
export const Default: Story = {
args: {
labels: [
"Tag 1",
"Tag 2",
"Tag 3",
"Tag 4",
"Tag 5",
"Tag 6",
"Tag 7",
"Tag 8",
"Tag 9",
"Tag 10",
],
},
};
export const Inverted: Story = {
args: {
...Default.args,
inverted: true,
},
};

View File

@@ -0,0 +1,21 @@
import "./TagGroup.css";
import cx from "classnames";
import { For } from "solid-js";
import { Tag } from "@/src/components/v2/Tag/Tag";
export interface TagGroupProps {
labels: string[];
inverted?: boolean;
}
export const TagGroup = (props: TagGroupProps) => {
const inverted = () => props.inverted || false;
return (
<div class={cx("tag-group", { inverted: inverted() })}>
<For each={props.labels}>
{(label) => <Tag label={label} inverted={inverted()} />}
</For>
</div>
);
};

View File

@@ -1,17 +1,12 @@
import argparse
import json
import logging
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from clan_lib.api import API
from clan_lib.cmd import RunOpts, run
from clan_lib.dirs import specific_machine_dir
from clan_lib.errors import ClanCmdError, ClanError
from clan_lib.git import commit_file
from clan_lib.machines.hardware import (
HardwareConfig,
HardwareGenerateOptions,
generate_machine_hardware_info,
)
from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_config, nix_eval
from clan_lib.ssh.remote import Remote
from clan_cli.completions import add_dynamic_completer, complete_machines
@@ -21,142 +16,6 @@ from .types import machine_name_type
log = logging.getLogger(__name__)
class HardwareConfig(Enum):
NIXOS_FACTER = "nixos-facter"
NIXOS_GENERATE_CONFIG = "nixos-generate-config"
NONE = "none"
def config_path(self, machine: Machine) -> Path:
machine_dir = specific_machine_dir(machine)
if self == HardwareConfig.NIXOS_FACTER:
return machine_dir / "facter.json"
return machine_dir / "hardware-configuration.nix"
@classmethod
def detect_type(cls: type["HardwareConfig"], machine: Machine) -> "HardwareConfig":
hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path(machine)
if hardware_config.exists() and "throw" not in hardware_config.read_text():
return HardwareConfig.NIXOS_GENERATE_CONFIG
if HardwareConfig.NIXOS_FACTER.config_path(machine).exists():
return HardwareConfig.NIXOS_FACTER
return HardwareConfig.NONE
@API.register
def show_machine_hardware_config(machine: Machine) -> HardwareConfig:
"""
Show hardware information for a machine returns None if none exist.
"""
return HardwareConfig.detect_type(machine)
@API.register
def show_machine_hardware_platform(machine: Machine) -> str | None:
"""
Show hardware information for a machine returns None if none exist.
"""
config = nix_config()
system = config["system"]
cmd = nix_eval(
[
f"{machine.flake}#clanInternals.machines.{system}.{machine.name}",
"--apply",
"machine: { inherit (machine.pkgs) system; }",
"--json",
]
)
proc = run(cmd, RunOpts(prefix=machine.name))
res = proc.stdout.strip()
host_platform = json.loads(res)
return host_platform.get("system", None)
@dataclass
class HardwareGenerateOptions:
machine: Machine
backend: HardwareConfig
password: str | None = None
@API.register
def generate_machine_hardware_info(
opts: HardwareGenerateOptions, target_host: Remote
) -> HardwareConfig:
"""
Generate hardware information for a machine
and place the resulting *.nix file in the machine's directory.
"""
machine = opts.machine
hw_file = opts.backend.config_path(opts.machine)
hw_file.parent.mkdir(parents=True, exist_ok=True)
if opts.backend == HardwareConfig.NIXOS_FACTER:
config_command = ["nixos-facter"]
else:
config_command = [
"nixos-generate-config",
# Filesystems are managed by disko
"--no-filesystems",
"--show-hardware-config",
]
with target_host.ssh_control_master() as ssh, ssh.become_root() as sudo_ssh:
out = sudo_ssh.run(config_command, opts=RunOpts(check=False))
if out.returncode != 0:
if "nixos-facter" in out.stderr and "not found" in out.stderr:
machine.error(str(out.stderr))
msg = (
"Please use our custom nixos install images from https://github.com/nix-community/nixos-images/releases/tag/nixos-unstable. "
"nixos-factor only works on nixos / clan systems currently."
)
raise ClanError(msg)
machine.error(str(out))
msg = f"Failed to inspect {opts.machine}. Address: {target_host.target}"
raise ClanError(msg)
backup_file = None
if hw_file.exists():
backup_file = hw_file.with_suffix(".bak")
hw_file.replace(backup_file)
hw_file.write_text(out.stdout)
print(f"Successfully generated: {hw_file}")
# try to evaluate the machine
# If it fails, the hardware-configuration.nix file is invalid
commit_file(
hw_file,
opts.machine.flake.path,
f"machines/{opts.machine}/{hw_file.name}: update hardware configuration",
)
try:
show_machine_hardware_platform(opts.machine)
if backup_file:
backup_file.unlink(missing_ok=True)
except ClanCmdError as e:
log.exception("Failed to evaluate hardware-configuration.nix")
# Restore the backup file
print(f"Restoring backup file {backup_file}")
if backup_file:
backup_file.replace(hw_file)
# TODO: Undo the commit
msg = "Invalid hardware-configuration.nix file"
raise ClanError(
msg,
description=f"Configuration at '{hw_file}' is invalid. Please check the file and try again.",
) from e
return opts.backend
def update_hardware_config_command(args: argparse.Namespace) -> None:
host_key_check = args.host_key_check
machine = Machine(flake=args.flake, name=args.machine)

View File

@@ -1,17 +1,11 @@
import argparse
import logging
import os
import sys
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from tempfile import TemporaryDirectory
from clan_lib.api import API
from clan_lib.cmd import Log, RunOpts, run
from clan_lib.errors import ClanError
from clan_lib.machines.install import BuildOn, InstallOptions, install_machine
from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_shell
from clan_lib.ssh.remote import Remote
from clan_cli.completions import (
@@ -19,145 +13,12 @@ from clan_cli.completions import (
complete_machines,
complete_target_host,
)
from clan_cli.facts.generate import generate_facts
from clan_cli.machines.hardware import HardwareConfig
from clan_cli.ssh.deploy_info import DeployInfo, find_reachable_host, ssh_command_parse
from clan_cli.vars.generate import generate_vars
log = logging.getLogger(__name__)
class BuildOn(Enum):
AUTO = "auto"
LOCAL = "local"
REMOTE = "remote"
@dataclass
class InstallOptions:
machine: Machine
kexec: str | None = None
debug: bool = False
no_reboot: bool = False
phases: str | None = None
build_on: BuildOn | None = None
nix_options: list[str] = field(default_factory=list)
update_hardware_config: HardwareConfig = HardwareConfig.NONE
password: str | None = None
identity_file: Path | None = None
use_tor: bool = False
@API.register
def install_machine(opts: InstallOptions, target_host: Remote) -> None:
machine = opts.machine
machine.debug(f"installing {machine.name}")
generate_facts([machine])
generate_vars([machine])
with (
TemporaryDirectory(prefix="nixos-install-") as _base_directory,
):
base_directory = Path(_base_directory).resolve()
activation_secrets = base_directory / "activation_secrets"
upload_dir = activation_secrets / machine.secrets_upload_directory.lstrip("/")
upload_dir.mkdir(parents=True)
machine.secret_facts_store.upload(upload_dir)
machine.secret_vars_store.populate_dir(
upload_dir, phases=["activation", "users", "services"]
)
partitioning_secrets = base_directory / "partitioning_secrets"
partitioning_secrets.mkdir(parents=True)
machine.secret_vars_store.populate_dir(
partitioning_secrets, phases=["partitioning"]
)
if opts.password:
os.environ["SSHPASS"] = opts.password
cmd = [
"nixos-anywhere",
"--flake",
f"{machine.flake}#{machine.name}",
"--extra-files",
str(activation_secrets),
]
for path in partitioning_secrets.rglob("*"):
if path.is_file():
cmd.extend(
[
"--disk-encryption-keys",
str(
"/run/partitioning-secrets"
/ path.relative_to(partitioning_secrets)
),
str(path),
]
)
if opts.no_reboot:
cmd.append("--no-reboot")
if opts.phases:
cmd += ["--phases", str(opts.phases)]
if opts.update_hardware_config is not HardwareConfig.NONE:
cmd.extend(
[
"--generate-hardware-config",
str(opts.update_hardware_config.value),
str(opts.update_hardware_config.config_path(machine)),
]
)
if opts.password:
cmd += [
"--env-password",
"--ssh-option",
"IdentitiesOnly=yes",
]
if opts.identity_file:
cmd += ["-i", str(opts.identity_file)]
if opts.build_on:
cmd += ["--build-on", opts.build_on.value]
if target_host.port:
cmd += ["--ssh-port", str(target_host.port)]
if opts.kexec:
cmd += ["--kexec", opts.kexec]
if opts.debug:
cmd.append("--debug")
# Add nix options to nixos-anywhere
cmd.extend(opts.nix_options)
cmd.append(target_host.target)
if opts.use_tor:
# nix copy does not support tor socks proxy
# cmd.append("--ssh-option")
# cmd.append("ProxyCommand=nc -x 127.0.0.1:9050 -X 5 %h %p")
cmd = nix_shell(
[
"nixos-anywhere",
"tor",
],
["torify", *cmd],
)
else:
cmd = nix_shell(
["nixos-anywhere"],
cmd,
)
run(cmd, RunOpts(log=Log.BOTH, prefix=machine.name, needs_user_terminal=True))
def install_command(args: argparse.Namespace) -> None:
try:
# Only if the caller did not specify a target_host via args.target_host

View File

@@ -280,7 +280,9 @@ def update_command(args: argparse.Namespace) -> None:
address=args.target_host,
).override(host_key_check=host_key_check)
else:
target_host = machine.target_host()
target_host = machine.target_host().override(
host_key_check=host_key_check
)
runtime.async_run(
AsyncOpts(
tid=machine.name,

View File

@@ -1,34 +0,0 @@
from dataclasses import dataclass
from typing import Generic
from clan_lib.errors import CmdOut
from clan_lib.ssh.remote import Remote
from clan_cli.ssh import T
@dataclass
class HostResult(Generic[T]):
host: Remote
_result: T | Exception
@property
def error(self) -> Exception | None:
"""
Returns an error if the command failed
"""
if isinstance(self._result, Exception):
return self._result
return None
@property
def result(self) -> T:
"""
Unwrap the result
"""
if isinstance(self._result, Exception):
raise self._result
return self._result
Results = list[HostResult[CmdOut]]

View File

@@ -5,13 +5,12 @@ from dataclasses import dataclass
from typing import Any, TypedDict
from uuid import uuid4
from clan_cli.machines.hardware import HardwareConfig, show_machine_hardware_config
from clan_lib.api import API
from clan_lib.api.modules import Frontmatter, extract_frontmatter
from clan_lib.dirs import TemplateType, clan_templates
from clan_lib.errors import ClanError
from clan_lib.git import commit_file
from clan_lib.machines.hardware import HardwareConfig, show_machine_hardware_config
from clan_lib.machines.machines import Machine
log = logging.getLogger(__name__)

View File

@@ -0,0 +1,152 @@
import json
import logging
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from clan_lib.api import API
from clan_lib.cmd import RunOpts, run
from clan_lib.dirs import specific_machine_dir
from clan_lib.errors import ClanCmdError, ClanError
from clan_lib.git import commit_file
from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_config, nix_eval
from clan_lib.ssh.remote import Remote
log = logging.getLogger(__name__)
class HardwareConfig(Enum):
NIXOS_FACTER = "nixos-facter"
NIXOS_GENERATE_CONFIG = "nixos-generate-config"
NONE = "none"
def config_path(self, machine: Machine) -> Path:
machine_dir = specific_machine_dir(machine)
if self == HardwareConfig.NIXOS_FACTER:
return machine_dir / "facter.json"
return machine_dir / "hardware-configuration.nix"
@classmethod
def detect_type(cls: type["HardwareConfig"], machine: Machine) -> "HardwareConfig":
hardware_config = HardwareConfig.NIXOS_GENERATE_CONFIG.config_path(machine)
if hardware_config.exists() and "throw" not in hardware_config.read_text():
return HardwareConfig.NIXOS_GENERATE_CONFIG
if HardwareConfig.NIXOS_FACTER.config_path(machine).exists():
return HardwareConfig.NIXOS_FACTER
return HardwareConfig.NONE
@API.register
def show_machine_hardware_config(machine: Machine) -> HardwareConfig:
"""
Show hardware information for a machine returns None if none exist.
"""
return HardwareConfig.detect_type(machine)
@API.register
def show_machine_hardware_platform(machine: Machine) -> str | None:
"""
Show hardware information for a machine returns None if none exist.
"""
config = nix_config()
system = config["system"]
cmd = nix_eval(
[
f"{machine.flake}#clanInternals.machines.{system}.{machine.name}",
"--apply",
"machine: { inherit (machine.pkgs) system; }",
"--json",
]
)
proc = run(cmd, RunOpts(prefix=machine.name))
res = proc.stdout.strip()
host_platform = json.loads(res)
return host_platform.get("system", None)
@dataclass
class HardwareGenerateOptions:
machine: Machine
backend: HardwareConfig
password: str | None = None
@API.register
def generate_machine_hardware_info(
opts: HardwareGenerateOptions, target_host: Remote
) -> HardwareConfig:
"""
Generate hardware information for a machine
and place the resulting *.nix file in the machine's directory.
"""
machine = opts.machine
hw_file = opts.backend.config_path(opts.machine)
hw_file.parent.mkdir(parents=True, exist_ok=True)
if opts.backend == HardwareConfig.NIXOS_FACTER:
config_command = ["nixos-facter"]
else:
config_command = [
"nixos-generate-config",
# Filesystems are managed by disko
"--no-filesystems",
"--show-hardware-config",
]
with target_host.ssh_control_master() as ssh, ssh.become_root() as sudo_ssh:
out = sudo_ssh.run(config_command, opts=RunOpts(check=False))
if out.returncode != 0:
if "nixos-facter" in out.stderr and "not found" in out.stderr:
machine.error(str(out.stderr))
msg = (
"Please use our custom nixos install images from https://github.com/nix-community/nixos-images/releases/tag/nixos-unstable. "
"nixos-factor only works on nixos / clan systems currently."
)
raise ClanError(msg)
machine.error(str(out))
msg = f"Failed to inspect {opts.machine}. Address: {target_host.target}"
raise ClanError(msg)
backup_file = None
if hw_file.exists():
backup_file = hw_file.with_suffix(".bak")
hw_file.replace(backup_file)
hw_file.write_text(out.stdout)
print(f"Successfully generated: {hw_file}")
# try to evaluate the machine
# If it fails, the hardware-configuration.nix file is invalid
commit_file(
hw_file,
opts.machine.flake.path,
f"machines/{opts.machine}/{hw_file.name}: update hardware configuration",
)
try:
show_machine_hardware_platform(opts.machine)
if backup_file:
backup_file.unlink(missing_ok=True)
except ClanCmdError as e:
log.exception("Failed to evaluate hardware-configuration.nix")
# Restore the backup file
print(f"Restoring backup file {backup_file}")
if backup_file:
backup_file.replace(hw_file)
# TODO: Undo the commit
msg = "Invalid hardware-configuration.nix file"
raise ClanError(
msg,
description=f"Configuration at '{hw_file}' is invalid. Please check the file and try again.",
) from e
return opts.backend

View File

@@ -0,0 +1,149 @@
import logging
import os
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from tempfile import TemporaryDirectory
from clan_cli.facts.generate import generate_facts
from clan_cli.machines.hardware import HardwareConfig
from clan_cli.vars.generate import generate_vars
from clan_lib.api import API
from clan_lib.cmd import Log, RunOpts, run
from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_shell
from clan_lib.ssh.remote import Remote
log = logging.getLogger(__name__)
class BuildOn(Enum):
AUTO = "auto"
LOCAL = "local"
REMOTE = "remote"
@dataclass
class InstallOptions:
machine: Machine
kexec: str | None = None
debug: bool = False
no_reboot: bool = False
phases: str | None = None
build_on: BuildOn | None = None
nix_options: list[str] = field(default_factory=list)
update_hardware_config: HardwareConfig = HardwareConfig.NONE
password: str | None = None
identity_file: Path | None = None
use_tor: bool = False
@API.register
def install_machine(opts: InstallOptions, target_host: Remote) -> None:
machine = opts.machine
machine.debug(f"installing {machine.name}")
generate_facts([machine])
generate_vars([machine])
with (
TemporaryDirectory(prefix="nixos-install-") as _base_directory,
):
base_directory = Path(_base_directory).resolve()
activation_secrets = base_directory / "activation_secrets"
upload_dir = activation_secrets / machine.secrets_upload_directory.lstrip("/")
upload_dir.mkdir(parents=True)
machine.secret_facts_store.upload(upload_dir)
machine.secret_vars_store.populate_dir(
upload_dir, phases=["activation", "users", "services"]
)
partitioning_secrets = base_directory / "partitioning_secrets"
partitioning_secrets.mkdir(parents=True)
machine.secret_vars_store.populate_dir(
partitioning_secrets, phases=["partitioning"]
)
if opts.password:
os.environ["SSHPASS"] = opts.password
cmd = [
"nixos-anywhere",
"--flake",
f"{machine.flake}#{machine.name}",
"--extra-files",
str(activation_secrets),
]
for path in partitioning_secrets.rglob("*"):
if path.is_file():
cmd.extend(
[
"--disk-encryption-keys",
str(
"/run/partitioning-secrets"
/ path.relative_to(partitioning_secrets)
),
str(path),
]
)
if opts.no_reboot:
cmd.append("--no-reboot")
if opts.phases:
cmd += ["--phases", str(opts.phases)]
if opts.update_hardware_config is not HardwareConfig.NONE:
cmd.extend(
[
"--generate-hardware-config",
str(opts.update_hardware_config.value),
str(opts.update_hardware_config.config_path(machine)),
]
)
if opts.password:
cmd += [
"--env-password",
"--ssh-option",
"IdentitiesOnly=yes",
]
if opts.identity_file:
cmd += ["-i", str(opts.identity_file)]
if opts.build_on:
cmd += ["--build-on", opts.build_on.value]
if target_host.port:
cmd += ["--ssh-port", str(target_host.port)]
if opts.kexec:
cmd += ["--kexec", opts.kexec]
if opts.debug:
cmd.append("--debug")
# Add nix options to nixos-anywhere
cmd.extend(opts.nix_options)
cmd.append(target_host.target)
if opts.use_tor:
# nix copy does not support tor socks proxy
# cmd.append("--ssh-option")
# cmd.append("ProxyCommand=nc -x 127.0.0.1:9050 -X 5 %h %p")
cmd = nix_shell(
[
"nixos-anywhere",
"tor",
],
["torify", *cmd],
)
else:
cmd = nix_shell(
["nixos-anywhere"],
cmd,
)
run(cmd, RunOpts(log=Log.BOTH, prefix=machine.name, needs_user_terminal=True))