Compare commits

..

1 Commits

Author SHA1 Message Date
a-kenji
0a502ab242 pkgs/cli: Add facts deprecation warning to clan facts help output 2025-07-15 14:28:36 +02:00
16 changed files with 124 additions and 490 deletions

View File

@@ -1,27 +0,0 @@
name: Update Flake Inputs
on:
schedule:
# Run weekly on Sunday at 4:00 AM UTC
- cron: "0 4 * * 0"
workflow_dispatch:
repository_dispatch:
jobs:
update-flake-inputs:
runs-on: nix
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure git
run: |
git config --global user.email "clan-bot@clan.lol"
git config --global user.name "clan-bot"
- name: Update flake inputs
uses: Mic92/update-flake-inputs-gitea@main
env:
# Exclude private flakes and update-clan-core checks flake
EXCLUDE_PATTERNS: "devFlake/private/flake.nix,checks/impure/flake.nix"

View File

@@ -9,37 +9,15 @@
interface =
{ lib, ... }:
{
options = {
allowedKeys = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.str;
description = "The allowed public keys for ssh access to the admin user";
example = {
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
};
};
rsaHostKey.enable = lib.mkEnableOption "Generate RSA host key";
# TODO: allow per-server domains that we than collect in the inventory
#certicficateDomains = lib.mkOption {
# type = lib.types.listOf lib.types.str;
# default = [ ];
# example = [ "git.mydomain.com" ];
# description = "List of domains to include in the certificate. This option will not prepend the machine name in front of each domain.";
#};
certificateSearchDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "mydomain.com" ];
description = ''
List of domains to include in the certificate.
This option will prepend the machine name in front of each domain before adding it to the certificate.
'';
options.allowedKeys = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.str;
description = "The allowed public keys for ssh access to the admin user";
example = {
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
};
};
};
perInstance =
@@ -49,15 +27,10 @@
{ ... }:
{
imports = [
# We don't have a good way to specify dependencies between
# clanServices for now. When it get's implemtende, we should just
# use the ssh and users modules here.
./ssh.nix
./root-password.nix
../../clanModules/sshd
../../clanModules/root-password
];
_module.args = { inherit settings; };
users.users.root.openssh.authorizedKeys.keys = builtins.attrValues settings.allowedKeys;
};
};

View File

@@ -1,39 +0,0 @@
# We don't have a way of specifying dependencies between clanServices for now.
# When it get's added this file should be removed and the users module used instead.
{
config,
pkgs,
...
}:
{
users.mutableUsers = false;
users.users.root.hashedPasswordFile =
config.clan.core.vars.generators.root-password.files.password-hash.path;
clan.core.vars.generators.root-password = {
files.password-hash.neededFor = "users";
files.password.deploy = false;
runtimeInputs = [
pkgs.coreutils
pkgs.mkpasswd
pkgs.xkcdpass
];
prompts.password.type = "hidden";
prompts.password.persist = true;
prompts.password.description = "You can autogenerate a password, if you leave this prompt blank.";
script = ''
prompt_value="$(cat "$prompts"/password)"
if [[ -n "''${prompt_value-}" ]]; then
echo "$prompt_value" | tr -d "\n" > "$out"/password
else
xkcdpass --numwords 5 --delimiter - --count 1 | tr -d "\n" > "$out"/password
fi
mkpasswd -s -m sha-512 < "$out"/password | tr -d "\n" > "$out"/password-hash
'';
};
}

View File

