Compare commits
1 Commits
update-dev
...
ke-facts-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a502ab242 |
@@ -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"
|
|
||||||
@@ -9,37 +9,15 @@
|
|||||||
interface =
|
interface =
|
||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
{
|
{
|
||||||
|
options.allowedKeys = lib.mkOption {
|
||||||
options = {
|
default = { };
|
||||||
allowedKeys = lib.mkOption {
|
type = lib.types.attrsOf lib.types.str;
|
||||||
default = { };
|
description = "The allowed public keys for ssh access to the admin user";
|
||||||
type = lib.types.attrsOf lib.types.str;
|
example = {
|
||||||
description = "The allowed public keys for ssh access to the admin user";
|
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
|
||||||
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.
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
perInstance =
|
perInstance =
|
||||||
@@ -49,15 +27,10 @@
|
|||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
# We don't have a good way to specify dependencies between
|
../../clanModules/sshd
|
||||||
# clanServices for now. When it get's implemtende, we should just
|
../../clanModules/root-password
|
||||||
# use the ssh and users modules here.
|
|
||||||
./ssh.nix
|
|
||||||
./root-password.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
_module.args = { inherit settings; };
|
|
||||||
|
|
||||||
users.users.root.openssh.authorizedKeys.keys = builtins.attrValues settings.allowedKeys;
|
users.users.root.openssh.authorizedKeys.keys = builtins.attrValues settings.allowedKeys;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
6
devFlake/private/flake.lock
generated
6
devFlake/private/flake.lock
generated
@@ -66,11 +66,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-dev": {
|
"nixpkgs-dev": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1752467518,
|
"lastModified": 1752039390,
|
||||||
"narHash": "sha256-7SSvjNlM5ZsFZMP7Nw2uUa7EKYhB6Ny9iNtxtPPhWYY=",
|
"narHash": "sha256-DTHMN6kh1cGoc5hc9O0pYN+VAOnjsyy0wxq4YO5ZRvg=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "2f21cef1d1dc734a2dd89f535427cf291aebc8ef",
|
"rev": "6ec4d5f023c3c000cda569255a3486e8710c39bf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -55,37 +55,9 @@ If you're using VSCode, it has a handy feature that makes paths to source code f
|
|||||||
|
|
||||||
## Finding Print Messages
|
## Finding Print Messages
|
||||||
|
|
||||||
To trace the origin of print messages in `clan-cli`, you can enable special debugging features using environment variables:
|
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.
|
||||||
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
## Analyzing Performance
|
||||||
|
|
||||||
|
|||||||
@@ -181,13 +181,6 @@ You can have a look and customize it if needed.
|
|||||||
!!! tip
|
!!! 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).
|
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
|
## Deploy the machine
|
||||||
|
|
||||||
**Finally deployment time!** Use one of the following commands to build and deploy the image via SSH onto your 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.
|
This is useful for machines that are not always online or are not part of the regular update cycle.
|
||||||
|
|
||||||
|
|||||||
8
flake.lock
generated
8
flake.lock
generated
@@ -16,11 +16,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1752451292,
|
"lastModified": 1751846468,
|
||||||
"narHash": "sha256-jvLbfYFvcS5f0AEpUlFS2xZRnK770r9TRM2smpUFFaU=",
|
"narHash": "sha256-h0mpWZIOIAKj4fmLNyI2HDG+c0YOkbYmyJXSj/bQ9s0=",
|
||||||
"rev": "309e06fbc9a6d133ab6dd1c7d8e4876526e058bb",
|
"rev": "a2166c13b0cb3febdaf36391cd2019aa2ccf4366",
|
||||||
"type": "tarball",
|
"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": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ let
|
|||||||
package
|
package
|
||||||
path
|
path
|
||||||
str
|
str
|
||||||
strMatching
|
|
||||||
submoduleWith
|
submoduleWith
|
||||||
;
|
;
|
||||||
# the original types.submodule has strange behavior
|
# the original types.submodule has strange behavior
|
||||||
@@ -48,7 +47,7 @@ in
|
|||||||
imports = [ ./generator.nix ];
|
imports = [ ./generator.nix ];
|
||||||
options = {
|
options = {
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = str;
|
type = lib.types.str;
|
||||||
description = ''
|
description = ''
|
||||||
The name of the generator.
|
The name of the generator.
|
||||||
This name will be used to refer to the generator in other generators.
|
This name will be used to refer to the generator in other generators.
|
||||||
@@ -154,7 +153,7 @@ in
|
|||||||
options =
|
options =
|
||||||
{
|
{
|
||||||
name = lib.mkOption {
|
name = lib.mkOption {
|
||||||
type = str;
|
type = lib.types.str;
|
||||||
description = ''
|
description = ''
|
||||||
name of the public fact
|
name of the public fact
|
||||||
'';
|
'';
|
||||||
@@ -163,7 +162,7 @@ in
|
|||||||
defaultText = "Name of the file";
|
defaultText = "Name of the file";
|
||||||
};
|
};
|
||||||
generatorName = lib.mkOption {
|
generatorName = lib.mkOption {
|
||||||
type = str;
|
type = lib.types.str;
|
||||||
description = ''
|
description = ''
|
||||||
name of the generator
|
name of the generator
|
||||||
'';
|
'';
|
||||||
@@ -172,7 +171,7 @@ in
|
|||||||
defaultText = "Name of the generator that generates this file";
|
defaultText = "Name of the generator that generates this file";
|
||||||
};
|
};
|
||||||
share = lib.mkOption {
|
share = lib.mkOption {
|
||||||
type = bool;
|
type = lib.types.bool;
|
||||||
description = ''
|
description = ''
|
||||||
Whether the generated vars should be shared between machines.
|
Whether the generated vars should be shared between machines.
|
||||||
Shared vars are only generated once, when the first machine using it is deployed.
|
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
|
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`.
|
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"
|
"partitioning"
|
||||||
"activation"
|
"activation"
|
||||||
"users"
|
"users"
|
||||||
@@ -252,7 +251,7 @@ in
|
|||||||
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
|
defaultText = lib.literalExpression ''if _class == "darwin" then "wheel" else "root"'';
|
||||||
};
|
};
|
||||||
mode = lib.mkOption {
|
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.";
|
description = "The unix file mode of the file. Must be a 4-digit octal number.";
|
||||||
default = "0400";
|
default = "0400";
|
||||||
};
|
};
|
||||||
@@ -376,7 +375,7 @@ in
|
|||||||
- all required programs are in PATH
|
- all required programs are in PATH
|
||||||
- sandbox is set up correctly
|
- sandbox is set up correctly
|
||||||
'';
|
'';
|
||||||
type = path;
|
type = lib.types.path;
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
internal = true;
|
internal = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -250,12 +250,12 @@ This subcommand allows seamless ssh access to the nixos-image builders or a mach
|
|||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
$ clan ssh berlin
|
$ clan ssh [ssh_args ...] berlin`
|
||||||
|
|
||||||
Will ssh in to the machine called `berlin`, using the
|
Will ssh in to the machine called `berlin`, using the
|
||||||
`clan.core.networking.targetHost` specified in its configuration
|
`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
|
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
|
the json string. [JSON] can either be a json formatted string itself, or point
|
||||||
towards a file containing the deployment information
|
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",
|
description="Manage facts",
|
||||||
epilog=(
|
epilog=(
|
||||||
f"""
|
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.
|
This subcommand provides an interface to facts of clan machines.
|
||||||
Facts are artifacts that a service can generate.
|
Facts are artifacts that a service can generate.
|
||||||
|
|||||||
@@ -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
|
# Only if the caller did not specify a target_host via args.target_host
|
||||||
# Find a suitable target_host that is reachable
|
# Find a suitable target_host that is reachable
|
||||||
target_host_str = args.target_host
|
target_host_str = args.target_host
|
||||||
deploy_info: DeployInfo | None = (
|
deploy_info: DeployInfo | None = ssh_command_parse(args)
|
||||||
ssh_command_parse(args) if target_host_str is None else None
|
|
||||||
)
|
|
||||||
|
|
||||||
use_tor = False
|
use_tor = False
|
||||||
if deploy_info:
|
if deploy_info and not args.target_host:
|
||||||
host = find_reachable_host(deploy_info)
|
host = find_reachable_host(deploy_info)
|
||||||
if host is None or host.tor_socks:
|
if host is None:
|
||||||
use_tor = True
|
use_tor = True
|
||||||
target_host_str = deploy_info.tor.target
|
target_host_str = deploy_info.tor.target
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import textwrap
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from clan_lib.cmd import run
|
from clan_lib.cmd import run
|
||||||
from clan_lib.errors import ClanError
|
from clan_lib.errors import ClanError
|
||||||
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.remote import HostKeyCheck, Remote
|
from clan_lib.ssh.remote import HostKeyCheck, Remote
|
||||||
|
|
||||||
@@ -39,23 +37,20 @@ class DeployInfo:
|
|||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
return addrs[0]
|
return addrs[0]
|
||||||
|
|
||||||
def overwrite_remotes(
|
@staticmethod
|
||||||
self,
|
def from_hostnames(
|
||||||
host_key_check: HostKeyCheck | None = None,
|
hostname: list[str], host_key_check: HostKeyCheck
|
||||||
private_key: Path | None = None,
|
|
||||||
ssh_options: dict[str, str] | None = None,
|
|
||||||
) -> "DeployInfo":
|
) -> "DeployInfo":
|
||||||
"""Return a new DeployInfo with all Remotes overridden with the given host_key_check."""
|
remotes = []
|
||||||
return DeployInfo(
|
for host in hostname:
|
||||||
addrs=[
|
if not host:
|
||||||
addr.override(
|
msg = "Hostname cannot be empty."
|
||||||
host_key_check=host_key_check,
|
raise ClanError(msg)
|
||||||
private_key=private_key,
|
remote = Remote.from_ssh_uri(
|
||||||
ssh_options=ssh_options,
|
machine_name="clan-installer", address=host
|
||||||
)
|
).override(host_key_check=host_key_check)
|
||||||
for addr in self.addrs
|
remotes.append(remote)
|
||||||
]
|
return DeployInfo(addrs=remotes)
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_json(data: dict[str, Any], host_key_check: HostKeyCheck) -> "DeployInfo":
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
def ssh_shell_from_deploy(
|
def ssh_shell_from_deploy(deploy_info: DeployInfo) -> None:
|
||||||
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)
|
|
||||||
|
|
||||||
if host := find_reachable_host(deploy_info):
|
if host := find_reachable_host(deploy_info):
|
||||||
host.interactive_ssh(command)
|
host.interactive_ssh()
|
||||||
return
|
return
|
||||||
|
|
||||||
log.info("Could not reach host via clearnet 'addrs'")
|
log.info("Could not reach host via clearnet 'addrs'")
|
||||||
@@ -145,7 +127,7 @@ def ssh_shell_from_deploy(
|
|||||||
log.info(
|
log.info(
|
||||||
"Host reachable via tor address, starting interactive ssh session."
|
"Host reachable via tor address, starting interactive ssh session."
|
||||||
)
|
)
|
||||||
tor_addr.interactive_ssh(command)
|
tor_addr.interactive_ssh()
|
||||||
return
|
return
|
||||||
|
|
||||||
log.error("Could not reach host via tor address.")
|
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:
|
def ssh_command_parse(args: argparse.Namespace) -> DeployInfo | None:
|
||||||
host_key_check = args.host_key_check
|
host_key_check = args.host_key_check
|
||||||
deploy = None
|
|
||||||
|
|
||||||
if args.json:
|
if args.json:
|
||||||
json_file = Path(args.json)
|
json_file = Path(args.json)
|
||||||
if json_file.is_file():
|
if json_file.is_file():
|
||||||
data = json.loads(json_file.read_text())
|
data = json.loads(json_file.read_text())
|
||||||
return DeployInfo.from_json(data, host_key_check)
|
return DeployInfo.from_json(data, host_key_check)
|
||||||
data = json.loads(args.json)
|
data = json.loads(args.json)
|
||||||
deploy = DeployInfo.from_json(data, host_key_check)
|
return DeployInfo.from_json(data, host_key_check)
|
||||||
elif args.png:
|
if args.png:
|
||||||
deploy = DeployInfo.from_qr_code(Path(args.png), host_key_check)
|
return 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
|
|
||||||
|
|
||||||
ssh_options = None
|
if hasattr(args, "machines"):
|
||||||
if hasattr(args, "ssh_option") and args.ssh_option:
|
return DeployInfo.from_hostnames(args.machines, host_key_check)
|
||||||
for name, value in args.ssh_option:
|
return None
|
||||||
ssh_options = {}
|
|
||||||
ssh_options[name] = value
|
|
||||||
|
|
||||||
deploy = deploy.overwrite_remotes(ssh_options=ssh_options)
|
|
||||||
|
|
||||||
return deploy
|
|
||||||
|
|
||||||
|
|
||||||
def ssh_command(args: argparse.Namespace) -> None:
|
def ssh_command(args: argparse.Namespace) -> None:
|
||||||
@@ -189,63 +155,36 @@ def ssh_command(args: argparse.Namespace) -> None:
|
|||||||
if not deploy_info:
|
if not deploy_info:
|
||||||
msg = "No MACHINE, --json or --png data provided"
|
msg = "No MACHINE, --json or --png data provided"
|
||||||
raise ClanError(msg)
|
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:
|
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
group = parser.add_mutually_exclusive_group()
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
group.add_argument(
|
machines_parser = group.add_argument(
|
||||||
"machine",
|
"machines",
|
||||||
type=str,
|
type=str,
|
||||||
nargs="?",
|
nargs="*",
|
||||||
|
default=[],
|
||||||
metavar="MACHINE",
|
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(
|
group.add_argument(
|
||||||
"-j",
|
"-j",
|
||||||
"--json",
|
"--json",
|
||||||
type=str,
|
help="specify the json file for ssh data (generated by starting the clan installer)",
|
||||||
help=(
|
|
||||||
"Deployment information as a JSON string or path to a JSON file "
|
|
||||||
"(generated by starting the clan installer)."
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"-P",
|
"-P",
|
||||||
"--png",
|
"--png",
|
||||||
type=str,
|
help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer)",
|
||||||
help="Deployment information as a QR code image file (generated by starting the clan installer).",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--host-key-check",
|
"--host-key-check",
|
||||||
choices=["strict", "ask", "tofu", "none"],
|
choices=["strict", "ask", "tofu", "none"],
|
||||||
default="tofu",
|
default="tofu",
|
||||||
help="Host key (.ssh/known_hosts) check mode.",
|
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)
|
parser.set_defaults(func=ssh_command)
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ from clan_lib.nix import nix_shell
|
|||||||
from clan_lib.ssh.remote import Remote
|
from clan_lib.ssh.remote import Remote
|
||||||
|
|
||||||
from clan_cli.ssh.deploy_info import DeployInfo, find_reachable_host
|
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:
|
def test_qrcode_scan(temp_dir: Path) -> None:
|
||||||
@@ -71,10 +69,7 @@ def test_from_json() -> None:
|
|||||||
@pytest.mark.with_core
|
@pytest.mark.with_core
|
||||||
def test_find_reachable_host(hosts: list[Remote]) -> None:
|
def test_find_reachable_host(hosts: list[Remote]) -> None:
|
||||||
host = hosts[0]
|
host = hosts[0]
|
||||||
|
deploy_info = DeployInfo.from_hostnames(["172.19.1.2", host.ssh_url()], "none")
|
||||||
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)
|
|
||||||
|
|
||||||
assert deploy_info.addrs[0].address == "172.19.1.2"
|
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 is not None
|
||||||
assert remote.ssh_url() == host.ssh_url()
|
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()
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import textwrap
|
|
||||||
from dataclasses import asdict, dataclass, field
|
from dataclasses import asdict, dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
@@ -589,7 +588,7 @@ class FlakeCache:
|
|||||||
|
|
||||||
def load_from_file(self, path: Path) -> None:
|
def load_from_file(self, path: Path) -> None:
|
||||||
with path.open("r") as f:
|
with path.open("r") as f:
|
||||||
log.debug("Loading flake cache from file")
|
log.debug(f"Loading cache from {path}")
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
self.cache = FlakeCacheEntry.from_json(data["cache"])
|
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
|
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 (
|
from clan_lib.nix import (
|
||||||
nix_command,
|
nix_command,
|
||||||
)
|
)
|
||||||
@@ -682,10 +681,7 @@ class Flake:
|
|||||||
self.identifier,
|
self.identifier,
|
||||||
]
|
]
|
||||||
|
|
||||||
trace_prefetch = os.environ.get("CLAN_DEBUG_NIX_PREFETCH", "0") == "1"
|
flake_prefetch = run(nix_command(cmd))
|
||||||
if not trace_prefetch:
|
|
||||||
log.debug(f"Prefetching flake {self.identifier}")
|
|
||||||
flake_prefetch = run(nix_command(cmd), RunOpts(trace=trace_prefetch))
|
|
||||||
flake_metadata = json.loads(flake_prefetch.stdout)
|
flake_metadata = json.loads(flake_prefetch.stdout)
|
||||||
self.store_path = flake_metadata["storePath"]
|
self.store_path = flake_metadata["storePath"]
|
||||||
self.hash = flake_metadata["hash"]
|
self.hash = flake_metadata["hash"]
|
||||||
@@ -702,6 +698,8 @@ class Flake:
|
|||||||
nix_metadata,
|
nix_metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
log.debug(f"Invalidating cache for {self.identifier}")
|
||||||
|
|
||||||
self.prefetch()
|
self.prefetch()
|
||||||
|
|
||||||
self._cache = FlakeCache()
|
self._cache = FlakeCache()
|
||||||
@@ -815,42 +813,36 @@ class Flake:
|
|||||||
];
|
];
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
if len(selectors) > 1 :
|
if len(selectors) > 1:
|
||||||
msg = textwrap.dedent(f"""
|
log.debug(f"""
|
||||||
clan select "{selectors}"
|
selecting: {selectors}
|
||||||
""").lstrip("\n").rstrip("\n")
|
to debug run:
|
||||||
if os.environ.get("CLAN_DEBUG_NIX_SELECTORS"):
|
nix repl --expr 'rec {{
|
||||||
msg += textwrap.dedent(f"""
|
flake = builtins.getFlake "self.identifier";
|
||||||
to debug run:
|
selectLib = (builtins.getFlake "path:{select_source()}?narHash={select_hash}").lib;
|
||||||
nix repl --expr 'rec {{
|
query = [
|
||||||
flake = builtins.getFlake "{self.identifier}";
|
{" ".join(
|
||||||
selectLib = (builtins.getFlake "path:{select_source()}?narHash={select_hash}").lib;
|
[
|
||||||
query = [
|
f"(selectLib.select ''{selector}'' flake)"
|
||||||
{" ".join(
|
for selector in selectors
|
||||||
[
|
]
|
||||||
f"(selectLib.select ''{selector}'' flake)"
|
)}
|
||||||
for selector in selectors
|
];
|
||||||
]
|
}}'
|
||||||
)}
|
""")
|
||||||
];
|
|
||||||
}}'
|
|
||||||
""").lstrip("\n")
|
|
||||||
log.debug(msg)
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
elif len(selectors) == 1:
|
elif len(selectors) == 1:
|
||||||
msg = textwrap.dedent(f"""
|
log.debug(
|
||||||
$ clan select "{selectors[0]}"
|
f"""
|
||||||
""").lstrip("\n").rstrip("\n")
|
selecting: {selectors[0]}
|
||||||
if os.environ.get("CLAN_DEBUG_NIX_SELECTORS"):
|
to debug run:
|
||||||
msg += textwrap.dedent(f"""
|
nix repl --expr 'rec {{
|
||||||
to debug run:
|
flake = builtins.getFlake "{self.identifier}";
|
||||||
nix repl --expr 'rec {{
|
selectLib = (builtins.getFlake "path:{select_source()}?narHash={select_hash}").lib;
|
||||||
flake = builtins.getFlake "{self.identifier}";
|
query = selectLib.select '"''{selectors[0]}''"' flake;
|
||||||
selectLib = (builtins.getFlake "path:{select_source()}?narHash={select_hash}").lib;
|
}}'
|
||||||
query = selectLib.select '"''{selectors[0]}''"' flake;
|
"""
|
||||||
}}'
|
)
|
||||||
""").lstrip("\n")
|
|
||||||
log.debug(msg)
|
|
||||||
|
|
||||||
build_output = Path(
|
build_output = Path(
|
||||||
run(
|
run(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from tempfile import TemporaryDirectory
|
|||||||
from clan_lib.api import API
|
from clan_lib.api import API
|
||||||
from clan_lib.cmd import ClanCmdError, ClanCmdTimeoutError, CmdOut, RunOpts, run
|
from clan_lib.cmd import ClanCmdError, ClanCmdTimeoutError, CmdOut, RunOpts, run
|
||||||
from clan_lib.colors import AnsiColor
|
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.nix import nix_shell
|
||||||
from clan_lib.ssh.host_key import HostKeyCheck, hostkey_to_ssh_opts
|
from clan_lib.ssh.host_key import HostKeyCheck, hostkey_to_ssh_opts
|
||||||
from clan_lib.ssh.parse import parse_ssh_uri
|
from clan_lib.ssh.parse import parse_ssh_uri
|
||||||
@@ -61,9 +61,6 @@ class Remote:
|
|||||||
private_key: Path | None = None,
|
private_key: Path | None = None,
|
||||||
password: str | None = None,
|
password: str | None = None,
|
||||||
tor_socks: bool | None = None,
|
tor_socks: bool | None = None,
|
||||||
command_prefix: str | None = None,
|
|
||||||
port: int | None = None,
|
|
||||||
ssh_options: dict[str, str] | None = None,
|
|
||||||
) -> "Remote":
|
) -> "Remote":
|
||||||
"""
|
"""
|
||||||
Returns a new Remote instance with the same data but with a different host_key_check.
|
Returns a new Remote instance with the same data but with a different host_key_check.
|
||||||
@@ -71,8 +68,8 @@ class Remote:
|
|||||||
return Remote(
|
return Remote(
|
||||||
address=self.address,
|
address=self.address,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
command_prefix=command_prefix or self.command_prefix,
|
command_prefix=self.command_prefix,
|
||||||
port=port or self.port,
|
port=self.port,
|
||||||
private_key=private_key if private_key is not None else self.private_key,
|
private_key=private_key if private_key is not None else self.private_key,
|
||||||
password=password if password is not None else self.password,
|
password=password if password is not None else self.password,
|
||||||
forward_agent=self.forward_agent,
|
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
|
host_key_check if host_key_check is not None else self.host_key_check
|
||||||
),
|
),
|
||||||
verbose_ssh=self.verbose_ssh,
|
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,
|
tor_socks=tor_socks if tor_socks is not None else self.tor_socks,
|
||||||
_control_path_dir=self._control_path_dir,
|
_control_path_dir=self._control_path_dir,
|
||||||
_askpass_path=self._askpass_path,
|
_askpass_path=self._askpass_path,
|
||||||
@@ -421,31 +418,11 @@ class Remote:
|
|||||||
msg = f"SSH command failed with return code {res.returncode}"
|
msg = f"SSH command failed with return code {res.returncode}"
|
||||||
raise ClanError(msg)
|
raise ClanError(msg)
|
||||||
|
|
||||||
def interactive_ssh(self, command: list[str] | None = None) -> None:
|
def interactive_ssh(self) -> None:
|
||||||
ssh_cmd = self.ssh_cmd(tty=True, control_master=False)
|
cmd_list = self.ssh_cmd(tty=True, control_master=False)
|
||||||
if command:
|
res = subprocess.run(cmd_list, check=False)
|
||||||
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)
|
|
||||||
|
|
||||||
# We only check the error code if a password is set, as sshpass is used.
|
self.check_sshpass_errorcode(res)
|
||||||
# AS sshpass swallows all output.
|
|
||||||
if self.password:
|
|
||||||
self.check_sshpass_errorcode(res)
|
|
||||||
|
|
||||||
def check_machine_ssh_reachable(self) -> bool:
|
def check_machine_ssh_reachable(self) -> bool:
|
||||||
return check_machine_ssh_reachable(self).ok
|
return check_machine_ssh_reachable(self).ok
|
||||||
@@ -454,7 +431,7 @@ class Remote:
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ConnectionOptions:
|
class ConnectionOptions:
|
||||||
timeout: int = 2
|
timeout: int = 2
|
||||||
retries: int = 5
|
retries: int = 10
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -527,10 +504,6 @@ def check_machine_ssh_reachable(
|
|||||||
if opts is None:
|
if opts is None:
|
||||||
opts = ConnectionOptions()
|
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
|
address_family = socket.AF_INET6 if ":" in remote.address else socket.AF_INET
|
||||||
for _ in range(opts.retries):
|
for _ in range(opts.retries):
|
||||||
with socket.socket(address_family, socket.SOCK_STREAM) as sock:
|
with socket.socket(address_family, socket.SOCK_STREAM) as sock:
|
||||||
|
|||||||
15
renovate.json
Normal file
15
renovate.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user