1
0
forked from clan/clan-core

Compare commits

..

1 Commits

Author SHA1 Message Date
Clan Merge Bot
58a5e51192 update flake lock - 2024-05-06T00:00+00:00
Flake lock file updates:

• Updated input 'disko':
    'github:nix-community/disko/285e26465a0bae510897ca04da26ce6307c652b4' (2024-04-26)
  → 'github:nix-community/disko/d57058eb09dd5ec00c746df34fe0a603ea744370' (2024-05-02)
• Updated input 'flake-parts':
    'github:hercules-ci/flake-parts/9126214d0a59633752a136528f5f3b9aa8565b7d' (2024-04-01)
  → 'github:hercules-ci/flake-parts/e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e' (2024-05-02)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/30ddacc06345a478f9528fa29e2c8857b90381b2' (2024-04-28)
  → 'github:NixOS/nixpkgs/9f5a6d72fa3985e4cd8fca3926d14ae8b54bcf75' (2024-05-05)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/f1b0adc27265274e3b0c9b872a8f476a098679bd' (2024-04-23)
  → 'github:Mic92/sops-nix/10dc39496d5b027912038bde8d68c836576ad0bc' (2024-05-05)
2024-05-06 00:00:20 +00:00
171 changed files with 1114 additions and 10178 deletions

8
.gitignore vendored
View File