@@ -1,115 +0,0 @@
{
config,
pkgs,
lib,
settings,
...
}:
let
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
domains = stringSet settings.certificateSearchDomains;
in
{
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
settings.HostCertificate = lib.mkIf (
settings.certificateSearchDomains != [ ]
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
hostKeys =
[
{
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
type = "ed25519";
}
]
++ lib.optional settings.rsaHostKey.enable {
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
type = "rsa";
};
};
clan.core.vars.generators.openssh = {
files."ssh.id_ed25519" = { };
files."ssh.id_ed25519.pub".secret = false;
migrateFact = "openssh";
runtimeInputs = [
pkgs.coreutils
pkgs.openssh
];
script = ''
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/ssh.id_ed25519
'';
};
programs.ssh.knownHosts.clan-sshd-self-ed25519 = {
hostNames = [
"localhost"
config.networking.hostName
] ++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
publicKey = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519.pub".value;
};
clan.core.vars.generators.openssh-rsa = lib.mkIf settings.rsaHostKey.enable {
files."ssh.id_rsa" = { };
files."ssh.id_rsa.pub".secret = false;
runtimeInputs = [
pkgs.coreutils
pkgs.openssh
];
script = ''
ssh-keygen -t rsa -b 4096 -N "" -C "" -f "$out"/ssh.id_rsa
'';
};
clan.core.vars.generators.openssh-cert = lib.mkIf (settings.certificateSearchDomains != [ ]) {
files."ssh.id_ed25519-cert.pub".secret = false;
dependencies = [
"openssh"
"openssh-ca"
];
validation = {
name = config.clan.core.settings.machine.name;
domains = lib.genAttrs settings.certificateSearchDomains lib.id;
};
runtimeInputs = [
pkgs.openssh
pkgs.jq
];
script = ''
ssh-keygen \
-s $in/openssh-ca/id_ed25519 \
-I ${config.clan.core.settings.machine.name} \
-h \
-n ${lib.concatMapStringsSep "," (d: "${config.clan.core.settings.machine.name}.${d}") domains} \
$in/openssh/ssh.id_ed25519.pub
mv $in/openssh/ssh.id_ed25519-cert.pub "$out"/ssh.id_ed25519-cert.pub
'';
};
clan.core.vars.generators.openssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
share = true;
files.id_ed25519.deploy = false;
files."id_ed25519.pub" = {
deploy = false;
secret = false;
};
runtimeInputs = [
pkgs.openssh
];
script = ''
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
'';
};
programs.ssh.knownHosts.ssh-ca = lib.mkIf (settings.certificateSearchDomains != [ ]) {
certAuthority = true;
extraHostNames = builtins.map (domain: "*.${domain}") settings.certificateSearchDomains;
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
};
}

View File