@ -13,9 +13,6 @@ nixos.qcow2
**/*.glade~
/docs/out
# dream2nix
.dream2nix
# python
__pycache__
.coverage
@ -31,8 +28,3 @@ build
build-dir
repo
.env
# node
node_modules
dist
.webui

View File

@ -1,4 +1,4 @@
# Contributing to Clan
# Contributing to cLAN
## Live-reloading documentation

View File

@ -1,6 +1,6 @@
# Clan Core Repository
Welcome to the Clan Core Repository, the heart of the [clan.lol](https://clan.lol/) project! This monorepo is the foundation of Clan, a revolutionary open-source project aimed at restoring fun, freedom, and functionality to computing. Here, you'll find all the essential packages, NixOS modules, CLI tools, and tests needed to contribute to and work with the Clan project. Clan leverages the Nix system to ensure reliability, security, and seamless management of digital environments, putting the power back into the hands of users.
Welcome to the Clan Core Repository, the heart of the [clan.lol](https://clan.lol/) project! This monorepo is the foundation of Clan, a revolutionary open-source project aimed at restoring fun, freedom, and functionality to computing. Here, you'll find all the essential packages, NixOS modules, CLI tools, and tests needed to contribute to and work with the cLAN project. Clan leverages the Nix system to ensure reliability, security, and seamless management of digital environments, putting the power back into the hands of users.
## Why Clan?
@ -40,6 +40,6 @@ Clan is more than a tool; it's a movement towards a better digital future. By co
Connect with us and the Clan community for support and discussion:
- [Matrix channel](https://matrix.to/#/#clan:lassul.us) for live discussions.
- [Matrix channel](https://matrix.to/#/!djzOHBBBHnwQkgNgdV:matrix.org?via=blog.clan.lol) for live discussions.
- IRC bridges (coming soon) for real-time chat support.

View File

@ -145,14 +145,14 @@
machine.succeed("echo testing > /var/test-backups/somefile")
# create
machine.succeed("clan backups create --debug --flake ${self} test-backup")
machine.succeed("clan --debug --flake ${self} backups create test-backup")
machine.wait_until_succeeds("! systemctl is-active borgbackup-job-test-backup >&2")
machine.succeed("test -f /run/mount-external-disk")
machine.succeed("test -f /run/unmount-external-disk")
# list
backup_id = json.loads(machine.succeed("borg-job-test-backup list --json"))["archives"][0]["archive"]
out = machine.succeed("clan backups list --debug --flake ${self} test-backup").strip()
out = machine.succeed("clan --debug --flake ${self} backups list test-backup").strip()
print(out)
assert backup_id in out, f"backup {backup_id} not found in {out}"
localbackup_id = "hdd::/mnt/external-disk/snapshot.0"
@ -160,14 +160,14 @@
## borgbackup restore
machine.succeed("rm -f /var/test-backups/somefile")
machine.succeed(f"clan backups restore --debug --flake ${self} test-backup borgbackup 'test-backup::borg@machine:.::{backup_id}' >&2")
machine.succeed(f"clan --debug --flake ${self} backups restore test-backup borgbackup 'test-backup::borg@machine:.::{backup_id}' >&2")
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
machine.succeed("test -f /var/test-service/pre-restore-command")
machine.succeed("test -f /var/test-service/post-restore-command")
## localbackup restore
machine.succeed("rm -f /var/test-backups/somefile /var/test-service/{pre,post}-restore-command")
machine.succeed(f"clan backups restore --debug --flake ${self} test-backup localbackup '{localbackup_id}' >&2")
machine.succeed(f"clan --debug --flake ${self} backups restore test-backup localbackup '{localbackup_id}' >&2")
assert machine.succeed("cat /var/test-backups/somefile").strip() == "testing", "restore failed"
machine.succeed("test -f /var/test-service/pre-restore-command")
machine.succeed("test -f /var/test-service/post-restore-command")

View File

@ -9,16 +9,16 @@
}:
let
dependencies = [
pkgs.disko
self
pkgs.stdenv.drvPath
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.toplevel
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.diskoScript
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.build.diskoScript.drvPath
self.clanInternals.machines.${pkgs.hostPlatform.system}.test_install_machine.config.system.clan.deployment.file
self.inputs.nixpkgs.legacyPackages.${pkgs.hostPlatform.system}.disko
] ++ builtins.map (i: i.outPath) (builtins.attrValues self.inputs);
closureInfo = pkgs.closureInfo { rootPaths = dependencies; };
in
{
# Currently disabled...
checks = pkgs.lib.mkIf (pkgs.stdenv.isLinux) {
flash = (import ../lib/test-base.nix) {
name = "flash";
@ -41,8 +41,7 @@
};
testScript = ''
start_all()
machine.succeed("clan flash --debug --flake ${../..} --yes --disk main /dev/vdb test_install_machine")
machine.succeed("clan --flake ${../..} flash --debug --yes --disk main /dev/vdb test_install_machine")
'';
} { inherit pkgs self; };
};

View File

@ -7,8 +7,6 @@
#!${pkgs.bash}/bin/bash
set -euo pipefail
unset CLAN_DIR
export PATH="${
lib.makeBinPath [
pkgs.gitMinimal

View File

@ -2,8 +2,8 @@
{
clan.machines.test_install_machine = {
clan.networking.targetHost = "test_install_machine";
fileSystems."/".device = lib.mkDefault "/dev/vdb";
boot.loader.grub.device = lib.mkDefault "/dev/vdb";
fileSystems."/".device = lib.mkDefault "/dev/null";
boot.loader.grub.device = lib.mkDefault "/dev/null";
imports = [ self.nixosModules.test_install_machine ];
};
@ -98,7 +98,7 @@
client.succeed("${pkgs.coreutils}/bin/install -Dm 600 ${../lib/ssh/privkey} /root/.ssh/id_ed25519")
client.wait_until_succeeds("ssh -o StrictHostKeyChecking=accept-new -v root@target hostname")
client.succeed("clan machines install --debug --flake ${../..} --yes test_install_machine root@target >&2")
client.succeed("clan --debug --flake ${../..} machines install --yes test_install_machine root@target >&2")
try:
target.shutdown()
except BrokenPipeError:

View File

@ -10,7 +10,6 @@ in
hostPkgs = pkgs;
# speed-up evaluation
defaults = {
nix.package = pkgs.nixVersions.latest;
documentation.enable = lib.mkDefault false;
boot.isContainer = true;

View File

@ -10,7 +10,6 @@ in
defaults = {
documentation.enable = lib.mkDefault false;
nix.settings.min-free = 0;
nix.package = pkgs.nixVersions.latest;
};
# to accept external dependencies such as disko

View File

@ -20,7 +20,6 @@
boot = {
size = "1M";
type = "EF02"; # for grub MBR
priority = 1;
};
ESP = {
size = "512M";

View File

@ -1,26 +1,25 @@
{ ... }:
{ inputs, ... }:
{
flake.clanModules = {
disk-layouts = {
imports = [ ./disk-layouts ];
imports = [
./disk-layouts
inputs.disko.nixosModules.default
];
};
borgbackup = ./borgbackup;
deltachat = ./deltachat;
ergochat = ./ergochat;
deltachat = ./deltachat;
localbackup = ./localbackup;
localsend = ./localsend;
matrix-synapse = ./matrix-synapse;
moonlight = ./moonlight;
root-password = ./root-password;
sshd = ./sshd;
sunshine = ./sunshine;
static-hosts = ./static-hosts;
syncthing = ./syncthing;
root-password = ./root-password;
thelounge = ./thelounge;
trusted-nix-caches = ./trusted-nix-caches;
user-password = ./user-password;
xfce = ./xfce;
zerotier-static-peers = ./zerotier-static-peers;
zt-tcp-relay = ./zt-tcp-relay;
};
}

View File

@ -9,7 +9,7 @@
# - cli frontend: https://github.com/localsend/localsend/issues/11
# - ipv6 support: https://github.com/localsend/localsend/issues/549
options.clan.localsend = {
enable = lib.mkEnableOption "enable the localsend module";
enable = lib.mkEnableOption (lib.mdDoc "enable the localsend module");
defaultLocation = lib.mkOption {
type = lib.types.str;
description = "The default download location";

View File

@ -1,7 +1,6 @@
{ config, pkgs, ... }:
{
services.openssh.enable = true;
services.openssh.settings.PasswordAuthentication = false;
services.openssh.hostKeys = [
{

View File

@ -1,2 +0,0 @@
Statically configure the host names of machines based on their respective zerotier-ip.
---

View File

@ -1,44 +0,0 @@
{ lib, config, ... }:
{
options.clan.static-hosts = {
excludeHosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default =
if config.clan.static-hosts.topLevelDomain != "" then [ ] else [ config.clanCore.machineName ];
description = "Hosts that should be excluded";
};
topLevelDomain = lib.mkOption {
type = lib.types.str;
default = "";
description = "Top level domain to reach hosts";
};
};
config.networking.hosts =
let
clanDir = config.clanCore.clanDir;
machineDir = clanDir + "/machines/";
zerotierIpMachinePath = machines: machineDir + machines + "/facts/zerotier-ip";
machines = builtins.readDir machineDir;
filteredMachines = lib.filterAttrs (
name: _: !(lib.elem name config.clan.static-hosts.excludeHosts)
) machines;
in
lib.filterAttrs (_: value: value != null) (
lib.mapAttrs' (
machine: _:
let
path = zerotierIpMachinePath machine;
in
if builtins.pathExists path then
lib.nameValuePair (builtins.readFile path) (
if (config.clan.static-hosts.topLevelDomain == "") then
[ machine ]
else
[ "${machine}.${config.clan.static-hosts.topLevelDomain}" ]
)
else
null
) filteredMachines
);
}

View File

@ -1,2 +0,0 @@
This module sets the `clan.lol` and `nix-community` cache up as a trusted cache.
----

View File

@ -1,10 +0,0 @@
{
nix.settings.trusted-substituters = [
"https://cache.clan.lol"
"https://nix-community.cachix.org"
];
nix.settings.trusted-public-keys = [
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
"cache.clan.lol-1:3KztgSAB5R1M+Dz7vzkBGzXdodizbgLXGXKXlcQLA28="
];
}

View File

@ -1,18 +0,0 @@
Automatically generates and configures a password for the specified user account.
---
If setting the option prompt to true, the user will be prompted to type in their desired password.
!!! Note
This module will set `mutableUsers` to `false`, meaning you can not manage user passwords through `passwd` anymore.
After the system was installed/deployed the following command can be used to display the user-password:
```bash
clan secrets get {machine_name}-user-password
```
See also: [Facts / Secrets](../../getting-started/secrets.md)
To regenerate the password, delete the password files in the clan directory and redeploy the machine.

View File

@ -1,49 +0,0 @@
{
pkgs,
config,
lib,
...
}:
{
options.clan.user-password = {
user = lib.mkOption {
type = lib.types.str;
example = "alice";
description = "The user the password should be generated for.";
};
prompt = lib.mkOption {
type = lib.types.bool;
default = true;
example = false;
description = "Whether the user should be prompted.";
};
};
config = {
users.mutableUsers = false;
users.users.${config.clan.user-password.user}.hashedPasswordFile =
config.clanCore.facts.services.user-password.secret.user-password-hash.path;
sops.secrets."${config.clanCore.machineName}-user-password-hash".neededForUsers = true;
clanCore.facts.services.user-password = {
secret.user-password = { };
secret.user-password-hash = { };
generator.prompt = (
lib.mkIf config.clan.user-password.prompt "Set the password for your $user: ${config.clan.user-password.user}.
You can autogenerate a password, if you leave this prompt blank."
);
generator.path = with pkgs; [
coreutils
xkcdpass
mkpasswd
];
generator.script = ''
if [[ -n $prompt_value ]]; then
echo $prompt_value > $secrets/user-password
else
xkcdpass --numwords 3 --delimiter - --count 1 > $secrets/user-password
fi
cat $secrets/user-password | mkpasswd -s -m sha-512 > $secrets/user-password-hash
'';
};
};
}

View File

@ -1,5 +0,0 @@
Statically configure the `zerotier` peers of a clan network.
---
Statically configure the `zerotier` peers of a clan network.
Requires a machine, that is the zerotier controller configured in the network.

View File

@ -1,71 +0,0 @@
{
lib,
config,
pkgs,
inputs,
...
}:
let
clanDir = config.clanCore.clanDir;
machineDir = clanDir + "/machines/";
machinesFileSet = builtins.readDir machineDir;
machines = lib.mapAttrsToList (name: _: name) machinesFileSet;
zerotierNetworkIdPath = machines: machineDir + machines + "/facts/zerotier-network-id";
networkIdsUnchecked = builtins.map (
machine:
let
fullPath = zerotierNetworkIdPath machine;
in
if builtins.pathExists fullPath then builtins.readFile fullPath else null
) machines;
networkIds = lib.filter (machine: machine != null) networkIdsUnchecked;
networkId = if builtins.length networkIds == 0 then null else builtins.elemAt networkIds 0;
in
#TODO:trace on multiple found network-ids
#TODO:trace on no single found networkId
{
options.clan.zerotier-static-peers = {
excludeHosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ config.clanCore.machineName ];
description = "Hosts that should be excluded";
};
};
config.systemd.services.zerotier-static-peers-autoaccept =
let
machines = builtins.readDir machineDir;
zerotierIpMachinePath = machines: machineDir + machines + "/facts/zerotier-ip";
filteredMachines = lib.filterAttrs (
name: _: !(lib.elem name config.clan.zerotier-static-peers.excludeHosts)
) machines;
hosts = lib.mapAttrsToList (host: _: host) (
lib.mapAttrs' (
machine: _:
let
fullPath = zerotierIpMachinePath machine;
in
if builtins.pathExists fullPath then
lib.nameValuePair (builtins.readFile fullPath) [ machine ]
else
null
) filteredMachines
);
in
lib.mkIf (config.clan.networking.zerotier.controller.enable) {
wantedBy = [ "multi-user.target" ];
after = [ "zerotierone.service" ];
path = [ pkgs.zerotierone ];
serviceConfig.ExecStart = pkgs.writeScript "static-zerotier-peers-autoaccept" ''
#!/bin/sh
${lib.concatMapStringsSep "\n" (host: ''
${
inputs.clan-core.packages.${pkgs.system}.zerotier-members
}/bin/zerotier-members allow --member-ip ${host}
'') hosts}
'';
};
config.clan.networking.zerotier.networkId = lib.mkDefault networkId;
}

View File

@ -1,4 +1,3 @@
{ ... }:
{
perSystem =
{
@ -27,8 +26,7 @@
packages = [
select-shell
pkgs.tea
# Better error messages than nix 2.18
pkgs.nixVersions.latest
pkgs.nix
self'.packages.tea-create-pr
self'.packages.merge-after-ci
self'.packages.pending-reviews
@ -36,6 +34,9 @@
config.treefmt.build.wrapper
];
shellHook = ''
# no longer used
rm -f "$(git rev-parse --show-toplevel)/.git/hooks/pre-commit"
echo -e "${ansiEscapes.green}switch to another dev-shell using: select-shell${ansiEscapes.reset}"
'';
};

View File

@ -1,6 +1,6 @@
source_up
watch_file $(find ./nix -name "*.nix" -printf '%p ')
watch_file $(find ./nix -name "*.nix" -printf '"%p" ')
# Because we depend on nixpkgs sources, uploading to builders takes a long time
use flake .#docs --builders ''

4
docs/.gitignore vendored
View File

@ -1,3 +1 @@
/site/reference
/site/static/Roboto-Regular.ttf
/site/static/FiraCode-VF.ttf
/site/reference

View File

@ -15,124 +15,92 @@ Let's get your development environment up and running:
1. **Install Nix Package Manager**:
- You can install the Nix package manager by either [downloading the Nix installer](https://github.com/DeterminateSystems/nix-installer/releases) or running this command:
```bash
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```
- You can install the Nix package manager by either [downloading the Nix installer](https://github.com/DeterminateSystems/nix-installer/releases) or running this command:
```bash
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```
2. **Install direnv**:
- To automatically setup a devshell on entering the directory
```bash
nix profile install nixpkgs#nix-direnv-flakes
```
- Download the direnv package from [here](https://direnv.net/docs/installation.html) or run the following command:
```bash
curl -sfL https://direnv.net/install.sh | bash
```
3. **Add direnv to your shell**:
- Direnv needs to [hook into your shell](https://direnv.net/docs/hook.html) to work.
You can do this by executing following command. The example below will setup direnv for `zsh` and `bash`
- Direnv needs to [hook into your shell](https://direnv.net/docs/hook.html) to work.
You can do this by executing following command. The example below will setup direnv for `zsh` and `bash`
```bash
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL"
```
4. **Clone the Repository and Navigate**:
- Clone this repository and navigate to it.
5. **Allow .envrc**:
- When you enter the directory, you'll receive an error message like this:
```bash
direnv: error .envrc is blocked. Run `direnv allow` to approve its content
```
- Execute `direnv allow` to automatically execute the shell script `.envrc` when entering the directory.
# Setting Up Your Git Workflow
Let's set up your Git workflow to collaborate effectively:
1. **Register Your Gitea Account Locally**:
- Execute the following command to add your Gitea account locally:
```bash
tea login add
```
- Fill out the prompt as follows:
- URL of Gitea instance: `https://git.clan.lol`
- Name of new Login [gitea.gchq.icu]: `gitea.gchq.icu:7171`
- Do you have an access token? No
- Username: YourUsername
- Password: YourPassword
- Set Optional settings: No
2. **Git Workflow**:
1. Add your changes to Git using `git add <file1> <file2>`.
2. Run `nix fmt` to lint your files.
3. Commit your changes with a descriptive message: `git commit -a -m "My descriptive commit message"`.
4. Make sure your branch has the latest changes from upstream by executing:
```bash
echo 'eval "$(direnv hook zsh)"' >> ~/.zshrc && echo 'eval "$(direnv hook bash)"' >> ~/.bashrc && eval "$SHELL"
git fetch && git rebase origin/main --autostash
```
5. Use `git status` to check for merge conflicts.
6. If conflicts exist, resolve them. Here's a tutorial for resolving conflicts in [VSCode](https://code.visualstudio.com/docs/sourcecontrol/overview#_merge-conflicts).
7. After resolving conflicts, execute `git merge --continue` and repeat step 5 until there are no conflicts.
4. **Create a Gitea Account**:
- Register an account on https://git.clan.lol
- Fork the [clan-core](https://git.clan.lol/clan/clan-core) repository
- Clone the repository and navigate to it
- Add a new remote called upstream:
```bash
git remote add upstream gitea@git.clan.lol:clan/clan-core.git
```
3. **Create a Pull Request**:
5. **Register Your Gitea Account Locally**:
- To automatically open a pull request that gets merged if all tests pass, execute:
```bash
merge-after-ci
```
- Execute the following command to add your Gitea account locally:
```bash
tea login add
```
- Fill out the prompt as follows:
- URL of Gitea instance: `https://git.clan.lol`
- Name of new Login [git.clan.lol]:
- Do you have an access token? No
- Username: YourUsername
- Password: YourPassword
- Set Optional settings: No
4. **Review Your Pull Request**:
- Visit https://git.clan.lol and go to the project page. Check under "Pull Requests" for any issues with your pull request.
6. **Allow .envrc**:
- When you enter the directory, you'll receive an error message like this:
```bash
direnv: error .envrc is blocked. Run `direnv allow` to approve its content
```
- Execute `direnv allow` to automatically execute the shell script `.envrc` when entering the directory.
7. **(Optional) Install Git Hooks**:
- To syntax check your code you can run:
```bash
nix fmt
```
- To make this automatic install the git hooks
```bash
./scripts/pre-commit
```
8. **Open a Pull Request**:
- To automatically open up a pull request you can use our tool called:
```
merge-after-ci --reviewers Mic92 Lassulus Qubasa
```
5. **Push Your Changes**:
- If there are issues, fix them and redo step 2. Afterward, execute:
```bash
git push origin HEAD:YourUsername-main
```
- This will directly push to your open pull request.
# Debugging
Here are some methods for debugging and testing the clan-cli:
## See all possible packages and tests
To quickly show all possible packages and tests execute:
```bash
nix flake show --system no-eval
```
Under `checks` you will find all tests that are executed in our CI. Under `packages` you find all our projects.
```
git+file:///home/lhebendanz/Projects/clan-core
├───apps
│ └───x86_64-linux
│ ├───install-vm: app
│ └───install-vm-nogui: app
├───checks
│ └───x86_64-linux
│ ├───borgbackup omitted (use '--all-systems' to show)
│ ├───check-for-breakpoints omitted (use '--all-systems' to show)
│ ├───clan-dep-age omitted (use '--all-systems' to show)
│ ├───clan-dep-bash omitted (use '--all-systems' to show)
│ ├───clan-dep-e2fsprogs omitted (use '--all-systems' to show)
│ ├───clan-dep-fakeroot omitted (use '--all-systems' to show)
│ ├───clan-dep-git omitted (use '--all-systems' to show)
│ ├───clan-dep-nix omitted (use '--all-systems' to show)
│ ├───clan-dep-openssh omitted (use '--all-systems' to show)
│ ├───"clan-dep-python3.11-mypy" omitted (use '--all-systems' to show)
├───packages
│ └───x86_64-linux
│ ├───clan-cli omitted (use '--all-systems' to show)
│ ├───clan-cli-docs omitted (use '--all-systems' to show)
│ ├───clan-ts-api omitted (use '--all-systems' to show)
│ ├───clan-vm-manager omitted (use '--all-systems' to show)
│ ├───default omitted (use '--all-systems' to show)
│ ├───deploy-docs omitted (use '--all-systems' to show)
│ ├───docs omitted (use '--all-systems' to show)
│ ├───editor omitted (use '--all-systems' to show)
└───templates
├───default: template: Initialize a new clan flake
└───new-clan: template: Initialize a new clan flake
```
You can execute every test separately by following the tree path `nix build .#checks.x86_64-linux.clan-pytest` for example.
## Test Locally in Devshell with Breakpoints
To test the cli locally in a development environment and set breakpoints for debugging, follow these steps:
@ -182,14 +150,12 @@ If you need to inspect the Nix sandbox while running tests, follow these steps:
2. Use `cntr` and `psgrep` to attach to the Nix sandbox. This allows you to interactively debug your code while it's paused. For example:
```bash
cntr exec -w your_sandbox_name
psgrep -a -x your_python_process_name
cntr attach <container id, container name or process id>
```
Or you can also use the [nix breakpoint hook](https://nixos.org/manual/nixpkgs/stable/#breakpointhook)
# Standards
- Every new module name should be in kebab-case.
- Every fact definition, where possible should be in kebab-case.
Every new module name should be in kebab-case.
Every fact definition, where possible should be in kebab-case.

View File

@ -20,11 +20,11 @@ There are several reasons for choosing to self-host. These can include:
Alice wants to self-host a mumble server for her family.
- She visits to the Clan website, and follows the instructions on how to install Clan-OS on her server.
- Alice logs into a terminal on her server via SSH (alternatively uses Clan GUI app)
- Using the Clan CLI or GUI tool, alice creates a new private network for her family (VPN)
- Alice now browses a list of curated Clan modules and finds a module for mumble.
- She adds this module to her network using the Clan tool.
- She visits to the cLAN website, and follows the instructions on how to install cLAN-OS on her server.
- Alice logs into a terminal on her server via SSH (alternatively uses cLAN GUI app)
- Using the cLAN CLI or GUI tool, alice creates a new private network for her family (VPN)
- Alice now browses a list of curated cLAN modules and finds a module for mumble.
- She adds this module to her network using the cLAN tool.
- After that, she uses the clan tool to invite her family members to her network
- Other family members join the private network via the invitation.
- By accepting the invitation, other members automatically install all required software to interact with the network on their machine.
@ -33,7 +33,7 @@ Alice wants to self-host a mumble server for her family.
Alice wants to add a photos app to her private network
- She uses the clan CLI or GUI tool to manage her existing private Clan family network
- She uses the clan CLI or GUI tool to manage her existing private cLAN family network
- She discovers a module for photoprism, and adds it to her server using the tool
- Other members who are already part of her network, will receive a notification that an update is required to their environment
- After accepting, all new software and services to interact with the new photoprism service will be installed automatically.

View File

@ -1,4 +1,4 @@
# Joining a Clan network
# Joining a cLAN network
## General Description
@ -8,13 +8,13 @@ Joining a self-hosted infrastructure involves connecting to a network, server, o
### Story 1: Joining a private network
Alice' son Bob has never heard of Clan, but receives an invitation URL from Alice who already set up private Clan network for her family.
Alice' son Bob has never heard of cLAN, but receives an invitation URL from Alice who already set up private cLAN network for her family.
Bob opens the invitation link and lands on the Clan website. He quickly learns about what Clan is and can see that the invitation is for a private network of his family that hosts a number of services, like a private voice chat and a photo sharing platform.
Bob opens the invitation link and lands on the cLAN website. He quickly learns about what cLAN is and can see that the invitation is for a private network of his family that hosts a number of services, like a private voice chat and a photo sharing platform.
Bob decides to join the network and follows the instructions to install the Clan tool on his computer.
Bob decides to join the network and follows the instructions to install the cLAN tool on his computer.
Feeding the invitation link to the Clan tool, bob registers his machine with the network.
Feeding the invitation link to the cLAN tool, bob registers his machine with the network.
All programs required to interact with the network will be installed and configured automatically and securely.
@ -22,7 +22,7 @@ Optionally, bob can customize the configuration of these programs through a simp
### Story 2: Receiving breaking changes
The Clan family network which Bob is part of received an update.
The cLAN family network which Bob is part of received an update.
The existing photo sharing service has been removed and replaced with another alternative service. The new photo sharing service requires a different client app to view and upload photos.
@ -30,7 +30,7 @@ Bob accepts the update. Now his environment will be updated. The old client soft
Because Bob has customized the previous photo viewing app, he is notified that this customization is no longer valid, as the software has been removed (deprecation message).l
Optionally, Bob can now customize the new photo viewing software through his Clan configuration app or via a config file.
Optionally, Bob can now customize the new photo viewing software through his cLAN configuration app or via a config file.
## Challenges

View File

@ -1,10 +1,10 @@
# Clan module maintaining
# cLAN module maintaining
## General Description
Clan modules are pieces of software that can be used by admins to build a private or public infrastructure.
cLAN modules are pieces of software that can be used by admins to build a private or public infrastructure.
Clan modules should have the following properties:
cLAN modules should have the following properties:
1. Documented: It should be clear what the module does and how to use it.
1. Self contained: A module should be usable as is. If it requires any other software or settings, those should be delivered with the module itself.

View File

@ -1,41 +0,0 @@
from typing import Any
def define_env(env: Any) -> None:
static_dir = "/static/"
video_dir = "https://clan.lol/" + "videos/"
asciinema_dir = static_dir + "asciinema-player/"
@env.macro
def video(name: str) -> str:
return f"""<video loop muted autoplay id="{name}">
<source src={video_dir + name} type="video/webm">
Your browser does not support the video tag.
</video>"""
@env.macro
def asciinema(name: str) -> str:
return f"""<div id="{name}">
<script>
// Function to load the script and then create the Asciinema player
function loadAsciinemaPlayer() {{
var script = document.createElement('script');
script.src = "{asciinema_dir}/asciinema-player.min.js";
script.onload = function() {{
AsciinemaPlayer.create('{video_dir + name}', document.getElementById("{name}"), {{
loop: true,
autoPlay: true,
controls: false,
speed: 1.5,
theme: "solarized-light"
}});
}};
document.head.appendChild(script);
}}
// Load the Asciinema player script
loadAsciinemaPlayer();
</script>
<link rel="stylesheet" type="text/css" href="{asciinema_dir}/asciinema-player.css" />
</div>"""

View File

@ -1,4 +1,4 @@
site_name: Clan Documentation
site_name: cLAN documentation
site_url: https://docs.clan.lol
repo_url: https://git.clan.lol/clan/clan-core/
repo_name: clan-core
@ -13,7 +13,6 @@ markdown_extensions:
- admonition
- attr_list
- footnotes
- md_in_html
- meta
- plantuml_markdown
- pymdownx.emoji:
@ -27,8 +26,6 @@ markdown_extensions:
- pymdownx.details
- pymdownx.highlight:
use_pygments: true
anchor_linenums: true
- pymdownx.keys
- toc:
title: On this page
@ -38,19 +35,24 @@ exclude_docs: |
/drafts/
nav:
- Blog:
- blog/index.md
- Getting started:
- index.md
- Installer: getting-started/installer.md
- Configure: getting-started/configure.md
- Secrets & Facts: getting-started/secrets.md
- Deploy Machine: getting-started/deploy.md
- Mesh VPN: getting-started/mesh-vpn.md
- Deploy Machine: getting-started/machines.md
- Installer: getting-started/installer.md
- Setup Networking: getting-started/networking.md
- Provision Secrets & Passwords: getting-started/secrets.md
- Backup & Restore: getting-started/backups.md
- Flake-parts: getting-started/flake-parts.md
- Modules:
- Clan Modules:
- Templates: templates/index.md
- Reference:
- clan-core:
- reference/clan-core/index.md
- reference/clan-core/backups.md
- reference/clan-core/facts.md
- reference/clan-core/sops.md
- reference/clan-core/state.md
- clanModules:
- reference/clanModules/borgbackup.md
- reference/clanModules/deltachat.md
- reference/clanModules/disk-layouts.md
@ -63,50 +65,24 @@ nav:
- reference/clanModules/sshd.md
- reference/clanModules/sunshine.md
- reference/clanModules/syncthing.md
- reference/clanModules/static-hosts.md
- reference/clanModules/thelounge.md
- reference/clanModules/trusted-nix-caches.md
- reference/clanModules/user-password.md
- reference/clanModules/xfce.md
- reference/clanModules/zerotier-static-peers.md
- reference/clanModules/zt-tcp-relay.md
- CLI:
- reference/cli/index.md
- reference/cli/backups.md
- reference/cli/config.md
- reference/cli/facts.md
- reference/cli/flakes.md
- reference/cli/flash.md
- reference/cli/history.md
- reference/cli/machines.md
- reference/cli/secrets.md
- reference/cli/ssh.md
- reference/cli/vms.md
- Clan Core:
- reference/clan-core/index.md
- reference/clan-core/backups.md
- reference/clan-core/facts.md
- reference/clan-core/sops.md
- reference/clan-core/state.md
- Contributing: contributing/contributing.md
docs_dir: site
site_dir: out
theme:
font: false
logo: https://clan.lol/static/logo/clan-white.png
favicon: https://clan.lol/static/dark-favicon/128x128.png
logo: static/logo.png
name: material
features:
- navigation.instant
- navigation.tabs
- content.code.annotate
- content.code.copy
- content.tabs.link
icon:
repo: fontawesome/brands/git-alt
custom_dir: overrides
repo: fontawesome/brands/git
palette:
# Palette toggle for light mode
@ -127,33 +103,5 @@ theme:
icon: material/weather-sunny
name: Switch to light mode
extra_css:
- static/extra.css
extra:
social:
- icon: fontawesome/regular/comment
link: https://matrix.to/#/#clan:lassul.us
- icon: fontawesome/brands/gitlab
link: https://git.clan.lol/clan/clan-core
- icon: fontawesome/brands/github
link: https://github.com/clan-lol/clan-core
- icon: fontawesome/solid/rss
link: /feed_rss_created.xml
plugins:
- search
- blog
- macros
- rss:
match_path: blog/posts/.*
use_git: false
date_from_meta:
as_creation: "date"
as_update: "date"
datetime_format: "%Y-%m-%d %H:%M"
default_timezone: Europe/Paris
default_time: "17:18"
categories:
- categories
- tags

View File

@ -1,13 +1,4 @@
{
pkgs,
module-docs,
clan-cli-docs,
asciinema-player-js,
asciinema-player-css,
roboto,
fira-code,
...
}:
{ pkgs, module-docs, ... }:
let
uml-c4 = pkgs.python3Packages.plantuml-markdown.override { plantuml = pkgs.plantuml-c4; };
in
@ -24,21 +15,11 @@ pkgs.stdenv.mkDerivation {
++ (with pkgs.python3Packages; [
mkdocs
mkdocs-material
mkdocs-rss-plugin
mkdocs-macros
]);
configurePhase = ''
mkdir -p ./site/reference/cli
mkdir -p ./site/reference
cp -af ${module-docs}/* ./site/reference/
cp -af ${clan-cli-docs}/* ./site/reference/cli/
mkdir -p ./site/static/asciinema-player
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
ln -snf ${asciinema-player-css} ./site/static/asciinema-player/asciinema-player.css
# Link to fonts
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/
ln -snf ${fira-code}/share/fonts/truetype/FiraCode-VF.ttf ./site/static/
'';
buildPhase = ''

View File

@ -40,15 +40,6 @@
mypy --strict $out
'';
asciinema-player-js = pkgs.fetchurl {
url = "https://github.com/asciinema/asciinema-player/releases/download/v3.7.0/asciinema-player.min.js";
sha256 = "sha256-Ymco/+FinDr5YOrV72ehclpp4amrczjo5EU3jfr/zxs=";
};
asciinema-player-css = pkgs.fetchurl {
url = "https://github.com/asciinema/asciinema-player/releases/download/v3.7.0/asciinema-player.css";
sha256 = "sha256-GZMeZFFGvP5GMqqh516mjJKfQaiJ6bL38bSYOXkaohc=";
};
module-docs = pkgs.runCommand "rendered" { nativeBuildInputs = [ pkgs.python3 ]; } ''
export CLAN_CORE=${jsonDocs.clanCore}/share/doc/nixos/options.json
# A file that contains the links to all clanModule docs
@ -63,21 +54,19 @@
in
{
devShells.docs = pkgs.callPackage ./shell.nix {
inherit (self'.packages) docs clan-cli-docs;
inherit (self'.packages) docs;
inherit module-docs;
inherit asciinema-player-js;
inherit asciinema-player-css;
};
packages = {
docs = pkgs.python3.pkgs.callPackage ./default.nix {
inherit (self'.packages) clan-cli-docs;
inherit (inputs) nixpkgs;
inherit module-docs;
inherit asciinema-player-js;
inherit asciinema-player-css;
};
deploy-docs = pkgs.callPackage ./deploy-docs.nix { inherit (config.packages) docs; };
inherit module-docs;
};
legacyPackages = {
foo = jsonDocs;
};
};
}

View File

@ -40,14 +40,13 @@ def sanitize(text: str) -> str:
return text.replace(">", "\\>")
def replace_store_path(text: str) -> tuple[str, str]:
def replace_store_path(text: str) -> Path:
res = text
if text.startswith("/nix/store/"):
res = "https://git.clan.lol/clan/clan-core/src/branch/main/" + str(
Path(*Path(text).parts[4:])
)
name = Path(res).name
return (res, name)
return Path(res)
def render_option_header(name: str) -> str:
@ -75,7 +74,7 @@ def render_option(name: str, option: dict[str, Any], level: int = 3) -> str:
read_only = option.get("readOnly")
res = f"""
{"#" * level} {sanitize(name)}
{"#" * level} {sanitize(name)} {{#{sanitize(name)}}}
{"Readonly" if read_only else ""}
{option.get("description", "No description available.")}
@ -109,10 +108,9 @@ def render_option(name: str, option: dict[str, Any], level: int = 3) -> str:
"""
decls = option.get("declarations", [])
source_path, name = replace_store_path(decls[0])
print(source_path, name)
source_path = replace_store_path(decls[0])
res += f"""
:simple-git: [{name}]({source_path})
:simple-git: [{source_path.name}]({source_path})
"""
res += "\n"
@ -162,7 +160,7 @@ def produce_clan_core_docs() -> None:
for option_name, info in options.items():
outfile = f"{module_name}/index.md"
# Create separate files for nested options
# Create seperate files for nested options
if len(option_name.split(".")) <= 2:
# i.e. clan-core.clanDir
output = core_outputs.get(

View File

@ -2,30 +2,15 @@
docs,
pkgs,
module-docs,
clan-cli-docs,
asciinema-player-js,
asciinema-player-css,
roboto,
fira-code,
...
}:
pkgs.mkShell {
inputsFrom = [ docs ];
shellHook = ''
mkdir -p ./site/reference/cli
mkdir -p ./site/reference
cp -af ${module-docs}/* ./site/reference/
cp -af ${clan-cli-docs}/* ./site/reference/cli/
chmod +w ./site/reference/*
echo "Generated API documentation in './site/reference/' "
mkdir -p ./site/static/asciinema-player
ln -snf ${asciinema-player-js} ./site/static/asciinema-player/asciinema-player.min.js
ln -snf ${asciinema-player-css} ./site/static/asciinema-player/asciinema-player.css
# Link to fonts
ln -snf ${roboto}/share/fonts/truetype/Roboto-Regular.ttf ./site/static/
ln -snf ${fira-code}/share/fonts/truetype/FiraCode-VF.ttf ./site/static/
'';
}

View File

@ -1,12 +0,0 @@
{% extends "base.html" %}
{% block extrahead %}
<meta property="og:title" content="Clan - Documentation, Blog & Getting Started Guide" />
<meta property="og:description" content="Documentation for Clan. The peer-to-peer machine deployment framework." />
<meta property="og:image" content="https://clan.lol/static/dark-favicon/128x128.png" />
<meta property="og:url" content="https://docs.clan.lol" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Clan" />
<meta property="og:locale" content="en_US" />
{% endblock %}

View File

@ -1,26 +0,0 @@
authors:
DavHau:
name: "DavHau"
description: "Core Developer"
avatar: "https://clan.lol/static/profiles/davhau.jpg"
url: "https://DavHau.com"
Lassulus:
name: "Lassulus"
description: "Core Developer"
avatar: "https://clan.lol/static/profiles/lassulus.jpg"
url: "https://http://lassul.us/"
Mic92:
name: "Mic92"
description: "Core Developer"
avatar: "https://clan.lol/static/profiles/mic92.jpg"
url: "https://thalheim.io"
W:
name: "W"
description: "Founder of Clan"
avatar: "https://clan.lol/static/profiles/w_profile.webp"
url: ""
Qubasa:
name: "Qubasa"
description: "Core Developer"
avatar: "https://clan.lol/static/profiles/qubasa.png"
url: "https://github.com/Qubasa"

View File

@ -1,2 +0,0 @@
# Blog

View File

@ -1,72 +0,0 @@
---
title: "Introducing Clan: Full-Stack Computing Redefined"
description: "Introducing Clan, a new model for a decentralized network, designed to provide families, smaller groups, and small businesses a platform thats private, secure, and user-friendly."
authors:
- W
- Qubasa
date: 2024-03-19
---
In a digital age where users are guided increasingly toward submission and dependence, Clan reclaims computing and networking from the ground up.
Clan enables users to build any system from a git repository, automate secret handling, and join devices in a secure darknet. This control extends beyond applications to communication protocols and the operating system itself, putting you fully in charge of your own digital environment.
## Why We're Building Clan
Our mission is simple: to restore fun, freedom, and functionality to computing as an open source project. We believe in building tools that empower users, foster innovation, and challenge the limitations imposed by outdated paradigms. Clan, in its essence, is an open source endeavor; it's our contribution to a future where technology serves humanity, not the other way around.
## How Clan Changes the Game
Clan embodies a new philosophy in system, application, and network design. It enables seamless, secure communication across devices, simplifies software distribution and updates, and offers both public and private network configurations. Here are some of the ways it accomplishes this:
- **Nix as a Foundation:** Imagine a safety net for your computer's operating system, one that lets you make changes or updates without the fear of causing a crash or losing data. Nix simplifies the complexities of system design, ensuring that updates are safe and systems are more reliable.
- **Simplified System Deployment:** Building and managing a computer system, from the operating system to the software you use, often feels like putting together a complex puzzle. With Clan, the puzzle pieces are replaced by a set of building blocks. Leveraging the power of Nix and Clan's innovative toolkit, anyone from tech-savvy administrators to everyday users can create and maintain what we call "full-stack systems" (everything your computer needs to run smoothly).
- **A Leap in Connectivity:** Imagine if you could create private, secure pathways between your devices, bypassing the noisy and often insecure internet. Clan makes this possible through something called "overlay networks." These networks are like private tunnels, allowing your devices to talk to each other securely and directly. With Clan's built-in overlay networks and automatically configured services, connecting your devices becomes seamless, secure, and hassle-free.
- **Security Through Separation:** Clan employs sandboxing and virtual machines, a technology that runs code in isolated environments - so even if you explore new Clans, your system remains protected from potential threats.
- **Reliable:** With Clan, your data and services are preserved for the long haul. We focus on self-hosted backups and integration with the [Fediverse](https://de.wikipedia.org/wiki/Fediverse), a network of interconnected, independent online communities, so your digital life remains uninterrupted and under your control.
## A Glimpse at Clan's Features
- **Social Scaling:** Choose between creating a private sanctuary for your closest contacts, a dynamic space for a self-contained community, or embracing the open web with public Clans anyone can join.
{{ video(name="show_join.webm")}}
- **Seamless VM Integration:** Applications running in virtual machines can appear and behave as if they're part of your main operating system — a blend of power and simplicity.
{{ video(name="show_run.webm")}}
- **Robust Backup Management:** Keep your data safe _forever_ - never worry about cloud services disappearing in 10 years.
{{ asciinema(name="backups.cast") }}
- **Intuitive Secret Management:** Clan simplifies digital security by automating the creation and management of encryption keys and passwords for your services.
{{ asciinema(name="secrets.cast") }}
- **Remote Install:** Set up and manage Clan systems anywhere in the world with just a QR scan or SSH access, making remote installations as easy as snapping a photo or sharing a link.
{{ asciinema(name="nixos-install.cast") }}
## Who Stands to Benefit?
Clan is for anyone and everyone who believes in the power of open source technology to connect, empower, and protect. From system administrators to less tech-savvy individuals, small business owners to privacy-conscious users, Clan offers something for everyone — a way to reclaim control and redefine how we interact with technology.
## Join the Revolution
Ready to control your digital world? Clan is more than a tool—it's a movement. Secure your data, manage your systems easily, or connect with others how you like. Start with Clan for a better digital future.
Connect with us on our [Matrix channel at clan.lol](https://matrix.to/#/#clan:lassul.us) or through our IRC bridges (coming soon).
Want to see the code? Check it out [on our Gitea](https://git.clan.lol/clan/clan-core) or [on GitHub](https://github.com/clan-lol/clan-core).
Or follow our [RSS feed](https://docs.clan.lol/feed_rss_created.xml)!
Join us and be part of changing technology for the better, together.

View File

@ -1,194 +0,0 @@
---
title: "Dev Report: Introducing the NixOS to JSON Schema Converter"
description: "Discover our new library designed to extract JSON schema interfaces from NixOS modules, streamlining frontend development"
authors:
- DavHau
date: 2024-05-25
slug: jsonschema-converter
---
## Overview
Weve developed a new library designed to extract interfaces from NixOS modules and convert them into JSON schemas, paving the way for effortless GUI generation. This blog post outlines the motivations behind this development, demonstrates the capabilities of the library, and guides you through leveraging it to create GUIs seamlessly.
## Motivation
In recent months, our team has been exploring various graphical user interfaces (GUIs) to streamline NixOS machine configuration. While our opinionated Clan modules simplify NixOS configurations, there's a need to configure these modules from diverse frontends, such as:
- Command-line interfaces (CLIs)
- Web-based UIs
- Desktop applications
- Mobile applications
- Large Language Models (LLMs)
Given this need, a universal format like JSON is a natural choice. It is already possible as of now, to import json based NixOS configurations, as illustrated below:
`configuration.json`:
```json
{ "networking": { "hostName": "my-machine" } }
```
This configuration can be then imported inside a classic NixOS config:
```nix
{config, lib, pkgs, ...}: {
imports = [
(lib.importJSON ./configuration.json)
];
}
```
This straightforward approach allows us to build a frontend that generates JSON, enabling the configuration of NixOS machines. But, two critical questions arise:
1. How does the frontend learn about existing configuration options?
2. How can it verify user input without running Nix?
Introducing [JSON schema](https://json-schema.org/), a widely supported standard that defines interfaces in JSON and validates input against them.
Example schema for `networking.hostName`:
```json
{
"type": "object",
"properties": {
"networking": {
"type": "object",
"properties": {
"hostName": {
"type": "string",
"pattern": "^$|^[a-z0-9]([a-z0-9_-]{0,61}[a-z0-9])?$"
}
}
}
}
}
```
## Client-Side Input Validation
Validating input against JSON schemas is both efficient and well-supported across numerous programming languages. Using JSON schema validators, you can accurately check configurations like our `configuration.json`.
Validation example:
```shell
$ nix-shell -p check-jsonschema
$ jsonschema -o pretty ./schema.json -i ./configuration.json
===[SUCCESS]===(./configuration.json)===
```
In case of invalid input, schema validators provide explicit error messages:
```shell
$ echo '{ "networking": { "hostName": "my/machine" } }' > configuration.json
$ jsonschema -o pretty ./schema.json -i ./configuration.json
===[ValidationError]===(./configuration.json)===
'my/machine' does not match '^$|^[a-z0-9]([a-z0-9_-]{0,61}[a-z0-9])?$'
Failed validating 'pattern' in schema['properties']['networking']['properties']['hostName']:
{'pattern': '^$|^[a-z0-9]([a-z0-9_-]{0,61}[a-z0-9])?$',
'type': 'string'}
On instance['networking']['hostName']:
'my/machine'
```
## Automatic GUI Generation
Certain libraries facilitate straightforward GUI generation from JSON schemas. For instance, the [react-jsonschema-form playground](https://rjsf-team.github.io/react-jsonschema-form/) auto-generates a form for any given schema.
## NixOS Module to JSON Schema Converter
To enable the development of responsive frontends, our library allows the extraction of interfaces from NixOS modules to JSON schemas. Open-sourced for community collaboration, this library supports building sophisticated user interfaces for NixOS.
Heres a preview of our library's functions exposed through the [clan-core](https://git.clan.lol/clan/clan-core) flake:
- `lib.jsonschema.parseModule` - Generates a schema for a NixOS module.
- `lib.jsonschema.parseOption` - Generates a schema for a single NixOS option.
- `lib.jsonschema.parseOptions` - Generates a schema from an attrset of NixOS options.
Example:
`module.nix`:
```nix
{lib, config, pkgs, ...}: {
# a simple service with two options
options.services.example-web-service = {
enable = lib.mkEnableOption "Example web service";
port = lib.mkOption {
type = lib.types.int;
description = "Port used to serve the content";
};
};
}
```
Converted, using the `parseModule` function:
```shell
$ cd clan-core
$ nix eval --json --impure --expr \
'(import ./lib/jsonschema {}).parseModule ./module.nix' | jq | head
{
"properties": {
"services": {
"properties": {
"example-web-service": {
"properties": {
"enable": {
"default": false,
"description": "Whether to enable Example web service.",
"examples": [
...
```
This utility can also generate interfaces for existing NixOS modules or options.
## GUI for NGINX in Under a Minute
Creating a prototype GUI for the NGINX module using our library and [react-jsonschema-form playground](https://rjsf-team.github.io/react-jsonschema-form/) can be done quickly:
1. Export all NGINX options into a JSON schema using a Nix expression:
```nix
# export.nix
let
pkgs = import <nixpkgs> {};
clan-core = builtins.getFlake "git+https://git.clan.lol/clan/clan-core";
options = (pkgs.nixos {}).options.services.nginx;
in
clan-core.lib.jsonschema.parseOption options
```
2. Write the schema into a file:
```shell
$ nix eval --json -f ./export.nix | jq > nginx.json
```
3. Open the [react-jsonschema-form playground](https://rjsf-team.github.io/react-jsonschema-form/), select `Blank` and paste the `nginx.json` contents.
This provides a quick look at a potential GUI (screenshot is cropped).
![Image title](https://clan.lol/static/blog-post-jsonschema/nginx-gui.jpg)
## Limitations
### Laziness
JSON schema mandates the declaration of all required fields upfront, which might be configured implicitly or remain unused. For instance, `services.nginx.virtualHosts.<name>.sslCertificate` must be specified even if SSL isnt enabled.
### Limited Types
Certain NixOS module types, like `types.functionTo` and `types.package`, do not map straightforwardly to JSON. For full compatibility, adjustments to NixOS modules might be necessary, such as substituting `listOf package` with `listOf str`.
### Parsing NixOS Modules
Currently, our converter relies on the `options` attribute of evaluated NixOS modules, extracting information from the `type.name` attribute, which is suboptimal. Enhanced introspection capabilities within the NixOS module system would be beneficial.
## Future Prospects
We hope these experiments inspire the community, encourage contributions and further development in this space. Share your ideas and contributions through our issue tracker or matrix channel!
## Links
- [Comments on NixOS Discourse](https://discourse.nixos.org/t/introducing-the-nixos-to-json-schema-converter/45948)
- [Source Code of the JSON Schema Library](https://git.clan.lol/clan/clan-core/src/branch/main/lib/jsonschema)
- [Our Issue Tracker](https://git.clan.lol/clan/clan-core/issues)
- [Our Matrix Channel](https://matrix.to/#/#clan:lassul.us)
- [react-jsonschema-form Playground](https://rjsf-team.github.io/react-jsonschema-form/)

View File

@ -1,13 +0,0 @@
---
title: "New documentation site and weekly new meetup"
authors:
- Lassulus
- Mic92
date: 2024-04-16
---
Last week, we added a new documentation hub for clan at [docs.clan.lol](https://docs.clan.lol).
We are still working on improving the installation procedures, so stay tuned.
We now have weekly office hours where people are invited to hangout and ask questions.
They are every Wednesday 15:30 UTC (17:30 CEST) in our [jitsi](https://jitsi.lassul.us/clan.lol).
Otherwise drop by in our [matrix channel](https://matrix.to/#/#clan:lassul.us).

View File

@ -1,63 +0,0 @@
---
title: "Git Based Machine Deployment with Clan-Core"
description: ""
authors:
- Qubasa
date: 2024-05-25
---
## Revolutionizing Server Management
In the world of server management, countless tools claim to offer seamless deployment of multiple machines. Yet, many fall short, leaving server admins and self-hosting enthusiasts grappling with complexity. Enter the Clan-Core Framework—a groundbreaking all in one solution designed to transform decentralized self-hosting into an effortless and scalable endeavor.
### The Power of Clan-Core
Imagine having the power to manage your servers with unparalleled ease, scaling your IT infrastructure like never before. Clan-Core empowers you to do just that. At its core, Clan-Core leverages a single Git repository to define everything about your machines. This central repository utilizes Nix or JSON files to specify configurations, including disk formatting, ensuring a streamlined and unified approach.
### Simplified Deployment Process
With Clan-Core, the cumbersome task of bootstrapping a specific ISO is a thing of the past. All you need is SSH access to your Linux server. Clan-Core allows you to overwrite any existing Linux distribution live over SSH, eliminating time-consuming setup processes. This capability means you can deploy updates or new configurations swiftly and efficiently, maximizing uptime and minimizing hassle.
### Secure and Efficient Secret Management
Security is paramount in server management, and Clan-Core takes it seriously. Passwords and other sensitive information are encrypted within the Git repository, automatically decrypted during deployment. This not only ensures the safety of your secrets but also simplifies their management. Clan-Core supports sharing secrets with other admins, fostering collaboration and maintaining reproducibillity and security without sacrificing convenience.
### Services as Apps
Setting up a service can be quite difficult. Many server adjustments need to be made, from setting up a database to adjusting webserver configurations and generating the correct private keys. However, Clan-Core aims to make setting up a service as easy as installing an application. Through Clan-Core's Module system, everything down to secrets can be automatically set up. This transforms the often daunting task of service setup into a smooth, automated process, making it accessible to all.
### Decentralized Mesh VPN
Building on these features is a self-configuring decentralized mesh VPN that interconnects all your machines into a private darknet. This ensures that sensitive services, which might have too much attack surface to be hosted on the public internet, can still be made available privately without the need to worry about potential system compromise. By creating a secure, private network, Clan-Core offers an additional layer of protection for your most critical services.
### Decentralized Domain Name System
Current DNS implementations are distributed but not truly decentralized. For Clan-Core, we implemented our own truly decentralized DNS module. This module uses simple flooding and caching algorithms to discover available domains inside the darknet. This approach ensures that your internal domain name system is robust, reliable, and independent of external control, enhancing the resilience and security of your infrastructure.
### A New Era of Decentralized Self-Hosting
Clan-Core is more than just a tool; it's a paradigm shift in server management. By consolidating machine definitions, secrets and network configuration, into a single, secure repository, it transforms how you manage and scale your infrastructure. Whether you're a seasoned server admin or a self-hosting enthusiast, Clan-Core offers a powerful, user-friendly solution to take your capabilities to the next level.
### Key Features of Clan-Core:
- **Unified Git Repository**: All machine configurations and secrets stored in a single repository.
- **Live Overwrites**: Deploy configurations over existing Linux distributions via SSH.
- **Automated Service Setup**: Easily set up services with Clan-Core's Module system.
- **Decentralized Mesh VPN**: Securely interconnect all machines into a private darknet.
- **Decentralized DNS**: Robust, independent DNS using flooding and caching algorithms.
- **Automated Secret Management**: Encrypted secrets that are automatically decrypted during deployment.
- **Collaboration Support**: Share secrets securely with other admins.
## Clan-Cores Future
Our vision for Clan-Core extends far beyond being just another deployment tool. Clan-Core is a framework we've developed to achieve something much greater. We want to put the "personal" back into "personal computing." Our goal is for everyday users to fully customize their phones or laptops and create truly private spaces for friends and family.
Our first major step is to develop a Graphical User Interface (GUI) that makes configuring all this possible. Initial tests have shown that AI can be leveraged as an alternative to traditional GUIs. This paves the way for a future where people can simply talk to their computers, and they will configure themselves according to the users' wishes.
By adopting Clan, you're not just embracing a tool—you're joining a movement towards a more efficient, secure, and scalable approach to server management. Join us and revolutionize your IT infrastructure today.

View File

@ -5,6 +5,10 @@
In the `flake.nix` file:
- [x] set a unique `clanName`.
- [ ] set `clanIcon` (optional)
- [ ] Set `machineIcon` per machine (optional)
These icons will be used by our future GUI.
=== "**buildClan**"
@ -12,12 +16,16 @@ In the `flake.nix` file:
buildClan {
# Set a unique name
clanName = "Lobsters";
# Optional, a path to an image file
clanIcon = ./path/to/file;
# Should usually point to the directory of flake.nix
directory = ./.;
machines = {
jon = {
# ...
# Optional, a path to an image file
clanCore.machineIcon = ./path/to/file;
};
# ...
}
@ -32,10 +40,14 @@ In the `flake.nix` file:
clan = {
# Set a unique name
clanName = "Lobsters";
# Optional, a path to an image file
clanIcon = ./path/to/file;
machines = {
jon = {
# ...
# Optional, a path to an image file
clanCore.machineIcon = ./path/to/file;
};
# ...
}
@ -51,15 +63,12 @@ Adding or configuring a new machine requires two simple steps:
1. Find the remote disk id by executing:
```bash title="setup computer"
ssh root@flash-installer.local lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
ssh root@<target-computer> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
```
!!! Note
Replace `flash-installer.local` with the IP address of the machine if you don't have the avahi service running which resolves mDNS local domains.
Which should show something like:
```{.shellSession hl_lines="6" .no-copy}
```bash
NAME ID-LINK FSTYPE SIZE MOUNTPOINT
sda usb-ST_16GB_AA6271026J1000000509-0:0 14.9G
├─sda1 usb-ST_16GB_AA6271026J1000000509-0:0-part1 1M
@ -75,7 +84,7 @@ Adding or configuring a new machine requires two simple steps:
=== "**buildClan**"
```nix title="clan-core.lib.buildClan" hl_lines="18 23"
```nix title="clan-core.lib.buildClan"
buildClan {
# ...
machines = {
@ -83,32 +92,29 @@ Adding or configuring a new machine requires two simple steps:
imports = [
# ...
./modules/disko.nix
./machines/jon/configuration.nix
];
# ...
# Change this to the correct ip-address or hostname
# The hostname is the machine name by default
clan.networking.targetHost = pkgs.lib.mkDefault "root@jon"
clan.networking.targetHost = pkgs.lib.mkDefault "root@<hostname>"
# Change this to the ID-LINK of the desired disk shown by 'lsblk'
disko.devices.disk.main = {
device = "/dev/disk/by-id/__CHANGE_ME__";
}
# e.g. > cat ~/.ssh/id_ed25519.pub
users.users.root.openssh.authorizedKeys.keys = [
"<YOUR SSH_KEY>"
];
# ...
};
};
};
}
```
=== "**flakeParts**"
```nix title="clan-core.flakeModules.default" hl_lines="18 23"
```nix title="clan-core.flakeModules.default"
clan = {
# ...
machines = {
@ -116,72 +122,46 @@ Adding or configuring a new machine requires two simple steps:
imports = [
# ...
./modules/disko.nix
./machines/jon/configuration.nix
];
# ...
# Change this to the correct ip-address or hostname
# The hostname is the machine name by default
clan.networking.targetHost = pkgs.lib.mkDefault "root@jon"
clan.networking.targetHost = pkgs.lib.mkDefault "root@<hostname>"
# Change this to the ID-LINK of the desired disk shown by 'lsblk'
disko.devices.disk.main = {
device = "/dev/disk/by-id/__CHANGE_ME__";
}
# e.g. > cat ~/.ssh/id_ed25519.pub
users.users.root.openssh.authorizedKeys.keys = [
"__YOUR_SSH_KEY__"
];
# ...
};
};
};
};
```
### Step 2. Detect hardware specific drivers
!!! Info "Replace `__CHANGE_ME__` with the appropriate identifier, such as `nvme-eui.e8238fa6bf530001001b448b4aec2929`"
!!! Info "Replace `__YOUR_SSH_KEY__` with your personal key, like `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoMI0NC5eT9pHlQExrvR5ASV3iW9+BXwhfchq0smXUJ jon@jon-desktop`"
1. Generate a `hardware-configuration.nix` for your target computer
These steps will allow you to update your machine later.
```bash
ssh root@<target-computer> nixos-generate-config --no-filesystems --show-hardware-config > hardware-configuration.nix
```
### Step 2: Detect Drivers
2. Move the generated file to `machines/jon/hardware-configuration.nix`.
Generate the `hardware-configuration.nix` file for your machine by executing the following command:
### Initialize the facts
```bash
ssh root@flash-installer.local nixos-generate-config --no-filesystems --show-hardware-config > machines/jon/hardware-configuration.nix
```
This command connects to `flash-installer.local` as `root`, runs `nixos-generate-config` to detect hardware configurations (excluding filesystems), and writes them to `machines/jon/hardware-configuration.nix`.
### Step 3: Custom Disk Formatting
In `./modules/disko.nix`, a simple `ext4` disk partitioning scheme is defined for the Disko module. For more complex disk partitioning setups, refer to the [Disko examples](https://github.com/nix-community/disko/tree/master/example).
### Step 4: Custom Configuration
Modify `./machines/jon/configuration.nix` to personalize the system settings according to your requirements.
### Step 5: Check Configuration
Validate your configuration by running:
```bash
nix flake check
```
This command helps ensure that your system configuration is correct and free from errors.
!!! Note
Integrate this step into your [Continuous Integration](https://en.wikipedia.org/wiki/Continuous_integration) workflow to ensure that only valid Nix configurations are merged into your codebase. This practice helps maintain system stability and reduces integration issues.
!!! Info
**All facts are automatically initialized.**
If you need additional help see our [facts chapter](./secrets.md)
---
## Whats next?
- [Secrets & Facts](secrets.md): Setting up secrets with nix-sops
- [Deploying](machines.md): Deploying a Machine configuration
- [Secrets](secrets.md): Learn about secrets and facts
---

View File

@ -1,231 +0,0 @@
# Deploy Machine
Integrating a new machine into your Clan environment is an easy yet flexible process, allowing for a straight forward management of multiple NixOS configurations.
We'll walk you through adding a new computer to your Clan.
## Installing a New Machine
Clan CLI, in conjunction with [nixos-anywhere](https://github.com/nix-community/nixos-anywhere), provides a seamless method for installing NixOS on various machines.
This process involves preparing a suitable hardware and disk partitioning configuration and ensuring the target machine is accessible via SSH.
### Step 0. Prerequisites
=== "**Physical Hardware**"
- [x] **Two Computers**: You need one computer that you're getting ready (we'll call this the Target Computer) and another one to set it up from (we'll call this the Setup Computer). Make sure both can talk to each other over the network using SSH.
- [x] **Machine configuration**: See our basic [configuration guide](./configure.md)
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
- [x] **USB Flash Drive**: See [Clan Installer](installer.md)
!!! Steps
1. Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md).
2. Boot the target machine and connect it to a network that makes it reachable from your setup computer.
=== "**Remote Machines**"
- [x] **Two Computers**: You need one computer that you're getting ready (we'll call this the Target Computer) and another one to set it up from (we'll call this the Setup Computer). Make sure both can talk to each other over the network using SSH.
- [x] **Machine configuration**: See our basic [configuration guide](./configure.md)
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
!!! Steps
- Any cloud machine if it is reachable via SSH and supports `kexec`.
### Step 1. Deploy the machine
**Finally deployment time!** Use the following command to build and deploy the image via SSH onto your machine.
=== "**Image Installer**"
This method makes use of the image installers of [nixos-images](https://github.com/nix-community/nixos-images).
See how to prepare the installer for use [here](./installer.md).
The installer will randomly generate a password and local addresses on boot, then run ssh with these preconfigured.
The installer shows it's deployment relevant information in two formats, a text form, as well as a QR code.
This is an example of the booted installer.
```{ .bash .annotate .no-copy .nohighlight}
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ ┌───────────────────────────┐ │
│ │███████████████████████████│ # This is the QR Code (1) │
│ │██ ▄▄▄▄▄ █▀▄█▀█▀▄█ ▄▄▄▄▄ ██│ │
│ │██ █ █ █▀▄▄▄█ ▀█ █ █ ██│ │
│ │██ █▄▄▄█ █▀▄ ▀▄▄▄█ █▄▄▄█ ██│ │
│ │██▄▄▄▄▄▄▄█▄▀ ▀▄▀▄█▄▄▄▄▄▄▄██│ │
│ │███▀▀▀ █▄▄█ ▀▄ ▄▀▄█ ███│ │
│ │██▄██▄▄█▄▄▀▀██▄▀ ▄▄▄ ▄▀█▀██│ │
│ │██ ▄▄▄▄▄ █▄▄▄▄ █ █▄█ █▀ ███│ │
│ │██ █ █ █ █ █ ▄▄▄ ▄▀▀ ██│ │
│ │██ █▄▄▄█ █ ▄ ▄ ▄ ▀█ ▄███│ │
│ │██▄▄▄▄▄▄▄█▄▄▄▄▄▄█▄▄▄▄▄█▄███│ │
│ │███████████████████████████│ │
│ └───────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │Root password: cheesy-capital-unwell # password (2) │ │
│ │Local network addresses: │ │
│ │enp1s0 UP 192.168.178.169/24 metric 1024 fe80::21e:6ff:fe45:3c92/64 │ │
│ │enp2s0 DOWN │ │
│ │wlan0 DOWN # connect to wlan (3) │ │
│ │Onion address: 6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion │ │
│ │Multicast DNS: nixos-installer.local │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ Press 'Ctrl-C' for console access │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
```
1. This is not an actual QR code, because it is displayed rather poorly on text sites.
This would be the actual content of this specific QR code prettified:
```json
{
"pass": "cheesy-capital-unwell",
"tor": "6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion",
"addrs": [
"2001:9e8:347:ca00:21e:6ff:fe45:3c92"
]
}
```
To generate the actual QR code, that would be displayed use:
```shellSession
echo '{"pass":"cheesy-capital-unwell","tor":"6evxy5yhzytwpnhc2vpscrbti3iktxdhpnf6yim6bbs25p4v6beemzyd.onion","addrs":["2001:9e8:347:ca00:21e:6ff:fe45:3c92"]}' | nix run nixpkgs#qrencode -- -s 2 -m 2 -t utf8
```
2. The root password for the installer medium.
This password is autogenerated and meant to be easily typeable.
3. See how to connect the installer medium to wlan [here](./installer.md#optional-connect-to-wifi).
4. :man_raising_hand: I'm a code annotation! I can contain `code`, __formatted
text__, images, ... basically anything that can be written in Markdown.
!!!tip
For easy sharing of deployment information via QR code, we highly recommend using [KDE Connect](https://apps.kde.org/de/kdeconnect/).
There are two ways to deploy your machine:
1. **SSH with Password Authentication**
Run the following command to install using SSH:
```bash
clan machines install [MACHINE] flash-installer.local
```
2. **Scanning a QR Code for Installation Details**
You can input the information by following one of these methods:
- **Using a JSON String or File Path:**
Provide the path to a JSON string or input the string directly:
```terminal
clan machines install [MACHINE] --json [JSON]
```
- **Using an Image Containing the QR Code:**
Provide the path to an image file containing the relevant QR code:
```terminal
clan machines install [MACHINE] --png [PATH]
```
=== "**SSH access**"
Replace `<target_host>` with the **target computers' ip address**:
```bash
clan machines install [MACHINE] <target_host>
```
If you are using our template `[MACHINE]` would be `jon`
!!! success
Your machine is all set up. 🎉 🚀
## Update Your Machines
Clan CLI enables you to remotely update your machines over SSH. This requires setting up a target address for each target machine.
### Setting the Target Host
Replace `root@jon` with the actual hostname or IP address of your target machine:
```{.nix hl_lines="9" .no-copy}
buildClan {
# ...
machines = {
# "jon" will be the hostname of the machine
"jon" = {
# Set this for clan commands use ssh i.e. `clan machines update`
# If you change the hostname, you need to update this line to root@<new-hostname>
# This only works however if you have avahi running on your admin machine else use IP
clan.networking.targetHost = pkgs.lib.mkDefault "root@jon";
};
};
};
```
!!! warning
The use of `root@` in the target address implies SSH access as the `root` user.
Ensure that the root login is secured and only used when necessary.
### Updating Machine Configurations
Execute the following command to update the specified machine:
```bash
clan machines update jon
```
You can also update all configured machines simultaneously by omitting the machine name:
```bash
clan machines update
```
### Setting a Build Host
If the machine does not have enough resources to run the NixOS evaluation or build itself,
it is also possible to specify a build host instead.
During an update, the cli will ssh into the build host and run `nixos-rebuild` from there.
```{.nix hl_lines="5" .no-copy}
buildClan {
# ...
machines = {
"jon" = {
clan.networking.buildHost = "root@<host_or_ip>";
};
};
};
```
### Excluding a machine from `clan machine update`
To exclude machines from being updated when running `clan machines update` without any machines specified,
one can set the `clan.deployment.requireExplicitUpdate` option to true:
```{.nix hl_lines="5" .no-copy}
buildClan {
# ...
machines = {
"jon" = {
clan.deployment.requireExplicitUpdate = true;
};
};
};
```
This is useful for machines that are not always online or are not part of the regular update cycle.
---
## What's next ?
- [**Mesh VPN**](./mesh-vpn.md): Configuring a secure mesh network.
---

View File

@ -73,6 +73,7 @@ Below is a guide on how to structure this in your flake.nix:
# ... more modules
];
nixpkgs.hostPlatform = "x86_64-linux";
clanCore.machineIcon = null; # Optional, a path to an image file
# Set this for clan commands use ssh i.e. `clan machines update`
clan.networking.targetHost = pkgs.lib.mkDefault "root@jon";
@ -94,4 +95,9 @@ Below is a guide on how to structure this in your flake.nix:
For detailed information about configuring `flake-parts` and the available options within Clan,
refer to the Clan module documentation located [here](https://git.clan.lol/clan/clan-core/src/branch/main/flakeModules/clan.nix).
## Whats next?
- [Configure Machines](configure.md): Customize machine configuration
- [Deploying](machines.md): Deploying a Machine configuration
---

View File

@ -1,11 +1,10 @@
# Installer
Our installer image simplifies the process of performing remote installations.
We offer a dedicated installer to assist remote installations.
Follow our step-by-step guide to create and transfer this image onto a bootable USB drive.
In this tutorial we will guide you through building and flashing it to a bootable USB drive.
!!! info
If you already have a NixOS machine you can ssh into (in the cloud for example) you can skip this chapter and go directly to [Configure Machines](configure.md).
## Creating and Using the **Clan Installer**
### Step 0. Prerequisites
@ -22,7 +21,7 @@ Follow our step-by-step guide to create and transfer this image onto a bootable
lsblk
```
```{.shellSession hl_lines="2" .no-copy}
```{.console, .no-copy}
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sdb 8:0 1 117,2G 0 disk
└─sdb1 8:1 1 117,2G 0 part /run/media/qubasa/INTENSO
@ -39,53 +38,28 @@ Follow our step-by-step guide to create and transfer this image onto a bootable
```shellSession
sudo umount /dev/sdb1
```
=== "**Linux OS**"
### Step 2. Flash Custom Installer
Using clan flash enables the inclusion of ssh public keys.
It also allows to set language and keymap currently in the installer image.
### Step 2. Download the Installer
```bash
clan flash --flake git+https://git.clan.lol/clan/clan-core \
--ssh-pubkey $HOME/.ssh/id_ed25519.pub \
--keymap en \
--language en \
--disk main /dev/sd<X> \
flash-installer
```
```shellSession
wget https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-installer-x86_64-linux.iso
```
The `--ssh-pubkey`, `--language` and `--keymap` are optional.
Replace `$HOME/.ssh/id_ed25519.pub` with a path to your SSH public key.
If you do not have an ssh key yet, you can generate one with `ssh-keygen -t ed25519` command.
### Step 3. Flash the Installer to the USB Drive
!!! Danger "Specifying the wrong device can lead to unrecoverable data loss."
!!! Danger "Specifying the wrong device can lead to unrecoverable data loss."
The `clan flash` utility will erase the disk. Make sure to specify the correct device
The `dd` utility will erase the disk. Make sure to specify the correct device (`of=...`)
For example if the USB device is `sdb` use `of=/dev/sdb`.
=== "**Other OS**"
### Step 2. Download Generic Installer
Use the `dd` utility to write the NixOS installer image to your USB drive:
```shellSession
wget https://github.com/nix-community/nixos-images/releases/download/nixos-unstable/nixos-installer-x86_64-linux.iso
```
### Step 3. Flash the Installer to the USB Drive
!!! Danger "Specifying the wrong device can lead to unrecoverable data loss."
The `dd` utility will erase the disk. Make sure to specify the correct device (`of=...`)
For example if the USB device is `sdb` use `of=/dev/sdb`.
Use the `dd` utility to write the NixOS installer image to your USB drive:
```shellSession
sudo dd bs=4M conv=fsync oflag=direct status=progress if=./nixos-installer-x86_64-linux.iso of=/dev/sd<X>
```
```shellSession
sudo dd bs=4M conv=fsync oflag=direct status=progress if=./nixos-installer-x86_64-linux.iso of=/dev/sd<X>
```
### Step 4. Boot and Connect to your network
@ -115,6 +89,17 @@ Select `NixOS` to boot into the clan installer.
For deploying your configuration the machine needs to be connected via LAN (recommended).
For connecting via Wifi, please consult the [guide below](#optional-connect-to-wifi).
---
## Whats next?
- [Configure Machines](configure.md): Customize machine configuration
- [Deploying](machines.md): Deploying a Machine configuration
- [WiFi](#optional-connect-to-wifi): Guide for connecting to Wifi.
---
## (Optional) Connect to Wifi
@ -132,10 +117,9 @@ This will enter `iwd`
Now run the following command to connect to your Wifi:
```{.shellSession .no-copy}
```shellSession
# Identify your network device.
device list
# Replace 'wlan0' with your wireless device name
# Find your Wifi SSID.
station wlan0 scan
@ -157,17 +141,9 @@ Connected network FRITZ!Box (Your router device)
IPv4 address 192.168.188.50 (Your new local ip)
```
Press ++ctrl+d++ to exit `IWD`.
Press `ctrl-d` to exit `IWD`.
!!! Important
Press ++ctrl+d++ **again** to update the displayed QR code and connection information.
Press `ctrl-d` **again** to update the displayed QR code and connection information.
You're all set up
---
## Whats next?
- [Configure Machines](configure.md): Customize machine configuration
---

View File

@ -0,0 +1,125 @@
# Deploy Machine
Integrating a new machine into your Clan environment is an easy yet flexible process, allowing for a straight forward management of multiple NixOS configurations.
We'll walk you through adding a new computer to your Clan.
## Installing a New Machine
Clan CLI, in conjunction with [nixos-anywhere](https://github.com/nix-community/nixos-anywhere), provides a seamless method for installing NixOS on various machines.
This process involves preparing a suitable hardware and disk partitioning configuration and ensuring the target machine is accessible via SSH.
### Step 0. Prerequisites
=== "**Physical Hardware**"
- [x] **Two Computers**: You need one computer that you're getting ready (we'll call this the Target Computer) and another one to set it up from (we'll call this the Setup Computer). Make sure both can talk to each other over the network using SSH.
- [x] **Machine configuration**: See our basic [configuration guide](./configure.md)
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
- [x] **USB Flash Drive**: See [Clan Installer](installer.md)
!!! Steps
1. Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md).
2. Boot the target machine and connect it to a network that makes it reachable from your setup computer.
=== "**Baremetal Machines**"
- [x] **Two Computers**: You need one computer that you're getting ready (we'll call this the Target Computer) and another one to set it up from (we'll call this the Setup Computer). Make sure both can talk to each other over the network using SSH.
- [x] **Machine configuration**: See our basic [configuration guide](./configure.md)
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
!!! Steps
- Any cloud machine if it is reachable via SSH and supports `kexec`.
Confirm the machine is reachable via SSH from your setup computer.
```bash
ssh root@<your_target_machine_ip>
```
### Step 1. Deploy the machine
**Finally deployment time!** Use the following command to build and deploy the image via SSH onto your machine.
Replace `<target_host>` with the **target computers' ip address**:
```bash
clan machines install my-machine <target_host>
```
> Note: This may take a while for building and for the file transfer.
!!! success
Your machine is all set up. 🎉 🚀
---
## What's next ?
- [**Update a Machine**](#update-your-machines): Learn how to update an existing machine?
- [**Configure a Private Network**](./networking.md): Configuring a secure mesh network.
---
## Update Your Machines
Clan CLI enables you to remotely update your machines over SSH. This requires setting up a target address for each target machine.
### Setting the Target Host
Replace `host_or_ip` with the actual hostname or IP address of your target machine:
```bash
clan config --machine my-machine clan.networking.targetHost root@host_or_ip
```
!!! warning
The use of `root@` in the target address implies SSH access as the `root` user.
Ensure that the root login is secured and only used when necessary.
### Updating Machine Configurations
Execute the following command to update the specified machine:
```bash
clan machines update my-machine
```
You can also update all configured machines simultaneously by omitting the machine name:
```bash
clan machines update
```
### Setting a Build Host
If the machine does not have enough resources to run the NixOS evaluation or build itself,
it is also possible to specify a build host instead.
During an update, the cli will ssh into the build host and run `nixos-rebuild` from there.
```bash
clan config --machine my-machine clan.networking.buildHost root@host_or_ip
```
### Excluding a machine from `clan machine update`
To exclude machines from being updated when running `clan machines update` without any machines specified,
one can set the `clan.deployment.requireExplicitUpdate` option to true:
```bash
clan config --machine my-machine clan.deployment.requireExplicitUpdate true
```
This is useful for machines that are not always online or are not part of the regular update cycle.
---
# TODO:
* TODO: How to join others people zerotier
* `services.zerotier.joinNetworks = [ "network-id" ]`
* Controller needs to approve over webinterface or cli

View File

@ -1,4 +1,4 @@
# Mesh VPN
# Overlay Networks
This guide provides detailed instructions for configuring
[ZeroTier VPN](https://zerotier.com) within Clan. Follow the
@ -9,7 +9,7 @@ include a new machine into the VPN.
By default all machines within one clan are connected via a chosen network technology.
```{.no-copy}
```
Clan
Node A
<-> (zerotier / mycelium / ...)
@ -36,7 +36,7 @@ peers. Once addresses are allocated, the controller's continuous operation is no
```
3. **Update the Controller Machine**: Execute the following:
```bash
clan machines update <CONTROLLER>
$ clan machines update <CONTROLLER>
```
Your machine is now operational as the VPN controller.

View File

@ -4,15 +4,13 @@ Clan enables encryption of secrets (such as passwords & keys) ensuring security
Clan utilizes the [sops](https://github.com/getsops/sops) format and integrates with [sops-nix](https://github.com/Mic92/sops-nix) on NixOS machines.
This guide will walk you through:
This documentation will guide you through managing secrets with the Clan CLI
- **Creating a Keypair for Your User**: Learn how to generate a keypair for $USER to securely control all secrets.
- **Creating Your First Secret**: Step-by-step instructions on creating your initial secret.
- **Assigning Machine Access to the Secret**: Understand how to grant a machine access to the newly created secret.
## Initializing Secrets (Quickstart)
## Create Your Admin Keypair
### Create Your Master Keypair
To get started, you'll need to create **Your admin keypair**.
To get started, you'll need to create **Your master keypair**.
!!! info
Don't worry — if you've already made one before, this step won't change or overwrite it.
@ -32,7 +30,7 @@ Also add your age public key to the repository with 'clan secrets users add YOUR
!!! warning
Make sure to keep a safe backup of the private key you've just created.
If it's lost, you won't be able to get to your secrets anymore because they all need the admin key to be unlocked.
If it's lost, you won't be able to get to your secrets anymore because they all need the master key to be unlocked.
!!! note
It's safe to add any secrets created by the clan CLI and placed in your repository to version control systems like `git`.
@ -40,10 +38,11 @@ Also add your age public key to the repository with 'clan secrets users add YOUR
### Add Your Public Key
```bash
clan secrets users add $USER <your_public_key>
clan secrets users add <your_username> <your_public_key>
```
It's best to choose the same username as on your Setup/Admin Machine that you use to control the deployment with.
!!! note
Choose the same username as on your Setup/Source Machine that you use to control the deployment with.
Once run this will create the following files:
@ -53,146 +52,17 @@ sops/
└── <your_username>/
└── key.json
```
If you followed the quickstart tutorial all necessary secrets are initialized at this point.
---
## Whats next?
> If you followed the quickstart tutorial all necessary secrets are initialized at this point.
- [Deployment](deploy.md): How to remotely deploy your machine
- Continue with [deploying machines](./machines.md)
- Learn about the [basics concept](#concept) of clan secrets
---
## More on Secrets
If you want to know more about how to save and share passwords in your clan read further!
### Adding a Secret
```shellSession
clan secrets set mysecret
Paste your secret:
```
### Retrieving a Stored Secret
```bash
clan secrets get mysecret
```
### List all Secrets
```bash
clan secrets list
```
### NixOS integration
A NixOS machine will automatically import all secrets that are encrypted for the
current machine. At runtime it will use the host key to decrypt all secrets into
an in-memory, non-persistent filesystem using [sops-nix](https://github.com/Mic92/sops-nix).
In your nixos configuration you can get a path to secrets like this `config.sops.secrets.<name>.path`. For example:
```nix
{ config, ...}: {
sops.secrets.my-password.neededForUsers = true;
users.users.mic92 = {
isNormalUser = true;
passwordFile = config.sops.secrets.my-password.path;
};
}
```
### Assigning Access
When using `clan secrets set <secret>` without arguments, secrets are encrypted for the key of the user named like your current $USER.
To add machines/users to an existing secret use:
```bash
clan secrets machines add-secret <machine_name> <secret_name>
```
Alternatively specify users and machines while creating a secret:
```bash
clan secrets set --machine <machine1> --machine <machine2> --user <user1> --user <user2> <secret_name>
```
## Advanced
In this section we go into more advanced secret management topics.
### Groups
Clan CLI makes it easy to manage access by allowing you to create groups.
All users within a group inherit access to all secrets of the group.
This feature eases the process of handling permissions for multiple users.
Here's how to get started:
1. **Creating Groups**:
Assign users to a new group, e.g., `admins`:
```bash
clan secrets groups add admins <username>
```
2. **Listing Groups**:
```bash
clan secrets groups list
```
3. **Assigning Secrets to Groups**:
```bash
clan secrets groups add-secret <group_name> <secret_name>
```
### Adding Machine Keys
New machines in Clan come with age keys stored in `./sops/machines/<machine_name>`. To list these machines:
```bash
clan secrets machines list
```
For existing machines, add their keys:
```bash
clan secrets machines add <machine_name> <age_key>
```
To fetch an age key from an SSH host key:
```bash
ssh-keyscan <domain_name> | nix shell nixpkgs#ssh-to-age -c ssh-to-age
```
### Migration: Importing existing sops-based keys / sops-nix
`clan secrets` stores each secret in a single file, whereas [sops](https://github.com/Mic92/sops-nix) commonly allows to put all secrets in a yaml or json document.
If you already happened to use sops-nix, you can migrate by using the `clan secrets import-sops` command by importing these files:
```bash
% clan secrets import-sops --prefix matchbox- --group admins --machine matchbox nixos/matchbox/secrets/secrets.yaml
```
This will create secrets for each secret found in `nixos/matchbox/secrets/secrets.yaml` in a `./sops` folder of your repository.
Each member of the group `admins` in this case will be able to decrypt the secrets with their respective key.
Since our clan secret module will auto-import secrets that are encrypted for a particular nixos machine,
you can now remove `sops.secrets.<secrets> = { };` unless you need to specify more options for the secret like owner/group of the secret file.
## Indepth Explanation
## Concept
The secrets system conceptually knows two different entities:
@ -248,6 +118,9 @@ Rel_R(secret, machine, "Decrypt", "", "machine privkey" )
@enduml
```
### Groups
It is possible to create semantic groups to make access control more convenient.
#### User groups
@ -309,16 +182,146 @@ Rel(secret, c1, "Decrypt", "", "Both machine A or B can decrypt using their priv
<!-- TODO: See also [Groups Reference](#groups-reference) -->
---
## 2. Adding Machine Keys
New machines in Clan come with age keys stored in `./sops/machines/<machine_name>`. To list these machines:
```bash
$ clan secrets machines list
```
For existing machines, add their keys:
```bash
$ clan secrets machines add <machine_name> <age_key>
```
### Advanced
To fetch an age key from an SSH host key:
```bash
$ ssh-keyscan <domain_name> | nix shell nixpkgs#ssh-to-age -c ssh-to-age
```
## 3. Assigning Access
By default, secrets are encrypted for your key. To specify which users and machines can access a secret:
```bash
$ clan secrets set --machine <machine1> --machine <machine2> --user <user1> --user <user2> <secret_name>
```
You can add machines/users to existing secrets without modifying the secret:
```bash
$ clan secrets machines add-secret <machine_name> <secret_name>
```
## 4. Adding Secrets
```bash
$ clan secrets set mysecret
Paste your secret:
```
!!! note
As you type your secret won't be displayed. Press Enter to save the secret.
## 5. Retrieving Stored Secrets
```bash
$ clan secrets get mysecret
```
### List all Secrets
```bash
$ clan secrets list
```
## 6. Groups
Clan CLI makes it easy to manage access by allowing you to create groups.
All users within a group inherit access to all secrets of the group.
This feature eases the process of handling permissions for multiple users.
Here's how to get started:
1. **Creating Groups**:
Assign users to a new group, e.g., `admins`:
```bash
$ clan secrets groups add admins <username>
```
2. **Listing Groups**:
```bash
$ clan secrets groups list
```
3. **Assigning Secrets to Groups**:
```bash
$ clan secrets groups add-secret <group_name> <secret_name>
```
## Further
Secrets in the repository follow this structure:
```{.console, .no-copy}
sops/
├── secrets/
│ └── <secret_name>/
│ ├── secret
│ └── users/
│ └── <your_username>/
```
The content of the secret is stored encrypted inside the `secret` file under `mysecret`.
By default, secrets are encrypted with your key to ensure readability.
### NixOS integration
A NixOS machine will automatically import all secrets that are encrypted for the
current machine. At runtime it will use the host key to decrypt all secrets into
an in-memory, non-persistent filesystem using [sops-nix](https://github.com/Mic92/sops-nix).
In your nixos configuration you can get a path to secrets like this `config.sops.secrets.<name>.path`. For example:
```nix
{ config, ...}: {
sops.secrets.my-password.neededForUsers = true;
users.users.mic92 = {
isNormalUser = true;
passwordFile = config.sops.secrets.my-password.path;
};
}
```
See the [readme](https://github.com/Mic92/sops-nix) of sops-nix for more
examples.
### Migration: Importing existing sops-based keys / sops-nix
---
`clan secrets` stores each secret in a single file, whereas [sops](https://github.com/Mic92/sops-nix) commonly allows to put all secrets in a yaml or json document.
## Whats next?
If you already happened to use sops-nix, you can migrate by using the `clan secrets import-sops` command by importing these files:
- [Deployment](deploy.md): How to remotely deploy your machine
```bash
% clan secrets import-sops --prefix matchbox- --group admins --machine matchbox nixos/matchbox/secrets/secrets.yaml
```
---
This will create secrets for each secret found in `nixos/matchbox/secrets/secrets.yaml` in a `./sops` folder of your repository.
Each member of the group `admins` in this case will be able to decrypt the secrets with their respective key.
Since our clan secret module will auto-import secrets that are encrypted for a particular nixos machine,
you can now remove `sops.secrets.<secrets> = { };` unless you need to specify more options for the secret like owner/group of the secret file.

View File

@ -1,31 +1,44 @@
# Setup
# Getting Started
Create your own clan with these initial steps and manage a fleet of machines with one single testable git repository!
Welcome to your simple guide on starting a new Clan project.
## What's Inside
We've put together a straightforward guide to help you out:
- [**Starting with a New Clan Project**](#starting-with-a-new-clan-project): Create a new Clan from scratch.
- [**Integrating Clan using Flake-Parts**](getting-started/flake-parts.md)
---
## **Starting with a New Clan Project**
Create your own clan with these initial steps.
### Prerequisites
=== "**Linux**"
#### Linux
Clan depends on nix installed on your system. Run the following command to install nix.
Clan depends on nix installed on your system. Run the following command to install nix.
```bash
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```
```bash
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```
=== "**NixOS**"
#### NixOS
If you run NixOS the `nix` binary is already installed.
If you run NixOS the `nix` binary is already installed.
You will also need to enable the `flakes` and `nix-commands` experimental features.
You will also need to enable the `flakes` and `nix-commands` experimental features.
```bash
# /etc/nix/nix.conf or ~/.config/nix/nix.conf
experimental-features = nix-command flakes
```
```bash
# /etc/nix/nix.conf or ~/.config/nix/nix.conf
experimental-features = nix-command flakes
```
=== "**Other**"
#### Other
Clan doesn't offer dedicated support for other operating systems yet.
Clan doesn't offer dedicated support for other operating systems yet.
### Step 1: Add Clan CLI to Your Shell
@ -35,13 +48,6 @@ Add the Clan CLI into your development workflow:
nix shell git+https://git.clan.lol/clan/clan-core#clan-cli
```
You can find reference documentation for the `clan` cli program [here](./reference/cli/index.md).
Alternatively you can check out the help pages directly:
```terminalSession
clan --help
```
### Step 2: Initialize Your Project
Set the foundation of your Clan project by initializing it as follows:
@ -51,7 +57,6 @@ clan flakes create my-clan
```
This command creates the `flake.nix` and `.clan-flake` files for your project.
It will also generate files from a default template, to help show general clan usage patterns.
### Step 3: Verify the Project Structure
@ -97,6 +102,12 @@ sara
### What's Next?
- [**Machine Configuration**](getting-started/configure.md): Declare behavior and configuration of machines.
- [**Deploy Machines**](getting-started/machines.md): Learn how to deploy to any remote machine.
- [**Installer**](getting-started/installer.md): Setting up new computers remotely is easy with an USB stick.
- [**Check out our Templates**](templates/index.md)
---

View File

@ -1 +0,0 @@
/nix/store/8y5h98wk5p94mv1wyb2c4gkrr7bswd19-asciinema-player.css

View File

@ -1 +0,0 @@
/nix/store/w0i3f9qzn9n6jmfnfgiw5wnab2f9ssdw-asciinema-player.min.js

View File

@ -1,13 +0,0 @@
@font-face {
font-family: "Roboto";
src: url(./Roboto-Regular.ttf) format('truetype');
}
@font-face {
font-family: "Fira Code";
src: url(./FiraCode-VF.ttf) format('truetype');
}
:root {
--md-text-font: "Roboto";
--md-code-font: "Fira Code";
}

BIN
docs/site/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,24 @@
# Templates
We provide some starting templates you can easily use one of those via `nix flakes`.
They showcase best practices and guide you through setting up and using Clan's modules
I.e. To use the `new-clan` template run the following command:
```bash
nix flake init -t git+https://git.clan.lol/clan/clan-core#new-clan
```
## Available Templates
We offer the following templates:
To initialize a clan with one of those run:
```bash
nix flake init -t git+https://git.clan.lol/clan/clan-core#[TEMPLATE_NAME]
```
Substitute `[TEMPLATE_NAME]` with the name of the template.
- **new-clan**: Perfect for beginners, this template shows you how to link two machines in a basic setup.

View File

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1717177033,
"narHash": "sha256-G3CZJafCO8WDy3dyA2EhpUJEmzd5gMJ2IdItAg0Hijw=",
"lastModified": 1714612856,
"narHash": "sha256-W7+rtMzRmdovzndN2NYUv5xzkbMudtQ3jbyFuGk0O1E=",
"owner": "nix-community",
"repo": "disko",
"rev": "0274af4c92531ebfba4a5bd493251a143bc51f3c",
"rev": "d57058eb09dd5ec00c746df34fe0a603ea744370",
"type": "github"
},
"original": {
@ -27,11 +27,11 @@
]
},
"locked": {
"lastModified": 1717285511,
"narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=",
"lastModified": 1714641030,
"narHash": "sha256-yzcRNDoyVP7+SCNX0wmuDju1NUCt8Dz9+lyUXEI0dbI=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8",
"rev": "e5d10a24b66c3ea8f150e47dfdb0416ab7c3390e",
"type": "github"
},
"original": {
@ -55,22 +55,6 @@
"type": "github"
}
},
"nixos-2311": {
"locked": {
"lastModified": 1717017538,
"narHash": "sha256-S5kltvDDfNQM3xx9XcvzKEOyN2qk8Sa+aSOLqZ+1Ujc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64e468fd2652105710d86cd2ae3e65a5a6d58dec",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixos-generators": {
"inputs": {
"nixlib": "nixlib",
@ -79,11 +63,11 @@
]
},
"locked": {
"lastModified": 1716210724,
"narHash": "sha256-iqQa3omRcHGpWb1ds75jS9ruA5R39FTmAkeR3J+ve1w=",
"lastModified": 1713783234,
"narHash": "sha256-3yh0nqI1avYUmmtqqTW3EVfwaLE+9ytRWxsA5aWtmyI=",
"owner": "nix-community",
"repo": "nixos-generators",
"rev": "d14b286322c7f4f897ca4b1726ce38cb68596c94",
"rev": "722b512eb7e6915882f39fff0e4c9dd44f42b77e",
"type": "github"
},
"original": {
@ -92,34 +76,13 @@
"type": "github"
}
},
"nixos-images": {
"inputs": {
"nixos-2311": "nixos-2311",
"nixos-unstable": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1717040312,
"narHash": "sha256-yI/en4IxuCEClIUpIs3QTyYCCtmSPLOhwLJclfNwdeg=",
"owner": "nix-community",
"repo": "nixos-images",
"rev": "47bfb55316e105390dd761e0b6e8e0be09462b67",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-images",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1717298511,
"narHash": "sha256-9sXuJn/nL+9ImeYtlspTvjt83z1wIgU+9AwfNbnq+tI=",
"lastModified": 1714923658,
"narHash": "sha256-f54abULm+mOb74m4iDMbXpEsIClOu56q5u6ijbiuIbs=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6634a0509e9e81e980b129435fbbec518ab246d0",
"rev": "9f5a6d72fa3985e4cd8fca3926d14ae8b54bcf75",
"type": "github"
},
"original": {
@ -134,7 +97,6 @@
"disko": "disko",
"flake-parts": "flake-parts",
"nixos-generators": "nixos-generators",
"nixos-images": "nixos-images",
"nixpkgs": "nixpkgs",
"sops-nix": "sops-nix",
"treefmt-nix": "treefmt-nix"
@ -148,11 +110,11 @@
"nixpkgs-stable": []
},
"locked": {
"lastModified": 1717297459,
"narHash": "sha256-cZC2f68w5UrJ1f+2NWGV9Gx0dEYmxwomWN2B0lx0QRA=",
"lastModified": 1714878026,
"narHash": "sha256-YJ1k/jyd6vKqmVgGkkAb4n+ZfPPAt8+L5a73eAThqFU=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "ab2a43b0d21d1d37d4d5726a892f714eaeb4b075",
"rev": "10dc39496d5b027912038bde8d68c836576ad0bc",
"type": "github"
},
"original": {
@ -168,11 +130,11 @@
]
},
"locked": {
"lastModified": 1717278143,
"narHash": "sha256-u10aDdYrpiGOLoxzY/mJ9llST9yO8Q7K/UlROoNxzDw=",
"lastModified": 1714058656,
"narHash": "sha256-Qv4RBm4LKuO4fNOfx9wl40W2rBbv5u5m+whxRYUMiaA=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "3eb96ca1ae9edf792a8e0963cc92fddfa5a87706",
"rev": "c6aaf729f34a36c445618580a9f95a48f5e4e03f",
"type": "github"
},
"original": {

View File

@ -8,6 +8,7 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small";
disko.url = "github:nix-community/disko";
disko.inputs.nixpkgs.follows = "nixpkgs";
sops-nix.url = "github:Mic92/sops-nix";
@ -15,8 +16,6 @@
sops-nix.inputs.nixpkgs-stable.follows = "";
nixos-generators.url = "github:nix-community/nixos-generators";
nixos-generators.inputs.nixpkgs.follows = "nixpkgs";
nixos-images.url = "github:nix-community/nixos-images";
nixos-images.inputs.nixos-unstable.follows = "nixpkgs";
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
treefmt-nix.url = "github:numtide/treefmt-nix";
@ -24,14 +23,10 @@
};
outputs =
inputs@{ flake-parts, self, ... }:
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } (
{ ... }:
{
clan = {
# meta.name = "clan-core";
directory = self;
};
systems = [
"x86_64-linux"
"aarch64-linux"

View File

@ -17,33 +17,6 @@ let
cfg = config.clan;
in
{
imports = [
# TODO: figure out how to print the deprecation warning
# "${inputs.nixpkgs}/nixos/modules/misc/assertions.nix"
(lib.mkRenamedOptionModule
[
"clan"
"clanName"
]
[
"clan"
"meta"
"name"
]
)
(lib.mkRenamedOptionModule
[
"clan"
"clanIcon"
]
[
"clan"
"meta"
"icon"
]
)
];
options.clan = {
directory = mkOption {
type = types.path;
@ -60,27 +33,15 @@ in
default = { };
description = "Allows to include machine-specific modules i.e. machines.\${name} = { ... }";
};
# Checks are performed in 'buildClan'
# Not everyone uses flake-parts
meta = {
name = lib.mkOption {
type = types.nullOr types.str;
default = null;
description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.";
};
icon = mkOption {
type = types.nullOr types.path;
default = null;
description = "A path to an icon to be used for the clan in the GUI";
};
description = mkOption {
type = types.nullOr types.str;
default = null;
description = "A short description of the clan";
};
clanName = mkOption {
type = types.str;
description = "Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.";
};
clanIcon = mkOption {
type = types.nullOr types.path;
default = null;
description = "A path to an icon to be used for the clan, should be the same for all machines";
};
pkgsForSystem = mkOption {
type = types.functionTo types.raw;
default = _system: null;
@ -91,7 +52,6 @@ in
clanInternals = lib.mkOption {
type = lib.types.submodule {
options = {
meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
machinesFunc = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
@ -105,8 +65,9 @@ in
directory
specialArgs
machines
clanName
clanIcon
pkgsForSystem
meta
;
};
};

View File

@ -11,9 +11,7 @@
treefmt.programs.mypy.directories = {
"pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.testDependencies;
"pkgs/clan-vm-manager".extraPythonPackages =
# clan-vm-manager currently only exists on linux
(self'.packages.clan-vm-manager.externalTestDeps or [ ])
++ self'.packages.clan-cli.testDependencies;
self'.packages.clan-vm-manager.externalTestDeps ++ self'.packages.clan-cli.testDependencies;
};
treefmt.settings.formatter.nix = {

View File

@ -7,58 +7,16 @@
directory, # The directory containing the machines subdirectory
specialArgs ? { }, # Extra arguments to pass to nixosSystem i.e. useful to make self available
machines ? { }, # allows to include machine-specific modules i.e. machines.${name} = { ... }
# DEPRECATED: use meta.name instead
clanName ? null, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
# DEPRECATED: use meta.icon instead
clanName, # Needs to be (globally) unique, as this determines the folder name where the flake gets downloaded to.
clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines
meta ? { }, # A set containing clan meta: name :: string, icon :: string, description :: string
pkgsForSystem ? (_system: null), # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
# This improves performance, but all nipxkgs.* options will be ignored.
}:
let
deprecationWarnings = [
(lib.warnIf (
clanName != null
) "clanName is deprecated, please use meta.name instead. ${clanName}" null)
(lib.warnIf (clanIcon != null) "clanIcon is deprecated, please use meta.icon instead" null)
];
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
builtins.readDir (directory + /machines)
);
mergedMeta =
let
metaFromFile =
if (builtins.pathExists "${directory}/clan/meta.json") then
let
settings = builtins.fromJSON (builtins.readFile "${directory}/clan/meta.json");
in
settings
else
{ };
legacyMeta = lib.filterAttrs (_: v: v != null) {
name = clanName;
icon = clanIcon;
};
optionsMeta = lib.filterAttrs (_: v: v != null) meta;
warnings =
builtins.map (
name:
if
metaFromFile.${name} or null != optionsMeta.${name} or null && optionsMeta.${name} or null != null
then
lib.warn "meta.${name} is set in different places. (exlicit option meta.${name} overrides ${directory}/clan/meta.json)" null
else
null
) (builtins.attrNames metaFromFile)
++ [ (if (res.name or null == null) then (throw "meta.name should be set") else null) ];
res = metaFromFile // legacyMeta // optionsMeta;
in
# Print out warnings before returning the merged result
builtins.deepSeq warnings res;
machineSettings =
machineName:
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
@ -100,15 +58,11 @@ let
(machines.${name} or { })
(
{
# Settings
clanCore.clanDir = directory;
# Inherited from clan wide settings
clanCore.clanName = meta.name or clanName;
clanCore.clanIcon = meta.icon or clanIcon;
# Machine specific settings
clanCore.machineName = name;
networking.hostName = lib.mkDefault name;
clanCore.clanName = clanName;
clanCore.clanIcon = clanIcon;
clanCore.clanDir = directory;
clanCore.machineName = name;
nixpkgs.hostPlatform = lib.mkDefault system;
# speeds up nix commands by using the nixpkgs from the host system (especially useful in VMs)
@ -173,15 +127,10 @@ let
) supportedSystems
);
in
builtins.deepSeq deprecationWarnings {
{
inherit nixosConfigurations;
clanInternals = {
# Evaluated clan meta
# Merged /clan/meta.json with overrides from buildClan
meta = mergedMeta;
# machine specifics
machines = configsPerSystem;
machinesFunc = configsFuncPerSystem;
all-machines-json = lib.mapAttrs (

View File

@ -83,20 +83,20 @@ rec {
in
# either type
# TODO: if all nested options are excluded, the parent should be excluded too
# TODO: if all nested optiosn are excluded, the parent sould be excluded too
if
option.type.name or null == "either" || option.type.name or null == "coercedTo"
option.type.name or null == "either"
# return jsonschema property definition for either
then
let
optionsList' = [
{
type = option.type.nestedTypes.left or option.type.nestedTypes.coercedType;
type = option.type.nestedTypes.left;
_type = "option";
loc = option.loc;
}
{
type = option.type.nestedTypes.right or option.type.nestedTypes.finalType;
type = option.type.nestedTypes.right;
_type = "option";
loc = option.loc;
}
@ -157,21 +157,12 @@ rec {
# TODO: Add support for intMatching in jsonschema
# parse port type aka. "unsignedInt16"
else if
option.type.name == "unsignedInt16"
|| option.type.name == "unsignedInt"
|| option.type.name == "pkcs11"
|| option.type.name == "intBetween"
then
else if option.type.name == "unsignedInt16" then
default // example // description // { type = "integer"; }
# parse string
# TODO: parse more precise string types
else if
option.type.name == "str"
|| option.type.name == "singleLineStr"
|| option.type.name == "passwdEntry str"
|| option.type.name == "passwdEntry path"
# return jsonschema property definition for string
then
default // example // description // { type = "string"; }

View File

@ -1,17 +0,0 @@
{ lib, pkgs, ... }:
{
# use latest kernel we can support to get more hardware support
boot.kernelPackages =
lib.mkForce
(pkgs.zfs.override { removeLinuxDRM = pkgs.hostPlatform.isAarch64; }).latestCompatibleLinuxPackages;
boot.zfs.removeLinuxDRM = lib.mkDefault pkgs.hostPlatform.isAarch64;
# Enable bcachefs support
boot.supportedFilesystems.bcachefs = lib.mkDefault true;
environment.systemPackages = with pkgs; [
bcachefs-tools
keyutils
];
}

View File

@ -122,7 +122,7 @@ in
cores = lib.mkOption {
type = lib.types.ints.positive;
default = 1;
description = ''
description = lib.mdDoc ''
Specify the number of cores the guest is permitted to use.
The number can be higher than the available cores on the
host system.
@ -132,7 +132,7 @@ in
memorySize = lib.mkOption {
type = lib.types.ints.positive;
default = 1024;
description = ''
description = lib.mdDoc ''
The memory size in megabytes of the virtual machine.
'';
};
@ -140,7 +140,7 @@ in
graphics = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
description = lib.mdDoc ''
Whether to run QEMU with a graphics window, or in nographic mode.
Serial console will be enabled on both settings, but this will
change the preferred console.
@ -150,7 +150,7 @@ in
waypipe = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
description = lib.mdDoc ''
Whether to use waypipe for native wayland passthrough, or not.
'';
};

View File

@ -81,7 +81,7 @@ in
};
};
settings = lib.mkOption {
description = "override the network config in /var/lib/zerotier/bla/$network.json";
description = lib.mdDoc "override the network config in /var/lib/zerotier/bla/$network.json";
type = lib.types.submodule { freeformType = (pkgs.formats.json { }).type; };
};
};

View File

@ -2,15 +2,13 @@
{
flake.nixosModules = {
hidden-ssh-announce.imports = [ ./hidden-ssh-announce.nix ];
bcachefs.imports = [ ./bcachefs.nix ];
installer.imports = [
./installer
self.nixosModules.hidden-ssh-announce
self.nixosModules.bcachefs
inputs.disko.nixosModules.disko
];
clanCore.imports = [
inputs.sops-nix.nixosModules.sops
inputs.disko.nixosModules.default
./clanCore
./iso
(

View File

@ -4,38 +4,6 @@
modulesPath,
...
}:
let
network-status = pkgs.writeShellScript "network-status" ''
export PATH=${
lib.makeBinPath (
with pkgs;
[
iproute2
coreutils
gnugrep
nettools
gum
]
)
}
set -efu -o pipefail
msgs=()
if [[ -e /var/shared/qrcode.utf8 ]]; then
qrcode=$(gum style --border-foreground 240 --border normal "$(< /var/shared/qrcode.utf8)")
msgs+=("$qrcode")
fi
network_status="Local network addresses:
$(ip -brief -color addr | grep -v 127.0.0.1)
$([[ -e /var/shared/onion-hostname ]] && echo "Onion address: $(cat /var/shared/onion-hostname)" || echo "Onion address: Waiting for tor network to be ready...")
Multicast DNS: $(hostname).local"
network_status=$(gum style --border-foreground 240 --border normal "$network_status")
msgs+=("$network_status")
msgs+=("Press 'Ctrl-C' for console access")
gum join --vertical "''${msgs[@]}"
'';
in
{
############################################
# #
@ -43,21 +11,19 @@ in
# $ qemu-kvm result/stick.raw -snapshot #
# #
############################################
systemd.tmpfiles.rules = [ "d /var/shared 0777 root root - -" ];
imports = [
(modulesPath + "/profiles/installation-device.nix")
(modulesPath + "/profiles/all-hardware.nix")
(modulesPath + "/profiles/base.nix")
(modulesPath + "/installer/cd-dvd/iso-image.nix")
];
########################################################################################################
# #
# Copied from: #
# https://github.com/nix-community/nixos-images/blob/main/nix/image-installer/module.nix#L46C3-L117C6 #
# #
########################################################################################################
systemd.tmpfiles.rules = [ "d /var/shared 0777 root root - -" ];
services.openssh.settings.PermitRootLogin = lib.mkForce "prohibit-password";
services.openssh.settings.PermitRootLogin = "yes";
system.activationScripts.root-password = ''
mkdir -p /var/shared
${pkgs.pwgen}/bin/pwgen -s 16 1 > /var/shared/root-password
echo "root:$(cat /var/shared/root-password)" | chpasswd
'';
hidden-ssh-announce = {
enable = true;
script = pkgs.writeShellScript "write-hostname" ''
@ -78,48 +44,26 @@ in
echo "$1" > /var/shared/onion-hostname
local_addrs=$(ip -json addr | jq '[map(.addr_info) | flatten | .[] | select(.scope == "global") | .local]')
jq -nc \
--arg password "$(cat /var/shared/root-password)" \
--arg onion_address "$(cat /var/shared/onion-hostname)" \
--argjson local_addrs "$local_addrs" \
'{ pass: null, tor: $onion_address, addrs: $local_addrs }' \
'{ pass: $password, onion_address: $onion_address, addrs: $local_addrs }' \
> /var/shared/login.json
cat /var/shared/login.json | qrencode -s 2 -m 2 -t utf8 -o /var/shared/qrcode.utf8
cat /var/shared/login.json | qrencode -t utf8 -o /var/shared/qrcode.utf8
'';
};
services.getty.autologinUser = lib.mkForce "root";
console.earlySetup = true;
console.font = lib.mkDefault "${pkgs.terminus_font}/share/consolefonts/ter-u22n.psf.gz";
# Less ipv6 addresses to reduce the noise
networking.tempAddresses = "disabled";
# Tango theme: https://yayachiken.net/en/posts/tango-colors-in-terminal/
console.colors = lib.mkDefault [
"000000"
"CC0000"
"4E9A06"
"C4A000"
"3465A4"
"75507B"
"06989A"
"D3D7CF"
"555753"
"EF2929"
"8AE234"
"FCE94F"
"739FCF"
"AD7FA8"
"34E2E2"
"EEEEEC"
];
programs.bash.interactiveShellInit = ''
if [[ "$(tty)" =~ /dev/(tty1|hvc0|ttyS0)$ ]]; then
# workaround for https://github.com/NixOS/nixpkgs/issues/219239
systemctl restart systemd-vconsole-setup.service
watch --no-title --color ${network-status}
echo -n 'waiting for tor to generate the hidden service'
until test -e /var/shared/qrcode.utf8; do echo -n .; sleep 1; done
echo
echo "Root password: $(cat /var/shared/root-password)"
echo "Onion address: $(cat /var/shared/onion-hostname)"
echo "Local network addresses:"
${pkgs.iproute}/bin/ip -brief -color addr | grep -v 127.0.0.1
cat /var/shared/qrcode.utf8
fi
'';
isoImage.squashfsCompression = "zstd";
}

View File

@ -43,7 +43,6 @@ let
boot = {
size = "1M";
type = "EF02"; # for grub MBR
priority = 1; # Needs to be first partition
};
ESP = {
size = "100M";

View File

@ -1,15 +0,0 @@
import json
from clan_cli.api import API
def main() -> None:
schema = API.to_json_schema()
print(
f"""export const schema = {json.dumps(schema, indent=2)} as const;
"""
)
if __name__ == "__main__":
main()

View File

@ -51,19 +51,13 @@ class AppendOptionAction(argparse.Action):
lst.append(values[1])
def flake_path(arg: str) -> str | Path:
flake_dir = Path(arg).resolve()
if flake_dir.exists() and flake_dir.is_dir():
return flake_dir
return arg
def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog=prog, description="cLAN tool")
def add_common_flags(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--debug",
help="Enable debug logging",
action="store_true",
default=False,
)
parser.add_argument(
@ -75,6 +69,12 @@ def add_common_flags(parser: argparse.ArgumentParser) -> None:
default=[],
)
def flake_path(arg: str) -> str | Path:
flake_dir = Path(arg).resolve()
if flake_dir.exists() and flake_dir.is_dir():
return flake_dir
return arg
parser.add_argument(
"--flake",
help="path to the flake where the clan resides in, can be a remote flake or local, can be set through the [CLAN_DIR] environment variable",
@ -83,224 +83,51 @@ def add_common_flags(parser: argparse.ArgumentParser) -> None:
type=flake_path,
)
def register_common_flags(parser: argparse.ArgumentParser) -> None:
has_subparsers = False
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
for choice, child_parser in action.choices.items():
has_subparsers = True
register_common_flags(child_parser)
if not has_subparsers:
add_common_flags(parser)
def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog=prog,
description="The clan cli tool.",
epilog=(
"""
Online reference for the clan cli tool: https://docs.clan.lol/reference/cli/
For more detailed information, visit: https://docs.clan.lol
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
subparsers = parser.add_subparsers()
parser_backups = subparsers.add_parser(
"backups",
help="manage backups of clan machines",
description="manage backups of clan machines",
epilog=(
"""
This subcommand provides an interface to backups that clan machines expose.
Examples:
$ clan backups list [MACHINE]
List backups for the machine [MACHINE]
$ clan backups create [MACHINE]
Create a backup for the machine [MACHINE].
$ clan backups restore [MACHINE] [PROVIDER] [NAME]
The backup to restore for the machine [MACHINE] with the configured [PROVIDER]
with the name [NAME].
For more detailed information, visit: https://docs.clan.lol/getting-started/backups/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
backups.register_parser(parser_backups)
parser_flake = subparsers.add_parser(
"flakes",
help="create a clan flake inside the current directory",
description="create a clan flake inside the current directory",
epilog=(
"""
Examples:
$ clan flakes create [DIR]
Will create a new clan flake in the specified directory and create it if it
doesn't exist yet. The flake will be created from a default template.
For more detailed information, visit: https://docs.clan.lol/getting-started
"""
),
formatter_class=argparse.RawTextHelpFormatter,
"flakes", help="create a clan flake inside the current directory"
)
flakes.register_parser(parser_flake)
parser_config = subparsers.add_parser(
"config",
help="read a nixos configuration option",
description="read a nixos configuration option",
epilog=(
"""
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser_config = subparsers.add_parser("config", help="set nixos configuration")
config.register_parser(parser_config)
parser_ssh = subparsers.add_parser(
"ssh",
help="ssh to a remote machine",
description="ssh to a remote machine",
epilog=(
"""
This subcommand allows seamless ssh access to the nixos-image builders.
Examples:
$ 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
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine")
ssh_cli.register_parser(parser_ssh)
parser_secrets = subparsers.add_parser(
"secrets",
help="manage secrets",
description="manage secrets",
epilog=(
"""
This subcommand provides an interface to secret facts.
Examples:
$ clan secrets list [regex]
Will list secrets for all managed machines.
It accepts an optional regex, allowing easy filtering of returned secrets.
$ clan secrets get [SECRET]
Will display the content of the specified secret.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser_secrets = subparsers.add_parser("secrets", help="manage secrets")
secrets.register_parser(parser_secrets)
parser_facts = subparsers.add_parser(
"facts",
help="manage facts",
description="manage facts",
epilog=(
"""
This subcommand provides an interface to facts of clan machines.
Facts are artifacts that a service can generate.
There are public and secret facts.
Public facts can be referenced by other machines directly.
Public facts can include: ip addresses, public keys.
Secret facts can include: passwords, private keys.
A service is an included clan-module that implements facts generation functionality.
For example the zerotier module will generate private and public facts.
In this case the public fact will be the resulting zerotier-ip of the machine.
The secret fact will be the zerotier-identity-secret, which is used by zerotier
to prove the machine has control of the zerotier-ip.
Examples:
$ clan facts generate
Will generate facts for all machines.
$ clan facts generate --service [SERVICE] --regenerate
Will regenerate facts, if they are already generated for a specific service.
This is especially useful for resetting certain passwords while leaving the rest
of the facts for a machine in place.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser_facts = subparsers.add_parser("facts", help="manage facts")
facts.register_parser(parser_facts)
parser_machine = subparsers.add_parser(
"machines",
help="manage machines and their configuration",
description="manage machines and their configuration",
epilog=(
"""
This subcommand provides an interface to machines managed by clan.
Examples:
$ clan machines list
List all the machines managed by clan.
$ clan machines update [MACHINES]
Will update the specified machine [MACHINE], if [MACHINE] is omitted, the command
will attempt to update every configured machine.
$ clan machines install [MACHINES] [TARGET_HOST]
Will install the specified machine [MACHINE], to the specified [TARGET_HOST].
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
"""
),
formatter_class=argparse.RawTextHelpFormatter,
"machines", help="manage machines and their configuration"
)
machines.register_parser(parser_machine)
parser_vms = subparsers.add_parser(
"vms", help="manage virtual machines", description="manage virtual machines"
)
parser_vms = subparsers.add_parser("vms", help="manage virtual machines")
vms.register_parser(parser_vms)
parser_history = subparsers.add_parser(
"history",
help="manage history",
description="manage history",
)
parser_history = subparsers.add_parser("history", help="manage history")
history.register_parser(parser_history)
parser_flash = subparsers.add_parser(
"flash",
help="flash machines to usb sticks or into isos",
description="flash machines to usb sticks or into isos",
"flash", help="flash machines to usb sticks or into isos"
)
flash.register_parser(parser_flash)
if argcomplete:
argcomplete.autocomplete(parser)
register_common_flags(parser)
return parser

View File

@ -1,90 +0,0 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic, Literal, TypeVar
T = TypeVar("T")
ResponseDataType = TypeVar("ResponseDataType")
@dataclass
class ApiError:
message: str
description: str | None
location: list[str] | None
@dataclass
class ApiResponse(Generic[ResponseDataType]):
status: Literal["success", "error"]
errors: list[ApiError] | None
data: ResponseDataType | None
class _MethodRegistry:
def __init__(self) -> None:
self._registry: dict[str, Callable[[Any], Any]] = {}
def register(self, fn: Callable[..., T]) -> Callable[..., T]:
self._registry[fn.__name__] = fn
return fn
def to_json_schema(self) -> dict[str, Any]:
# Import only when needed
from typing import get_type_hints
from clan_cli.api.util import type_to_dict
api_schema: dict[str, Any] = {
"$comment": "An object containing API methods. ",
"type": "object",
"additionalProperties": False,
"required": [func_name for func_name in self._registry.keys()],
"properties": {},
}
for name, func in self._registry.items():
hints = get_type_hints(func)
serialized_hints = {
key: type_to_dict(
value, scope=name + " argument" if key != "return" else "return"
)
for key, value in hints.items()
}
return_type = serialized_hints.pop("return")
api_schema["properties"][name] = {
"type": "object",
"required": ["arguments", "return"],
"additionalProperties": False,
"properties": {
"return": return_type,
"arguments": {
"type": "object",
"required": [k for k in serialized_hints.keys()],
"additionalProperties": False,
"properties": serialized_hints,
},
},
}
return api_schema
def get_method_argtype(self, method_name: str, arg_name: str) -> Any:
from inspect import signature
func = self._registry.get(method_name, None)
if func:
sig = signature(func)
param = sig.parameters.get(arg_name)
if param:
param_class = param.annotation
return param_class
return None
API = _MethodRegistry()

View File

@ -1,84 +0,0 @@
import dataclasses
import pathlib
from types import NoneType, UnionType
from typing import Any, Union
def type_to_dict(t: Any, scope: str = "") -> dict:
if t is None:
return {"type": "null"}
if dataclasses.is_dataclass(t):
fields = dataclasses.fields(t)
properties = {
f.name: type_to_dict(f.type, f"{scope} {t.__name__}.{f.name}")
for f in fields
}
required = [pn for pn, pv in properties.items() if "null" not in pv["type"]]
return {
"type": "object",
"properties": properties,
"required": required,
# Dataclasses can only have the specified properties
"additionalProperties": False,
}
elif type(t) is UnionType:
return {
"type": [type_to_dict(arg, scope)["type"] for arg in t.__args__],
}
elif hasattr(t, "__origin__"): # Check if it's a generic type
origin = getattr(t, "__origin__", None)
if origin is None:
# Non-generic user-defined or built-in type
# TODO: handle custom types
raise BaseException("Unhandled Type: ", origin)
elif origin is Union:
return {"type": [type_to_dict(arg, scope)["type"] for arg in t.__args__]}
elif issubclass(origin, list):
return {"type": "array", "items": type_to_dict(t.__args__[0], scope)}
elif issubclass(origin, dict):
value_type = t.__args__[1]
if value_type is Any:
return {"type": "object", "additionalProperties": True}
else:
return {
"type": "object",
"additionalProperties": type_to_dict(value_type, scope),
}
raise BaseException(f"Error api type not yet supported {t!s}")
elif isinstance(t, type):
if t is str:
return {"type": "string"}
if t is int:
return {"type": "integer"}
if t is float:
return {"type": "number"}
if t is bool:
return {"type": "boolean"}
if t is object:
return {"type": "object"}
if t is Any:
raise BaseException(
f"Usage of the Any type is not supported for API functions. In: {scope}"
)
if t is pathlib.Path:
return {
# TODO: maybe give it a pattern for URI
"type": "string",
}
# Optional[T] gets internally transformed Union[T,NoneType]
if t is NoneType:
return {"type": "null"}
raise BaseException(f"Error primitive type not supported {t!s}")
else:
raise BaseException(f"Error type not supported {t!s}")

View File

@ -2,7 +2,6 @@ import argparse
import json
import logging
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
from ..machines.machines import Machine
@ -34,17 +33,13 @@ def create_backup(machine: Machine, provider: str | None = None) -> None:
def create_command(args: argparse.Namespace) -> None:
if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake)
create_backup(machine=machine, provider=args.provider)
def register_create_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machine", type=str, help="machine in the flake to create backups of"
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument("--provider", type=str, help="backup provider to use")
parser.set_defaults(func=create_command)

View File

@ -3,7 +3,6 @@ import json
import subprocess
from dataclasses import dataclass
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
from ..machines.machines import Machine
@ -49,8 +48,6 @@ def list_backups(machine: Machine, provider: str | None = None) -> list[Backup]:
def list_command(args: argparse.Namespace) -> None:
if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake)
backups = list_backups(machine=machine, provider=args.provider)
for backup in backups:
@ -58,9 +55,8 @@ def list_command(args: argparse.Namespace) -> None:
def register_list_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machine", type=str, help="machine in the flake to show backups of"
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument("--provider", type=str, help="backup provider to filter by")
parser.set_defaults(func=list_command)

View File

@ -62,8 +62,6 @@ def restore_backup(
def restore_command(args: argparse.Namespace) -> None:
if args.flake is None:
raise ClanError("Could not find clan flake toplevel directory")
machine = Machine(name=args.machine, flake=args.flake)
restore_backup(
machine=machine,

View File

@ -56,7 +56,7 @@ def handle_output(process: subprocess.Popen, log: Log) -> tuple[str, str]:
sys.stderr.buffer.write(ret)
sys.stderr.flush()
stderr_buf += ret
return stdout_buf.decode("utf-8", "replace"), stderr_buf.decode("utf-8", "replace")
return stdout_buf.decode("utf-8"), stderr_buf.decode("utf-8")
class TimeTable:
@ -101,19 +101,13 @@ TIME_TABLE = TimeTable()
def run(
cmd: list[str],
*,
input: bytes | None = None, # noqa: A002
env: dict[str, str] | None = None,
cwd: Path = Path.cwd(),
log: Log = Log.STDERR,
check: bool = True,
error_msg: str | None = None,
) -> CmdOut:
if input:
glog.debug(
f"""$: echo "{input.decode('utf-8', 'replace')}" | {shlex.join(cmd)} \nCaller: {get_caller()}"""
)
else:
glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
glog.debug(f"$: {shlex.join(cmd)} \nCaller: {get_caller()}")
tstart = datetime.now()
# Start the subprocess
@ -126,10 +120,7 @@ def run(
)
stdout_buf, stderr_buf = handle_output(process, log)
if input:
process.communicate(input)
else:
process.wait()
rc = process.wait()
tend = datetime.now()
global TIME_TABLE
@ -145,27 +136,7 @@ def run(
msg=error_msg,
)
if check and process.returncode != 0:
if check and rc != 0:
raise ClanCmdError(cmd_out)
return cmd_out
def run_no_stdout(
cmd: list[str],
*,
env: dict[str, str] | None = None,
cwd: Path = Path.cwd(),
log: Log = Log.STDERR,
check: bool = True,
error_msg: str | None = None,
) -> CmdOut:
"""
Like run, but automatically suppresses stdout, if not in DEBUG log level.
If in DEBUG log level the stdout of commands will be shown.
"""
if logging.getLogger(__name__.split(".")[0]).isEnabledFor(logging.DEBUG):
return run(cmd, env=env, log=log, check=check, error_msg=error_msg)
else:
log = Log.NONE
return run(cmd, env=env, log=log, check=check, error_msg=error_msg)

View File

@ -1,157 +0,0 @@
import argparse
import json
import subprocess
import threading
from collections.abc import Callable, Iterable
from types import ModuleType
from typing import Any
from .cmd import run
from .nix import nix_eval
"""
This module provides dynamic completions.
The completions should feel fast.
We target a maximum of 1second on our average machine.
"""
argcomplete: ModuleType | None = None
try:
import argcomplete # type: ignore[no-redef]
except ImportError:
pass
# The default completion timeout for commands
COMPLETION_TIMEOUT: int = 3
def clan_dir(flake: str | None) -> str | None:
from .dirs import get_clan_flake_toplevel_or_env
path_result = get_clan_flake_toplevel_or_env()
return str(path_result) if path_result is not None else None
def complete_machines(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for machine names configured in the clan.
"""
machines: list[str] = []
def run_cmd() -> None:
try:
# In my tests this was consistently faster than:
# nix eval .#nixosConfigurations --apply builtins.attrNames
cmd = ["nix", "flake", "show", "--system", "no-eval", "--json"]
if (clan_dir_result := clan_dir(None)) is not None:
cmd.append(clan_dir_result)
result = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
)
data = json.loads(result.stdout)
try:
machines.extend(data.get("nixosConfigurations").keys())
except KeyError:
pass
except subprocess.CalledProcessError:
pass
thread = threading.Thread(target=run_cmd)
thread.start()
thread.join(timeout=COMPLETION_TIMEOUT)
if thread.is_alive():
return iter([])
machines_dict = {name: "machine" for name in machines}
return machines_dict
def complete_services_for_machine(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for machine facts generation services.
"""
services: list[str] = []
# TODO: consolidate, if multiple machines are used
machines: list[str] = parsed_args.machines
def run_cmd() -> None:
try:
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
services_result = json.loads(
run(
nix_eval(
flags=[
f"{flake}#nixosConfigurations.{machines[0]}.config.clanCore.facts.services",
"--apply",
"builtins.attrNames",
],
),
).stdout.strip()
)
services.extend(services_result)
except subprocess.CalledProcessError:
pass
thread = threading.Thread(target=run_cmd)
thread.start()
thread.join(timeout=COMPLETION_TIMEOUT)
if thread.is_alive():
return iter([])
services_dict = {name: "service" for name in services}
return services_dict
def complete_secrets(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for clan secrets
"""
from pathlib import Path
from .secrets.secrets import ListSecretsOptions, list_secrets
if (clan_dir_result := clan_dir(None)) is not None:
flake = clan_dir_result
else:
flake = "."
options = ListSecretsOptions(
flake=Path(flake),
pattern=None,
)
secrets = list_secrets(options.flake, options.pattern)
secrets_dict = {name: "secret" for name in secrets}
return secrets_dict
def add_dynamic_completer(
action: argparse.Action,
completer: Callable[..., Iterable[str]],
) -> None:
"""
Add a completion function to an argparse action, this will only be added,
if the argcomplete module is loaded.
"""
if argcomplete:
# mypy doesn't check this correctly, so we ignore it
action.completer = completer # type: ignore[attr-defined]

View File

@ -150,15 +150,6 @@ def read_machine_option_value(
return out
def get_option(args: argparse.Namespace) -> None:
print(
read_machine_option_value(
args.flake, args.machine, args.option, args.show_trace
)
)
# Currently writing is disabled
def get_or_set_option(args: argparse.Namespace) -> None:
if args.value == []:
print(
@ -316,7 +307,7 @@ def register_parser(
)
# inject callback function to process the input later
parser.set_defaults(func=get_option)
parser.set_defaults(func=get_or_set_option)
parser.add_argument(
"--machine",
"-m",
@ -354,6 +345,13 @@ def register_parser(
type=str,
)
parser.add_argument(
"value",
# force this arg to be set
nargs="*",
help="option value to set (if omitted, the current value is printed)",
)
def main(argv: list[str] | None = None) -> None:
if argv is None:

View File

@ -16,116 +16,16 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
required=True,
)
check_parser = subparser.add_parser(
"check",
help="check if facts are up to date",
epilog=(
"""
This subcommand allows checking if all facts are up to date.
Examples:
$ clan facts check [MACHINE]
Will check facts for the specified machine.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
check_parser = subparser.add_parser("check", help="check if facts are up to date")
register_check_parser(check_parser)
list_parser = subparser.add_parser(
"list",
help="list all facts",
epilog=(
"""
This subcommand allows listing all public facts for a specific machine.
The resulting list will be a json string with the name of the fact as its key
and the fact itself as it's value.
This is how an example output might look like:
```
{
"[FACT_NAME]": "[FACT]"
}
```
Examples:
$ clan facts list [MACHINE]
Will list facts for the specified machine.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
list_parser = subparser.add_parser("list", help="list all facts")
register_list_parser(list_parser)
parser_generate = subparser.add_parser(
"generate",
help="generate public and secret facts for machines",
epilog=(
"""
This subcommand allows control of the generation of facts.
Often this function will be invoked automatically on deploying machines,
but there are situations the user may want to have more granular control,
especially for the regeneration of certain services.
A service is an included clan-module that implements facts generation functionality.
For example the zerotier module will generate private and public facts.
In this case the public fact will be the resulting zerotier-ip of the machine.
The secret fact will be the zerotier-identity-secret, which is used by zerotier
to prove the machine has control of the zerotier-ip.
Examples:
$ clan facts generate
Will generate facts for all machines.
$ clan facts generate [MACHINE]
Will generate facts for the specified machine.
$ clan facts generate [MACHINE] --service [SERVICE]
Will generate facts for the specified machine for the specified service.
$ clan facts generate --service [SERVICE] --regenerate
Will regenerate facts, if they are already generated for a specific service.
This is especially useful for resetting certain passwords while leaving the rest
of the facts for a machine in place.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
"generate", help="generate secrets for machines if they don't exist yet"
)
register_generate_parser(parser_generate)
parser_upload = subparser.add_parser(
"upload",
help="upload secrets for machines",
epilog=(
"""
This subcommand allows uploading secrets to remote machines.
If using sops as a secret backend it will upload the private key to the machine.
If using password store it uploads all the secrets you manage to the machine.
The default backend is sops.
Examples:
$ clan facts upload [MACHINE]
Will upload secrets to a specific machine.
For more detailed information, visit: https://docs.clan.lol/getting-started/secrets/
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
parser_upload = subparser.add_parser("upload", help="upload secrets for machines")
register_upload_parser(parser_upload)

View File

@ -2,7 +2,6 @@ import argparse
import importlib
import logging
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine
log = logging.getLogger(__name__)
@ -55,12 +54,10 @@ def check_command(args: argparse.Namespace) -> None:
def register_check_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machine",
help="The machine to check secrets for",
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument(
"--service",
help="the service to check",

View File

@ -9,11 +9,6 @@ from tempfile import TemporaryDirectory
from clan_cli.cmd import run
from ..completions import (
add_dynamic_completer,
complete_machines,
complete_services_for_machine,
)
from ..errors import ClanError
from ..git import commit_files
from ..machines.inventory import get_all_machines, get_selected_machines
@ -32,14 +27,12 @@ def read_multiline_input(prompt: str = "Finish with Ctrl-D") -> str:
"""
print(prompt, flush=True)
proc = subprocess.run(["cat"], stdout=subprocess.PIPE, text=True)
log.info("Input received. Processing...")
return proc.stdout
def generate_service_facts(
machine: Machine,
service: str,
regenerate: bool,
secret_facts_store: SecretStoreBase,
public_facts_store: FactStoreBase,
tmpdir: Path,
@ -49,7 +42,7 @@ def generate_service_facts(
# check if all secrets exist and generate them if at least one is missing
needs_regeneration = not check_secrets(machine, service=service)
log.debug(f"{service} needs_regeneration: {needs_regeneration}")
if not (needs_regeneration or regenerate):
if not needs_regeneration:
return False
if not isinstance(machine.flake, Path):
msg = f"flake is not a Path: {machine.flake}"
@ -141,11 +134,7 @@ def prompt_func(text: str) -> str:
def _generate_facts_for_machine(
machine: Machine,
service: str | None,
regenerate: bool,
tmpdir: Path,
prompt: Callable[[str], str] = prompt_func,
machine: Machine, tmpdir: Path, prompt: Callable[[str], str] = prompt_func
) -> bool:
local_temp = tmpdir / machine.name
local_temp.mkdir()
@ -156,23 +145,10 @@ def _generate_facts_for_machine(
public_facts_store = public_facts_module.FactStore(machine=machine)
machine_updated = False
if service and service not in machine.facts_data:
services = list(machine.facts_data.keys())
raise ClanError(
f"Could not find service with name: {service}. The following services are available: {services}"
)
if service:
machine_service_facts = {service: machine.facts_data[service]}
else:
machine_service_facts = machine.facts_data
for service in machine_service_facts:
for service in machine.facts_data:
machine_updated |= generate_service_facts(
machine=machine,
service=service,
regenerate=regenerate,
secret_facts_store=secret_facts_store,
public_facts_store=public_facts_store,
tmpdir=local_temp,
@ -185,10 +161,7 @@ def _generate_facts_for_machine(
def generate_facts(
machines: list[Machine],
service: str | None,
regenerate: bool,
prompt: Callable[[str], str] = prompt_func,
machines: list[Machine], prompt: Callable[[str], str] = prompt_func
) -> bool:
was_regenerated = False
with TemporaryDirectory() as tmp:
@ -197,9 +170,7 @@ def generate_facts(
for machine in machines:
errors = 0
try:
was_regenerated |= _generate_facts_for_machine(
machine, service, regenerate, tmpdir, prompt
)
was_regenerated |= _generate_facts_for_machine(machine, tmpdir, prompt)
except Exception as exc:
log.error(f"Failed to generate facts for {machine.name}: {exc}")
errors += 1
@ -215,35 +186,18 @@ def generate_facts(
def generate_command(args: argparse.Namespace) -> None:
if len(args.machines) == 0:
machines = get_all_machines(args.flake, args.option)
machines = get_all_machines(args.flake)
else:
machines = get_selected_machines(args.flake, args.option, args.machines)
generate_facts(machines, args.service, args.regenerate)
machines = get_selected_machines(args.flake, args.machines)
generate_facts(machines)
def register_generate_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machines",
type=str,
help="machine to generate facts for. if empty, generate facts for all machines",
nargs="*",
default=[],
)
add_dynamic_completer(machines_parser, complete_machines)
service_parser = parser.add_argument(
"--service",
type=str,
help="service to generate facts for, if empty, generate facts for every service",
default=None,
)
add_dynamic_completer(service_parser, complete_services_for_machine)
parser.add_argument(
"--regenerate",
type=bool,
action=argparse.BooleanOptionalAction,
help="whether to regenerate facts for the specified machine",
default=None,
)
parser.set_defaults(func=generate_command)

View File

@ -3,7 +3,6 @@ import importlib
import json
import logging
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine
log = logging.getLogger(__name__)
@ -27,21 +26,12 @@ def get_all_facts(machine: Machine) -> dict:
def get_command(args: argparse.Namespace) -> None:
machine = Machine(name=args.machine, flake=args.flake)
# the raw_facts are bytestrings making them not json serializable
raw_facts = get_all_facts(machine)
facts = dict()
for key in raw_facts["TODO"]:
facts[key] = raw_facts["TODO"][key].decode("utf8")
print(json.dumps(facts, indent=4))
print(json.dumps(get_all_facts(machine), indent=4))
def register_list_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machine",
help="The machine to print facts for",
)
add_dynamic_completer(machines_parser, complete_machines)
parser.set_defaults(func=get_command)

View File

@ -2,7 +2,6 @@ import os
import subprocess
from pathlib import Path
from clan_cli.cmd import Log, run
from clan_cli.machines.machines import Machine
from clan_cli.nix import nix_shell
@ -16,25 +15,25 @@ class SecretStore(SecretStoreBase):
def set(
self, service: str, name: str, value: bytes, groups: list[str]
) -> Path | None:
run(
subprocess.run(
nix_shell(
["nixpkgs#pass"],
["pass", "insert", "-m", f"machines/{self.machine.name}/{name}"],
),
input=value,
log=Log.BOTH,
error_msg=f"Failed to insert secret {name}",
check=True,
)
return None # we manage the files outside of the git repo
def get(self, service: str, name: str) -> bytes:
return run(
return subprocess.run(
nix_shell(
["nixpkgs#pass"],
["pass", "show", f"machines/{self.machine.name}/{name}"],
),
error_msg=f"Failed to get secret {name}",
).stdout.encode("utf-8")
check=True,
stdout=subprocess.PIPE,
).stdout
def exists(self, service: str, name: str) -> bool:
password_store = os.environ.get(
@ -49,7 +48,7 @@ class SecretStore(SecretStoreBase):
)
hashes = []
hashes.append(
run(
subprocess.run(
nix_shell(
["nixpkgs#git"],
[
@ -62,15 +61,13 @@ class SecretStore(SecretStoreBase):
f"machines/{self.machine.name}",
],
),
check=False,
)
.stdout.encode("utf-8")
.strip()
stdout=subprocess.PIPE,
).stdout.strip()
)
for symlink in Path(password_store).glob(f"machines/{self.machine.name}/**/*"):
if symlink.is_symlink():
hashes.append(
run(
subprocess.run(
nix_shell(
["nixpkgs#git"],
[
@ -83,10 +80,8 @@ class SecretStore(SecretStoreBase):
str(symlink),
],
),
check=False,
)
.stdout.encode("utf-8")
.strip()
stdout=subprocess.PIPE,
).stdout.strip()
)
# we sort the hashes to make sure that the order is always the same

View File

@ -5,7 +5,6 @@ from pathlib import Path
from tempfile import TemporaryDirectory
from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines
from ..machines.machines import Machine
from ..nix import nix_shell
@ -33,8 +32,6 @@ def upload_secrets(machine: Machine) -> None:
" ".join(["ssh"] + ssh_cmd[2:]),
"-az",
"--delete",
"--chown=root:root",
"--chmod=D700,F600",
f"{tempdir!s}/",
f"{host.user}@{host.host}:{machine.secrets_upload_directory}/",
],
@ -49,10 +46,8 @@ def upload_command(args: argparse.Namespace) -> None:
def register_upload_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machine",
help="The machine to upload secrets to",
)
add_dynamic_completer(machines_parser, complete_machines)
parser.set_defaults(func=upload_command)

View File

@ -43,10 +43,6 @@ def create_flake(directory: Path, url: str) -> dict[str, CmdOut]:
out = run(command, cwd=directory)
response["git config"] = out
command = ["nix", "flake", "update"]
out = run(command, cwd=directory)
response["flake update"] = out
return response

View File

@ -39,7 +39,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
system = config["system"]
# Check if the machine exists
machines = list_machines(flake_url, False)
machines = list_machines(flake_url)
if machine_name not in machines:
raise ClanError(
f"Machine {machine_name} not found in {flake_url}. Available machines: {', '.join(machines)}"
@ -53,7 +53,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
gcroot_icon: Path = machine_gcroot(flake_url=str(flake_url)) / vm.machine_name
nix_add_to_gcroots(vm.machine_icon, gcroot_icon)
# Get the Clan name
# Get the cLAN name
cmd = nix_eval(
[
f'{flake_url}#clanInternals.machines."{system}"."{machine_name}".config.clanCore.clanName'
@ -70,7 +70,7 @@ def inspect_flake(flake_url: str | Path, machine_name: str) -> FlakeConfig:
)
res = run_cmd(cmd)
# If the icon is null, no icon is set for this Clan
# If the icon is null, no icon is set for this cLAN
if res == "null":
icon_path = None
else:
@ -113,7 +113,7 @@ def inspect_command(args: argparse.Namespace) -> None:
res = inspect_flake(
flake_url=inspect_options.flake, machine_name=inspect_options.machine
)
print("Clan name:", res.clan_name)
print("cLAN name:", res.clan_name)
print("Icon:", res.icon)
print("Description:", res.description)
print("Last updated:", res.last_updated)

View File

@ -1,8 +1,8 @@
import argparse
import importlib
import json
import logging
import os
import shlex
import shutil
import textwrap
from collections.abc import Sequence
@ -12,7 +12,6 @@ from tempfile import TemporaryDirectory
from typing import Any
from .cmd import Log, run
from .completions import add_dynamic_completer, complete_machines
from .errors import ClanError
from .facts.secret_modules import SecretStoreBase
from .machines.machines import Machine
@ -22,15 +21,7 @@ log = logging.getLogger(__name__)
def flash_machine(
machine: Machine,
*,
mode: str,
disks: dict[str, str],
system_config: dict[str, Any],
dry_run: bool,
write_efi_boot_entries: bool,
debug: bool,
extra_args: list[str] = [],
machine: Machine, mode: str, disks: dict[str, str], dry_run: bool, debug: bool
) -> None:
secret_facts_module = importlib.import_module(machine.secret_facts_module)
secret_facts_store: SecretStoreBase = secret_facts_module.SecretStore(
@ -57,8 +48,6 @@ def flash_machine(
disko_install.append("sudo")
disko_install.append("disko-install")
if write_efi_boot_entries:
disko_install.append("--write-efi-boot-entries")
if dry_run:
disko_install.append("--dry-run")
if debug:
@ -69,19 +58,12 @@ def flash_machine(
disko_install.extend(["--extra-files", str(local_dir), upload_dir])
disko_install.extend(["--flake", str(machine.flake) + "#" + machine.name])
disko_install.extend(["--mode", str(mode)])
disko_install.extend(
[
"--system-config",
json.dumps(system_config),
]
)
disko_install.extend(["--option", "dry-run", "true"])
disko_install.extend(extra_args)
cmd = nix_shell(
["nixpkgs#disko"],
disko_install,
)
print("$", " ".join(map(shlex.quote, cmd)))
run(cmd, log=Log.BOTH, error_msg=f"Failed to flash {machine}")
@ -90,15 +72,10 @@ class FlashOptions:
flake: Path
machine: str
disks: dict[str, str]
ssh_keys_path: list[Path]
dry_run: bool
confirm: bool
debug: bool
mode: str
language: str
keymap: str
write_efi_boot_entries: bool
nix_options: list[str]
class AppendDiskAction(argparse.Action):
@ -122,17 +99,11 @@ def flash_command(args: argparse.Namespace) -> None:
flake=args.flake,
machine=args.machine,
disks=args.disk,
ssh_keys_path=args.ssh_pubkey,
dry_run=args.dry_run,
confirm=not args.yes,
debug=args.debug,
mode=args.mode,
language=args.language,
keymap=args.keymap,
write_efi_boot_entries=args.write_efi_boot_entries,
nix_options=args.option,
)
machine = Machine(opts.machine, flake=opts.flake)
if opts.confirm and not opts.dry_run:
disk_str = ", ".join(f"{name}={device}" for name, device in opts.disks.items())
@ -143,44 +114,17 @@ def flash_command(args: argparse.Namespace) -> None:
ask = input(msg)
if ask != "y":
return
extra_config: dict[str, Any] = {}
if opts.ssh_keys_path:
root_keys = []
for key_path in opts.ssh_keys_path:
try:
root_keys.append(key_path.read_text())
except OSError as e:
raise ClanError(f"Cannot read SSH public key file: {key_path}: {e}")
extra_config["users"] = {
"users": {"root": {"openssh": {"authorizedKeys": {"keys": root_keys}}}}
}
if opts.keymap:
extra_config["console"] = {"keyMap": opts.keymap}
if opts.language:
extra_config["i18n"] = {"defaultLocale": opts.language}
flash_machine(
machine,
mode=opts.mode,
disks=opts.disks,
system_config=extra_config,
dry_run=opts.dry_run,
debug=opts.debug,
write_efi_boot_entries=opts.write_efi_boot_entries,
extra_args=opts.nix_options,
machine, opts.mode, disks=opts.disks, dry_run=opts.dry_run, debug=opts.debug
)
def register_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machine",
type=str,
help="machine to install",
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument(
"--disk",
type=str,
@ -190,14 +134,12 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
help="device to flash to",
default={},
)
mode_help = textwrap.dedent(
"""\
mode_help = textwrap.dedent("""\
Specify the mode of operation. Valid modes are: format, mount."
Format will format the disk before installing.
Mount will mount the disk before installing.
Mount is useful for updating an existing system without losing data.
"""
)
""")
parser.add_argument(
"--mode",
type=str,
@ -205,23 +147,7 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
choices=["format", "mount"],
default="format",
)
parser.add_argument(
"--ssh-pubkey",
type=Path,
action="append",
default=[],
help="ssh pubkey file to add to the root user. Can be used multiple times",
)
parser.add_argument(
"--language",
type=str,
help="system language",
)
parser.add_argument(
"--keymap",
type=str,
help="system keymap",
)
parser.add_argument(
"--yes",
action="store_true",
@ -235,14 +161,8 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
action="store_true",
)
parser.add_argument(
"--write-efi-boot-entries",
help=textwrap.dedent(
"""
Write EFI boot entries to the NVRAM of the system for the installed system.
Specify this option if you plan to boot from this disk on the current machine,
but not if you plan to move the disk to another machine.
"""
).strip(),
"--debug",
help="Print debug information",
default=False,
action="store_true",
)

View File

@ -5,7 +5,6 @@ from .create import register_create_parser
from .delete import register_delete_parser
from .install import register_install_parser
from .list import register_list_parser
from .show import register_show_parser
from .update import register_update_parser
@ -18,26 +17,7 @@ def register_parser(parser: argparse.ArgumentParser) -> None:
required=True,
)
update_parser = subparser.add_parser(
"update",
help="Update a machine",
epilog=(
"""
This subcommand provides an interface to update machines managed by clan.
Examples:
$ clan machines update [MACHINES]
Will update the specified machine [MACHINE], if [MACHINE] is omitted, the command
will attempt to update every configured machine.
To exclude machines being updated `clan.deployment.requireExplicitUpdate = true;`
can be set in the machine config.
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
update_parser = subparser.add_parser("update", help="Update a machine")
register_update_parser(update_parser)
create_parser = subparser.add_parser("create", help="Create a machine")
@ -46,34 +26,9 @@ For more detailed information, visit: https://docs.clan.lol/getting-started/depl
delete_parser = subparser.add_parser("delete", help="Delete a machine")
register_delete_parser(delete_parser)
list_parser = subparser.add_parser(
"list",
help="List machines",
epilog=(
"""
This subcommand lists all machines managed by this clan.
Examples:
$ clan machines list
Lists all the machines and their descriptions.
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
list_parser = subparser.add_parser("list", help="List machines")
register_list_parser(list_parser)
show_parser = subparser.add_parser(
"show",
help="Show a machine",
epilog=(
"""
This subcommand shows the details of a machine managed by this clan like icon, description, etc
"""
),
)
register_show_parser(show_parser)
install_parser = subparser.add_parser(
"install",
help="Install a machine",
@ -82,23 +37,5 @@ This subcommand shows the details of a machine managed by this clan like icon, d
The target must be a Linux based system reachable via SSH.
Installing a machine means overwriting the target's disk.
""",
epilog=(
"""
This subcommand provides an interface to install machines managed by clan.
Examples:
$ clan machines install [MACHINE] [TARGET_HOST]
Will install the specified machine [MACHINE], to the specified [TARGET_HOST].
$ clan machines install [MACHINE] --json [JSON]
Will install the specified machine [MACHINE] to the host exposed by
the deployment information of the [JSON] deployment string.
For information on how to set up the installer see: https://docs.clan.lol/getting-started/installer/
For more detailed information, visit: https://docs.clan.lol/getting-started/deploy
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
register_install_parser(install_parser)

View File

@ -1,27 +1,13 @@
import argparse
import logging
from dataclasses import dataclass
from pathlib import Path
from clan_cli.api import API
from clan_cli.config.machine import set_config_for_machine
log = logging.getLogger(__name__)
@dataclass
class MachineCreateRequest:
name: str
config: dict[str, int]
@API.register
def create_machine(flake_dir: str | Path, machine: MachineCreateRequest) -> None:
set_config_for_machine(Path(flake_dir), machine.name, machine.config)
def create_command(args: argparse.Namespace) -> None:
create_machine(args.flake, MachineCreateRequest(args.machine, dict()))
set_config_for_machine(args.flake, args.machine, dict())
def register_create_parser(parser: argparse.ArgumentParser) -> None:

View File

@ -1,7 +1,6 @@
import argparse
import shutil
from ..completions import add_dynamic_completer, complete_machines
from ..dirs import specific_machine_dir
from ..errors import ClanError
@ -15,7 +14,5 @@ def delete_command(args: argparse.Namespace) -> None:
def register_delete_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument("host", type=str)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument("host", type=str)
parser.set_defaults(func=delete_command)

View File

@ -1,33 +1,20 @@
import argparse
import importlib
import json
import logging
import os
from dataclasses import dataclass
from pathlib import Path
from tempfile import TemporaryDirectory
from ..cmd import Log, run
from ..completions import add_dynamic_completer, complete_machines
from ..facts.generate import generate_facts
from ..machines.machines import Machine
from ..nix import nix_shell
from ..ssh.cli import is_ipv6, is_reachable, qrcode_scan
log = logging.getLogger(__name__)
class ClanError(Exception):
pass
def install_nixos(
machine: Machine,
kexec: str | None = None,
debug: bool = False,
password: str | None = None,
no_reboot: bool = False,
extra_args: list[str] = [],
machine: Machine, kexec: str | None = None, debug: bool = False
) -> None:
secret_facts_module = importlib.import_module(machine.secret_facts_module)
log.info(f"installing {machine.name}")
@ -37,7 +24,7 @@ def install_nixos(
target_host = f"{h.user or 'root'}@{h.host}"
log.info(f"target host: {target_host}")
generate_facts([machine], None, False)
generate_facts([machine])
with TemporaryDirectory() as tmpdir_:
tmpdir = Path(tmpdir_)
@ -49,28 +36,14 @@ def install_nixos(
upload_dir.mkdir(parents=True)
secret_facts_store.upload(upload_dir)
if password:
os.environ["SSHPASS"] = password
cmd = [
"nixos-anywhere",
"--flake",
f"{machine.flake}#{machine.name}",
"--no-reboot",
"--extra-files",
str(tmpdir),
*extra_args,
]
if no_reboot:
cmd.append("--no-reboot")
if password:
cmd += [
"--env-password",
"--ssh-option",
"IdentitiesOnly=yes",
]
if machine.target_host.port:
cmd += ["--ssh-port", str(machine.target_host.port)]
if kexec:
@ -96,42 +69,16 @@ class InstallOptions:
kexec: str | None
confirm: bool
debug: bool
no_reboot: bool
json_ssh_deploy: dict[str, str] | None
nix_options: list[str]
def install_command(args: argparse.Namespace) -> None:
json_ssh_deploy = None
if args.json:
json_file = Path(args.json)
if json_file.is_file():
json_ssh_deploy = json.loads(json_file.read_text())
else:
json_ssh_deploy = json.loads(args.json)
elif args.png:
json_ssh_deploy = json.loads(qrcode_scan(args.png))
if not json_ssh_deploy and not args.target_host:
raise ClanError("No target host provided, please provide a target host.")
if json_ssh_deploy:
target_host = f"root@{find_reachable_host_from_deploy_json(json_ssh_deploy)}"
password = json_ssh_deploy["pass"]
else:
target_host = args.target_host
password = None
opts = InstallOptions(
flake=args.flake,
machine=args.machine,
target_host=target_host,
target_host=args.target_host,
kexec=args.kexec,
confirm=not args.yes,
debug=args.debug,
no_reboot=args.no_reboot,
json_ssh_deploy=json_ssh_deploy,
nix_options=args.option,
)
machine = Machine(opts.machine, flake=opts.flake)
machine.target_host_address = opts.target_host
@ -141,34 +88,7 @@ def install_command(args: argparse.Namespace) -> None:
if ask != "y":
return
install_nixos(
machine,
kexec=opts.kexec,
debug=opts.debug,
password=password,
no_reboot=opts.no_reboot,
extra_args=opts.nix_options,
)
def find_reachable_host_from_deploy_json(deploy_json: dict[str, str]) -> str:
host = None
for addr in deploy_json["addrs"]:
if is_reachable(addr):
if is_ipv6(addr):
host = f"[{addr}]"
else:
host = addr
break
if not host:
raise ClanError(
f"""
Could not reach any of the host addresses provided in the json string.
Please doublecheck if they are reachable from your machine.
Try `ping [ADDR]` with one of the addresses: {deploy_json['addrs']}
"""
)
return host
install_nixos(machine, kexec=opts.kexec, debug=opts.debug)
def register_install_parser(parser: argparse.ArgumentParser) -> None:
@ -177,41 +97,26 @@ def register_install_parser(parser: argparse.ArgumentParser) -> None:
type=str,
help="use another kexec tarball to bootstrap NixOS",
)
parser.add_argument(
"--no-reboot",
action="store_true",
help="do not reboot after installation",
default=False,
)
parser.add_argument(
"--yes",
action="store_true",
help="do not ask for confirmation",
default=False,
)
machines_parser = parser.add_argument(
parser.add_argument(
"--debug",
action="store_true",
help="print debug information",
default=False,
)
parser.add_argument(
"machine",
type=str,
help="machine to install",
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument(
"target_host",
type=str,
nargs="?",
help="ssh address to install to in the form of user@host:2222",
)
group = parser.add_mutually_exclusive_group(required=False)
group.add_argument(
"-j",
"--json",
help="specify the json file for ssh data (generated by starting the clan installer)",
)
group.add_argument(
"-P",
"--png",
help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer)",
)
parser.set_defaults(func=install_command)

View File

@ -7,7 +7,7 @@ from .machines import Machine
# function to speedup eval if we want to evauluate all machines
def get_all_machines(flake_dir: Path, nix_options: list[str]) -> list[Machine]:
def get_all_machines(flake_dir: Path) -> list[Machine]:
config = nix_config()
system = config["system"]
json_path = run(
@ -19,20 +19,13 @@ def get_all_machines(flake_dir: Path, nix_options: list[str]) -> list[Machine]:
machines = []
for name, machine_data in machines_json.items():
machines.append(
Machine(
name=name,
flake=flake_dir,
deployment_info=machine_data,
nix_options=nix_options,
)
Machine(name=name, flake=flake_dir, deployment_info=machine_data)
)
return machines
def get_selected_machines(
flake_dir: Path, nix_options: list[str], machine_names: list[str]
) -> list[Machine]:
def get_selected_machines(flake_dir: Path, machine_names: list[str]) -> list[Machine]:
machines = []
for name in machine_names:
machines.append(Machine(name=name, flake=flake_dir, nix_options=nix_options))
machines.append(Machine(name=name, flake=flake_dir))
return machines

View File

@ -3,16 +3,13 @@ import json
import logging
from pathlib import Path
from clan_cli.api import API
from ..cmd import run_no_stdout
from ..cmd import run
from ..nix import nix_config, nix_eval
log = logging.getLogger(__name__)
@API.register
def list_machines(flake_url: str | Path, debug: bool) -> list[str]:
def list_machines(flake_url: Path | str) -> list[str]:
config = nix_config()
system = config["system"]
cmd = nix_eval(
@ -23,17 +20,15 @@ def list_machines(flake_url: str | Path, debug: bool) -> list[str]:
"--json",
]
)
proc = run_no_stdout(cmd)
proc = run(cmd)
res = proc.stdout.strip()
return json.loads(res)
def list_command(args: argparse.Namespace) -> None:
flake_path = Path(args.flake).resolve()
for name in list_machines(flake_path, args.debug):
print(name)
for machine in list_machines(Path(args.flake)):
print(machine)
def register_list_parser(parser: argparse.ArgumentParser) -> None:

View File

@ -10,7 +10,7 @@ from clan_cli.clan_uri import ClanURI, MachineData
from clan_cli.dirs import vm_state_dir
from clan_cli.qemu.qmp import QEMUMonitorProtocol
from ..cmd import run_no_stdout
from ..cmd import run
from ..errors import ClanError
from ..nix import nix_build, nix_config, nix_eval, nix_metadata
from ..ssh import Host, parse_deployment_address
@ -41,10 +41,9 @@ class QMPWrapper:
class Machine:
name: str
flake: str | Path
name: str
data: MachineData
nix_options: list[str]
eval_cache: dict[str, str]
build_cache: dict[str, Path]
_flake_path: Path | None
@ -56,7 +55,6 @@ class Machine:
name: str,
flake: Path | str,
deployment_info: dict | None = None,
nix_options: list[str] = [],
machine: MachineData | None = None,
) -> None:
"""
@ -78,7 +76,6 @@ class Machine:
self.build_cache: dict[str, Path] = {}
self._flake_path: Path | None = None
self._deployment_info: None | dict = deployment_info
self.nix_options = nix_options
state_dir = vm_state_dir(flake_url=str(self.flake), vm_name=self.data.name)
@ -200,7 +197,7 @@ class Machine:
config_json.flush()
file_info = json.loads(
run_no_stdout(
run(
nix_eval(
[
"--impure",
@ -245,15 +242,15 @@ class Machine:
flake = f"path:{self.flake_dir}"
args += [
f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}'
f'{flake}#clanInternals.machines."{system}".{self.data.name}.{attr}',
*nix_options,
]
args += nix_options + self.nix_options
if method == "eval":
output = run_no_stdout(nix_eval(args)).stdout.strip()
output = run(nix_eval(args)).stdout.strip()
return output
elif method == "build":
outpath = run_no_stdout(nix_build(args)).stdout.strip()
outpath = run(nix_build(args)).stdout.strip()
return Path(outpath)
else:
raise ValueError(f"Unknown method {method}")

View File

@ -1,60 +0,0 @@
import argparse
import dataclasses
import json
import logging
from pathlib import Path
from clan_cli.api import API
from ..cmd import run_no_stdout
from ..completions import add_dynamic_completer, complete_machines
from ..nix import nix_config, nix_eval
from .types import machine_name_type
log = logging.getLogger(__name__)
@dataclasses.dataclass
class MachineInfo:
machine_name: str
machine_description: str | None
machine_icon: str | None
@API.register
def show_machine(flake_url: str | Path, machine_name: str, debug: bool) -> MachineInfo:
config = nix_config()
system = config["system"]
cmd = nix_eval(
[
f"{flake_url}#clanInternals.machines.{system}.{machine_name}",
"--apply",
"machine: { inherit (machine.config.clanCore) machineDescription machineIcon machineName; }",
"--json",
]
)
proc = run_no_stdout(cmd)
res = proc.stdout.strip()
machine = json.loads(res)
return MachineInfo(
machine_name=machine.get("machineName"),
machine_description=machine.get("machineDescription", None),
machine_icon=machine.get("machineIcon", None),
)
def show_command(args: argparse.Namespace) -> None:
flake_path = Path(args.flake).resolve()
machine = show_machine(flake_path, args.machine, args.debug)
print(f"Name: {machine.machine_name}")
print(f"Description: {machine.machine_description or ''}")
print(f"Icon: {machine.machine_icon or ''}")
def register_show_parser(parser: argparse.ArgumentParser) -> None:
parser.set_defaults(func=show_command)
machine_parser = parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type
)
add_dynamic_completer(machine_parser, complete_machines)

View File

@ -3,10 +3,9 @@ import json
import logging
import os
import shlex
import subprocess
import sys
from ..cmd import run
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
from ..facts.generate import generate_facts
from ..facts.upload import upload_secrets
@ -54,7 +53,11 @@ def upload_sources(
path,
]
)
run(cmd, env=env, error_msg="failed to upload sources")
proc = subprocess.run(cmd, stdout=subprocess.PIPE, env=env, check=False)
if proc.returncode != 0:
raise ClanError(
f"failed to upload sources: {shlex.join(cmd)} failed with {proc.returncode}"
)
return path
# Slow path: we need to upload all sources to the remote machine
@ -70,13 +73,16 @@ def upload_sources(
]
)
log.info("run %s", shlex.join(cmd))
proc = run(cmd, error_msg="failed to upload sources")
proc = subprocess.run(cmd, stdout=subprocess.PIPE, check=False)
if proc.returncode != 0:
raise ClanError(
f"failed to upload sources: {shlex.join(cmd)} failed with {proc.returncode}"
)
try:
return json.loads(proc.stdout)["path"]
except (json.JSONDecodeError, OSError) as e:
raise ClanError(
f"failed to parse output of {shlex.join(cmd)}: {e}\nGot: {proc.stdout}"
f"failed to parse output of {shlex.join(cmd)}: {e}\nGot: {proc.stdout.decode('utf-8', 'replace')}"
)
@ -92,7 +98,7 @@ def deploy_nixos(machines: MachineGroup) -> None:
env = os.environ.copy()
env["NIX_SSHOPTS"] = ssh_arg
generate_facts([machine], None, False)
generate_facts([machine])
upload_secrets(machine)
path = upload_sources(".", target)
@ -104,9 +110,11 @@ def deploy_nixos(machines: MachineGroup) -> None:
ssh_arg += " -i " + host.key if host.key else ""
extra_args = host.meta.get("extra_args", [])
cmd = [
"nixos-rebuild",
"switch",
*extra_args,
"--fast",
"--option",
"keep-going",
@ -116,7 +124,6 @@ def deploy_nixos(machines: MachineGroup) -> None:
"true",
"--build-host",
"",
*machine.nix_options,
"--flake",
f"{path}#{machine.name}",
]
@ -136,9 +143,7 @@ def update(args: argparse.Namespace) -> None:
raise ClanError("Could not find clan flake toplevel directory")
machines = []
if len(args.machines) == 1 and args.target_host is not None:
machine = Machine(
name=args.machines[0], flake=args.flake, nix_options=args.option
)
machine = Machine(name=args.machines[0], flake=args.flake)
machine.target_host_address = args.target_host
machines.append(machine)
@ -148,7 +153,7 @@ def update(args: argparse.Namespace) -> None:
else:
if len(args.machines) == 0:
ignored_machines = []
for machine in get_all_machines(args.flake, args.option):
for machine in get_all_machines(args.flake):
if machine.deployment_info.get("requireExplicitUpdate", False):
continue
try:
@ -168,13 +173,13 @@ def update(args: argparse.Namespace) -> None:
print(machine, file=sys.stderr)
else:
machines = get_selected_machines(args.flake, args.option, args.machines)
machines = get_selected_machines(args.flake, args.machines)
deploy_nixos(MachineGroup(machines))
def register_update_parser(parser: argparse.ArgumentParser) -> None:
machines_parser = parser.add_argument(
parser.add_argument(
"machines",
type=str,
nargs="*",
@ -182,9 +187,6 @@ def register_update_parser(parser: argparse.ArgumentParser) -> None:
metavar="MACHINE",
help="machine to update. If no machine is specified, all machines will be updated.",
)
add_dynamic_completer(machines_parser, complete_machines)
parser.add_argument(
"--target-host",
type=str,

View File

@ -106,7 +106,13 @@ def nix_shell(packages: list[str], cmd: list[str]) -> list[str]:
if os.environ.get("IN_NIX_SANDBOX"):
return cmd
return [
*nix_command(["shell", "--inputs-from", f"{nixpkgs_flake()!s}"]),
*nix_command(
[
"shell",
"--inputs-from",
f"{nixpkgs_flake()!s}",
]
),
*packages,
"-c",
*cmd,

View File

@ -1,44 +1,19 @@
import argparse
import logging
from pathlib import Path
from clan_cli.git import commit_files
from .. import tty
from ..errors import ClanError
from .secrets import update_secrets
from .sops import default_sops_key_path, generate_private_key, get_public_key
log = logging.getLogger(__name__)
def extract_public_key(filepath: Path) -> str:
"""
Extracts the public key from a given text file.
"""
try:
with open(filepath) as file:
for line in file:
# Check if the line contains the public key
if line.startswith("# public key:"):
# Extract and return the public key part after the prefix
return line.strip().split(": ")[1]
except FileNotFoundError:
raise ClanError(f"The file at {filepath} was not found.")
except Exception as e:
raise ClanError(f"An error occurred while extracting the public key: {e}")
raise ClanError(f"Could not find the public key in the file at {filepath}.")
def generate_key() -> str:
path = default_sops_key_path()
if path.exists():
log.info(f"Key already exists at {path}")
return extract_public_key(path)
raise ClanError(f"Key already exists at {path}")
priv_key, pub_key = generate_private_key(out_file=path)
log.info(
f"Generated age private key at '{default_sops_key_path()}' for your user. Please back it up on a secure location or you will lose access to your secrets."
)
return pub_key
@ -48,9 +23,13 @@ def show_key() -> str:
def generate_command(args: argparse.Namespace) -> None:
pub_key = generate_key()
log.info(
f"Also add your age public key to the repository with: \nclan secrets users add <username> {pub_key}"
tty.info(
f"Generated age private key at '{default_sops_key_path()}' for your user. Please back it up on a secure location or you will lose access to your secrets."
)
tty.info(
f"Also add your age public key to the repository with 'clan secrets users add youruser {pub_key}' (replace youruser with your user name)"
)
pass
def show_command(args: argparse.Namespace) -> None:

View File

@ -1,7 +1,6 @@
import argparse
from pathlib import Path
from ..completions import add_dynamic_completer, complete_machines
from ..errors import ClanError
from ..git import commit_files
from ..machines.types import machine_name_type, validate_hostname
@ -148,28 +147,25 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
# Parser
get_parser = subparser.add_parser("get", help="get a machine public key")
get_machine_parser = get_parser.add_argument(
get_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type
)
add_dynamic_completer(get_machine_parser, complete_machines)
get_parser.set_defaults(func=get_command)
# Parser
remove_parser = subparser.add_parser("remove", help="remove a machine")
remove_machine_parser = remove_parser.add_argument(
remove_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type
)
add_dynamic_completer(remove_machine_parser, complete_machines)
remove_parser.set_defaults(func=remove_command)
# Parser
add_secret_parser = subparser.add_parser(
"add-secret", help="allow a machine to access a secret"
)
machine_add_secret_parser = add_secret_parser.add_argument(
add_secret_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type
)
add_dynamic_completer(machine_add_secret_parser, complete_machines)
add_secret_parser.add_argument(
"secret", help="the name of the secret", type=secret_name_type
)
@ -179,10 +175,9 @@ def register_machines_parser(parser: argparse.ArgumentParser) -> None:
remove_secret_parser = subparser.add_parser(
"remove-secret", help="remove a group's access to a secret"
)
machine_remove_parser = remove_secret_parser.add_argument(
"machine", help="the name of the machine", type=machine_name_type
remove_secret_parser.add_argument(
"machine", help="the name of the group", type=machine_name_type
)
add_dynamic_completer(machine_remove_parser, complete_machines)
remove_secret_parser.add_argument(
"secret", help="the name of the secret", type=secret_name_type
)

View File

@ -9,7 +9,6 @@ from pathlib import Path
from typing import IO
from .. import tty
from ..completions import add_dynamic_completer, complete_secrets
from ..errors import ClanError
from ..git import commit_files
from .folders import (
@ -154,12 +153,8 @@ def remove_command(args: argparse.Namespace) -> None:
remove_secret(Path(args.flake), args.secret)
def add_secret_argument(parser: argparse.ArgumentParser, autocomplete: bool) -> None:
secrets_parser = parser.add_argument(
"secret", help="the name of the secret", type=secret_name_type
)
if autocomplete:
add_dynamic_completer(secrets_parser, complete_secrets)
def add_secret_argument(parser: argparse.ArgumentParser) -> None:
parser.add_argument("secret", help="the name of the secret", type=secret_name_type)
def machines_folder(flake_dir: Path, group: str) -> Path:
@ -328,11 +323,11 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
parser_list.set_defaults(func=list_command)
parser_get = subparser.add_parser("get", help="get a secret")
add_secret_argument(parser_get, True)
add_secret_argument(parser_get)
parser_get.set_defaults(func=get_command)
parser_set = subparser.add_parser("set", help="set a secret")
add_secret_argument(parser_set, False)
add_secret_argument(parser_set)
parser_set.add_argument(
"--group",
type=str,
@ -364,10 +359,10 @@ def register_secrets_parser(subparser: argparse._SubParsersAction) -> None:
parser_set.set_defaults(func=set_command)
parser_rename = subparser.add_parser("rename", help="rename a secret")
add_secret_argument(parser_rename, True)
add_secret_argument(parser_rename)
parser_rename.add_argument("new_name", type=str, help="the new name of the secret")
parser_rename.set_defaults(func=rename_command)
parser_remove = subparser.add_parser("remove", help="remove a secret")
add_secret_argument(parser_remove, True)
add_secret_argument(parser_remove)
parser_remove.set_defaults(func=remove_command)

View File

@ -1,5 +1,4 @@
import argparse
import ipaddress
import json
import logging
import socket
@ -97,13 +96,6 @@ def connect_ssh_from_json(ssh_data: dict[str, str]) -> None:
ssh(host=ssh_data["tor"], password=ssh_data["pass"], torify=True)
def is_ipv6(ip: str) -> bool:
try:
return isinstance(ipaddress.ip_address(ip), ipaddress.IPv6Address)
except ValueError:
return False
def main(args: argparse.Namespace) -> None:
if args.json:
json_file = Path(args.json)

View File

@ -69,7 +69,7 @@ def get_secrets(
secret_facts_module = importlib.import_module(machine.secret_facts_module)
secret_facts_store = secret_facts_module.SecretStore(machine=machine)
generate_facts([machine], None, False)
generate_facts([machine])
secret_facts_store.upload(secrets_dir)
return secrets_dir

Some files were not shown because too many files have changed in this diff Show More