@@ -66,11 +66,11 @@
},
"nixpkgs-dev": {
"locked": {
"lastModified": 1752467518,
"narHash": "sha256-7SSvjNlM5ZsFZMP7Nw2uUa7EKYhB6Ny9iNtxtPPhWYY=",
"lastModified": 1752039390,
"narHash": "sha256-DTHMN6kh1cGoc5hc9O0pYN+VAOnjsyy0wxq4YO5ZRvg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2f21cef1d1dc734a2dd89f535427cf291aebc8ef",
"rev": "6ec4d5f023c3c000cda569255a3486e8710c39bf",
"type": "github"
},
"original": {

View File

@@ -55,37 +55,9 @@ If you're using VSCode, it has a handy feature that makes paths to source code f
## Finding Print Messages
To trace the origin of print messages in `clan-cli`, you can enable special debugging features using environment variables:
- Set `TRACE_PRINT=1` to include the source location with each print message:
```bash
export TRACE_PRINT=1
```
When running commands with `--debug`, every print will show where it was triggered in the code.
- To see a deeper stack trace for each print, set `TRACE_DEPTH` to the desired number of stack frames (e.g., 3):
```bash
export TRACE_DEPTH=3
```
### Additional Debug Logging
You can enable more detailed logging for specific components by setting these environment variables:
- `CLAN_DEBUG_NIX_SELECTORS=1` — verbose logs for flake.select operations
- `CLAN_DEBUG_NIX_PREFETCH=1` — verbose logs for flake.prefetch operations
- `CLAN_DEBUG_COMMANDS=1` — print the diffed environment of executed commands
Example:
```bash
export CLAN_DEBUG_NIX_SELECTORS=1
export CLAN_DEBUG_NIX_PREFETCH=1
export CLAN_DEBUG_COMMANDS=1
```
These options help you pinpoint the source and context of print messages and debug logs during development.
To identify where a specific print message comes from, you can enable a helpful feature. Simply set the environment variable `export TRACE_PRINT=1`. When you run commands with `--debug` mode, each print message will include information about its source location.
If you need more details, you can expand the stack trace information that appears with each print by setting the environment variable `export TRACE_DEPTH=3`.
## Analyzing Performance

View File

@@ -181,13 +181,6 @@ You can have a look and customize it if needed.
!!! tip
For advanced partitioning, see [Disko templates](https://github.com/nix-community/disko-templates) or [Disko examples](https://github.com/nix-community/disko/tree/master/example).
!!! Danger
Don't change the `disko.nix` after the machine is installed for the first time.
Changing disko configuration requires wiping and reinstalling the machine.
Unless you really know what you are doing.
## Deploy the machine
**Finally deployment time!** Use one of the following commands to build and deploy the image via SSH onto your machine.
@@ -274,3 +267,4 @@ clan {
```
This is useful for machines that are not always online or are not part of the regular update cycle.

8
flake.lock generated
View File

@@ -16,11 +16,11 @@
]
},
"locked": {
"lastModified": 1752451292,
"narHash": "sha256-jvLbfYFvcS5f0AEpUlFS2xZRnK770r9TRM2smpUFFaU=",
"rev": "309e06fbc9a6d133ab6dd1c7d8e4876526e058bb",
"lastModified": 1751846468,
"narHash": "sha256-h0mpWZIOIAKj4fmLNyI2HDG+c0YOkbYmyJXSj/bQ9s0=",
"rev": "a2166c13b0cb3febdaf36391cd2019aa2ccf4366",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/309e06fbc9a6d133ab6dd1c7d8e4876526e058bb.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/a2166c13b0cb3febdaf36391cd2019aa2ccf4366.tar.gz"
},
"original": {
"type": "tarball",

View File

@@ -22,7 +22,6 @@ let
package
path
str
strMatching
submoduleWith
;
# the original types.submodule has strange behavior
@@ -48,7 +47,7 @@ in
imports = [ ./generator.nix ];
options = {
name = lib.mkOption {
type = str;
type = lib.types.str;
description = ''
The name of the generator.
This name will be used to refer to the generator in other generators.
@@ -154,7 +153,7 @@ in
options =
{
name = lib.mkOption {
type = str;
type = lib.types.str;
description = ''
name of the public fact
'';
@@ -163,7 +162,7 @@ in
defaultText = "Name of the file";
};
generatorName = lib.mkOption {
type = str;
type = lib.types.str;
description = ''
name of the generator
'';
@@ -172,7 +171,7 @@ in
defaultText = "Name of the generator that generates this file";
};
share = lib.mkOption {
type = bool;
type = lib.types.bool;
description = ''
Whether the generated vars should be shared between machines.
Shared vars are only generated once, when the first machine using it is deployed.
@@ -234,7 +233,7 @@ in
By setting this to `user`, the secret will be deployed prior to users and groups are created, allowing
users' passwords to be managed by vars. The secret will be stored in `/run/secrets-for-users` and `owner` and `group` must be `root`.
'';
type = enum [
type = lib.types.enum [
"partitioning"
"activation"
"users"
@@ -252,7 +251,7 @@ in
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
};
mode = lib.mkOption {
type = strMatching "^[0-7]{4}$";
type = lib.types.strMatching "^[0-7]{4}$";
description = "The unix file mode of the file. Must be a 4-digit octal number.";
default = "0400";
};
@@ -376,7 +375,7 @@ in
- all required programs are in PATH
- sandbox is set up correctly
'';
type = path;
type = lib.types.path;
readOnly = true;
internal = true;
};

View File

@@ -250,12 +250,12 @@ This subcommand allows seamless ssh access to the nixos-image builders or a mach
Examples:
$ clan ssh berlin
$ clan ssh [ssh_args ...] berlin`
Will ssh in to the machine called `berlin`, using the
`clan.core.networking.targetHost` specified in its configuration
$ clan ssh --json [JSON] --host-key-check none
$ clan ssh [ssh_args ...] --json [JSON]
Will ssh in to the machine based on the deployment information contained in
the json string. [JSON] can either be a json formatted string itself, or point
towards a file containing the deployment information
@@ -297,6 +297,8 @@ For more detailed information, visit: {help_hyperlink("secrets", "https://docs.c
description="Manage facts",
epilog=(
f"""
Note: Facts are being deprecated, please use Vars instead.
For a migration guide visit: {help_hyperlink("vars", "https://docs.clan.lol/guides/migrations/migration-facts-vars")}
This subcommand provides an interface to facts of clan machines.
Facts are artifacts that a service can generate.

View File

@@ -24,14 +24,12 @@ def install_command(args: argparse.Namespace) -> None:
# Only if the caller did not specify a target_host via args.target_host
# Find a suitable target_host that is reachable
target_host_str = args.target_host
deploy_info: DeployInfo | None = (
ssh_command_parse(args) if target_host_str is None else None
)
deploy_info: DeployInfo | None = ssh_command_parse(args)
use_tor = False
if deploy_info:
if deploy_info and not args.target_host:
host = find_reachable_host(deploy_info)
if host is None or host.tor_socks:
if host is None:
use_tor = True
target_host_str = deploy_info.tor.target
else:

View File

@@ -1,14 +1,12 @@
import argparse
import json
import logging
import textwrap
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from clan_lib.cmd import run
from clan_lib.errors import ClanError
from clan_lib.machines.machines import Machine
from clan_lib.nix import nix_shell
from clan_lib.ssh.remote import HostKeyCheck, Remote
@@ -39,23 +37,20 @@ class DeployInfo:
raise ClanError(msg)
return addrs[0]
def overwrite_remotes(
self,
host_key_check: HostKeyCheck | None = None,
private_key: Path | None = None,
ssh_options: dict[str, str] | None = None,
@staticmethod
def from_hostnames(
hostname: list[str], host_key_check: HostKeyCheck
) -> "DeployInfo":
"""Return a new DeployInfo with all Remotes overridden with the given host_key_check."""
return DeployInfo(
addrs=[
addr.override(
host_key_check=host_key_check,
private_key=private_key,
ssh_options=ssh_options,
)
for addr in self.addrs
]
)
remotes = []
for host in hostname:
if not host:
msg = "Hostname cannot be empty."
raise ClanError(msg)
remote = Remote.from_ssh_uri(
machine_name="clan-installer", address=host
).override(host_key_check=host_key_check)
remotes.append(remote)
return DeployInfo(addrs=remotes)
@staticmethod
def from_json(data: dict[str, Any], host_key_check: HostKeyCheck) -> "DeployInfo":
@@ -108,22 +103,9 @@ def find_reachable_host(deploy_info: DeployInfo) -> Remote | None:
return None
def ssh_shell_from_deploy(
deploy_info: DeployInfo, command: list[str] | None = None
) -> None:
if command and len(command) == 1 and command[0].count(" ") > 0:
msg = (
textwrap.dedent("""
It looks like you quoted the remote command.
The first argument should be the command to run, not a quoted string.
""")
.lstrip("\n")
.rstrip("\n")
)
raise ClanError(msg)
def ssh_shell_from_deploy(deploy_info: DeployInfo) -> None:
if host := find_reachable_host(deploy_info):
host.interactive_ssh(command)
host.interactive_ssh()
return
log.info("Could not reach host via clearnet 'addrs'")
@@ -145,7 +127,7 @@ def ssh_shell_from_deploy(
log.info(
"Host reachable via tor address, starting interactive ssh session."
)
tor_addr.interactive_ssh(command)
tor_addr.interactive_ssh()
return
log.error("Could not reach host via tor address.")
@@ -153,35 +135,19 @@ def ssh_shell_from_deploy(
def ssh_command_parse(args: argparse.Namespace) -> DeployInfo | None:
host_key_check = args.host_key_check
deploy = None
if args.json:
json_file = Path(args.json)
if json_file.is_file():
data = json.loads(json_file.read_text())
return DeployInfo.from_json(data, host_key_check)
data = json.loads(args.json)
deploy = DeployInfo.from_json(data, host_key_check)
elif args.png:
deploy = DeployInfo.from_qr_code(Path(args.png), host_key_check)
elif hasattr(args, "machine") and args.machine:
machine = Machine(args.machine, args.flake)
target = machine.target_host().override(
command_prefix=machine.name, host_key_check=host_key_check
)
deploy = DeployInfo(addrs=[target])
else:
return None
return DeployInfo.from_json(data, host_key_check)
if args.png:
return DeployInfo.from_qr_code(Path(args.png), host_key_check)
ssh_options = None
if hasattr(args, "ssh_option") and args.ssh_option:
for name, value in args.ssh_option:
ssh_options = {}
ssh_options[name] = value
deploy = deploy.overwrite_remotes(ssh_options=ssh_options)
return deploy
if hasattr(args, "machines"):
return DeployInfo.from_hostnames(args.machines, host_key_check)
return None
def ssh_command(args: argparse.Namespace) -> None:
@@ -189,63 +155,36 @@ def ssh_command(args: argparse.Namespace) -> None:
if not deploy_info:
msg = "No MACHINE, --json or --png data provided"
raise ClanError(msg)
ssh_shell_from_deploy(deploy_info, args.remote_command)
ssh_shell_from_deploy(deploy_info)
def register_parser(parser: argparse.ArgumentParser) -> None:
group = parser.add_mutually_exclusive_group()
group.add_argument(
"machine",
group = parser.add_mutually_exclusive_group(required=True)
machines_parser = group.add_argument(
"machines",
type=str,
nargs="?",
nargs="*",
default=[],
metavar="MACHINE",
help="Machine to ssh into (uses clan.core.networking.targetHost from configuration).",
help="Machine to ssh into.",
)
add_dynamic_completer(machines_parser, complete_machines)
group.add_argument(
"-j",
"--json",
type=str,
help=(
"Deployment information as a JSON string or path to a JSON file "
"(generated by starting the clan installer)."
),
help="specify the json file for ssh data (generated by starting the clan installer)",
)
group.add_argument(
"-P",
"--png",
type=str,
help="Deployment information as a QR code image file (generated by starting the clan installer).",
help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer)",
)
parser.add_argument(
"--host-key-check",
choices=["strict", "ask", "tofu", "none"],
default="tofu",
help="Host key (.ssh/known_hosts) check mode.",
)
parser.add_argument(
"--ssh-option",
help="SSH option to set (can be specified multiple times)",
nargs=2,
metavar=("name", "value"),
action="append",
default=[],
)
parser.add_argument(
"-c",
"--remote-command",
type=str,
metavar="COMMAND",
nargs=argparse.REMAINDER,
help="Command to execute on the remote host, needs to be the LAST argument as it takes all remaining arguments.",
)
add_dynamic_completer(
parser._actions[1], # noqa: SLF001
complete_machines,
) # assumes 'machine' is the first positional
parser.set_defaults(func=ssh_command)

View File

@@ -7,8 +7,6 @@ from clan_lib.nix import nix_shell
from clan_lib.ssh.remote import Remote
from clan_cli.ssh.deploy_info import DeployInfo, find_reachable_host
from clan_cli.tests.fixtures_flakes import ClanFlake
from clan_cli.tests.helpers import cli
def test_qrcode_scan(temp_dir: Path) -> None:
@@ -71,10 +69,7 @@ def test_from_json() -> None:
@pytest.mark.with_core
def test_find_reachable_host(hosts: list[Remote]) -> None:
host = hosts[0]
uris = ["172.19.1.2", host.ssh_url()]
remotes = [Remote.from_ssh_uri(machine_name="some", address=uri) for uri in uris]
deploy_info = DeployInfo(addrs=remotes)
deploy_info = DeployInfo.from_hostnames(["172.19.1.2", host.ssh_url()], "none")
assert deploy_info.addrs[0].address == "172.19.1.2"
@@ -82,40 +77,3 @@ def test_find_reachable_host(hosts: list[Remote]) -> None:
assert remote is not None
assert remote.ssh_url() == host.ssh_url()
@pytest.mark.with_core
def test_ssh_shell_from_deploy(
hosts: list[Remote],
flake: ClanFlake,
) -> None:
host = hosts[0]
machine1_config = flake.machines["m1_machine"]
machine1_config["nixpkgs"]["hostPlatform"] = "x86_64-linux"
machine1_config["clan"]["networking"]["targetHost"] = host.ssh_url()
flake.refresh()
assert host.private_key
success_txt = flake.path / "success.txt"
assert not success_txt.exists()
cli.run(
[
"ssh",
"--flake",
str(flake.path),
"m1_machine",
"--host-key-check=none",
"--ssh-option",
"IdentityFile",
str(host.private_key),
"--remote-command",
"touch",
str(success_txt),
"&&",
"exit 0",
]
)
assert success_txt.exists()

View File

@@ -1,7 +1,6 @@
import json
import logging
import os
import textwrap
from dataclasses import asdict, dataclass, field
from enum import Enum
from hashlib import sha1
@@ -589,7 +588,7 @@ class FlakeCache:
def load_from_file(self, path: Path) -> None:
with path.open("r") as f:
log.debug("Loading flake cache from file")
log.debug(f"Loading cache from {path}")
data = json.load(f)
self.cache = FlakeCacheEntry.from_json(data["cache"])
@@ -663,7 +662,7 @@ class Flake:
"""
Loads the flake into the store and populates self.store_path and self.hash such that the flake can evaluate locally and offline
"""
from clan_lib.cmd import RunOpts, run
from clan_lib.cmd import run
from clan_lib.nix import (
nix_command,
)
@@ -682,10 +681,7 @@ class Flake:
self.identifier,
]
trace_prefetch = os.environ.get("CLAN_DEBUG_NIX_PREFETCH", "0") == "1"
if not trace_prefetch:
log.debug(f"Prefetching flake {self.identifier}")
flake_prefetch = run(nix_command(cmd), RunOpts(trace=trace_prefetch))
flake_prefetch = run(nix_command(cmd))
flake_metadata = json.loads(flake_prefetch.stdout)
self.store_path = flake_metadata["storePath"]
self.hash = flake_metadata["hash"]
@@ -702,6 +698,8 @@ class Flake:
nix_metadata,
)
log.debug(f"Invalidating cache for {self.identifier}")
self.prefetch()
self._cache = FlakeCache()
@@ -815,42 +813,36 @@ class Flake:
];
}}
"""
if len(selectors) > 1 :
msg = textwrap.dedent(f"""
clan select "{selectors}"
""").lstrip("\n").rstrip("\n")
if os.environ.get("CLAN_DEBUG_NIX_SELECTORS"):
msg += textwrap.dedent(f"""
to debug run:
nix repl --expr 'rec {{
flake = builtins.getFlake "{self.identifier}";
selectLib = (builtins.getFlake "path:{select_source()}?narHash={select_hash}").lib;
query = [
{" ".join(
[
f"(selectLib.select ''{selector}'' flake)"
for selector in selectors
]
)}
];
}}'
""").lstrip("\n")
log.debug(msg)
if len(selectors) > 1:
log.debug(f"""
selecting: {selectors}
to debug run:
nix repl --expr 'rec {{
flake = builtins.getFlake "self.identifier";
selectLib = (builtins.getFlake "path:{select_source()}?narHash={select_hash}").lib;
query = [
{" ".join(
[
f"(selectLib.select ''{selector}'' flake)"
for selector in selectors
]
)}
];
}}'
""")
# fmt: on
elif len(selectors) == 1:
msg = textwrap.dedent(f"""
$ clan select "{selectors[0]}"
""").lstrip("\n").rstrip("\n")
if os.environ.get("CLAN_DEBUG_NIX_SELECTORS"):
msg += textwrap.dedent(f"""
to debug run:
nix repl --expr 'rec {{
flake = builtins.getFlake "{self.identifier}";
selectLib = (builtins.getFlake "path:{select_source()}?narHash={select_hash}").lib;
query = selectLib.select '"''{selectors[0]}''"' flake;
}}'
""").lstrip("\n")
log.debug(msg)
log.debug(
f"""
selecting: {selectors[0]}
to debug run:
nix repl --expr 'rec {{
flake = builtins.getFlake "{self.identifier}";
selectLib = (builtins.getFlake "path:{select_source()}?narHash={select_hash}").lib;
query = selectLib.select '"''{selectors[0]}''"' flake;
}}'
"""
)
build_output = Path(
run(

View File

@@ -16,7 +16,7 @@ from tempfile import TemporaryDirectory
from clan_lib.api import API
from clan_lib.cmd import ClanCmdError, ClanCmdTimeoutError, CmdOut, RunOpts, run
from clan_lib.colors import AnsiColor
from clan_lib.errors import ClanError, indent_command # Assuming these are available
from clan_lib.errors import ClanError # Assuming these are available
from clan_lib.nix import nix_shell
from clan_lib.ssh.host_key import HostKeyCheck, hostkey_to_ssh_opts
from clan_lib.ssh.parse import parse_ssh_uri
@@ -61,9 +61,6 @@ class Remote:
private_key: Path | None = None,
password: str | None = None,
tor_socks: bool | None = None,
command_prefix: str | None = None,
port: int | None = None,
ssh_options: dict[str, str] | None = None,
) -> "Remote":
"""
Returns a new Remote instance with the same data but with a different host_key_check.
@@ -71,8 +68,8 @@ class Remote:
return Remote(
address=self.address,
user=self.user,
command_prefix=command_prefix or self.command_prefix,
port=port or self.port,
command_prefix=self.command_prefix,
port=self.port,
private_key=private_key if private_key is not None else self.private_key,
password=password if password is not None else self.password,
forward_agent=self.forward_agent,
@@ -80,7 +77,7 @@ class Remote:
host_key_check if host_key_check is not None else self.host_key_check
),
verbose_ssh=self.verbose_ssh,
ssh_options=ssh_options or self.ssh_options,
ssh_options=self.ssh_options,
tor_socks=tor_socks if tor_socks is not None else self.tor_socks,
_control_path_dir=self._control_path_dir,
_askpass_path=self._askpass_path,
@@ -421,31 +418,11 @@ class Remote:
msg = f"SSH command failed with return code {res.returncode}"
raise ClanError(msg)
def interactive_ssh(self, command: list[str] | None = None) -> None:
ssh_cmd = self.ssh_cmd(tty=True, control_master=False)
if command:
ssh_cmd = [
*self.ssh_cmd(tty=True, control_master=False),
"--",
"bash",
"-c",
quote('exec "$@"'),
"--",
" ".join(map(quote, command)),
]
cmdlog.info(
f"{indent_command(ssh_cmd)}",
extra={
"command_prefix": self.command_prefix,
"color": AnsiColor.GREEN.value,
},
)
res = subprocess.run(ssh_cmd, check=False)
def interactive_ssh(self) -> None:
cmd_list = self.ssh_cmd(tty=True, control_master=False)
res = subprocess.run(cmd_list, check=False)
# We only check the error code if a password is set, as sshpass is used.
# AS sshpass swallows all output.
if self.password:
self.check_sshpass_errorcode(res)
self.check_sshpass_errorcode(res)
def check_machine_ssh_reachable(self) -> bool:
return check_machine_ssh_reachable(self).ok
@@ -454,7 +431,7 @@ class Remote:
@dataclass(frozen=True)
class ConnectionOptions:
timeout: int = 2
retries: int = 5
retries: int = 10
@dataclass
@@ -527,10 +504,6 @@ def check_machine_ssh_reachable(
if opts is None:
opts = ConnectionOptions()
cmdlog.debug(
f"Checking SSH reachability for {remote.target} on port {remote.port or 22}",
)
address_family = socket.AF_INET6 if ":" in remote.address else socket.AF_INET
for _ in range(opts.retries):
with socket.socket(address_family, socket.SOCK_STREAM) as sock:

15
renovate.json Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"lockFileMaintenance": { "enabled": true },
"nix": {
"enabled": true
},
"packageRules": [
{
"matchManagers": ["npm"],
"matchPaths": ["pkgs/clan-app/ui/**"],
"enabled": false
}
]
}