Compare commits

..

38 Commits

Author SHA1 Message Date
a-kenji
dc915387d9 pkgs/clan(templates): Add shell completions 2025-07-13 21:00:30 +02:00
a-kenji
a890b586b4 pkgs/clan: Fix command typos 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
81da1e8b1d Users: add option for regularUser 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
74c5f71fd7 Templates: keep clan.nix in sync between default and flake-parts 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
c3f26b3728 Modules/users: add isNormalUser true
NormalUsers get:
- Home directory
- Can login

This is expected for users created through this module. We can make it configurable if the use arises
2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
2b3c5b0524 Templates/flake-parts: consistent default clan 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
6918a6f1e3 diskId: add migration docs and a big fat warning 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
71f8948a17 cli/templates: init apply disk 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
44d2a6485e lib/disks: add parameter to disable hardware checking 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
2e82109688 cli/machine/hardware: improve error message 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
c3a2891929 get_machine: fix error message for not existing machine 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
2e2156bc86 lib/copy: fix, copying the content of tempate directory, not the directory itself 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
802ef94798 Vars/helper: remove unneeded wrapper arount collectFiles 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
2ce4f8bf37 Template/docs: improve gnome example 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
24d82776e7 Templates/minimal: move name to flake.nix 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
ff41903e47 templates: remove duplicate logic, update gnome template 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
31e3a37da4 templates/flake-parts: remove importing clanModules 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
690072e29e docs: fix user module prompt description 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
d39fc575c6 modules/user: improce description, drop default groups 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
6019efe40a modules/user: add extraGroups setting with default 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
02d35395a8 modules: add explicit class constraints 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
e90ea62ab7 openapi: remove verb {open}, noun {file} 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
5bf1f06244 API: rename {open_file, open_clan_folder} into {get_system_file, get_clan_folder} 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
1403f47b0d Docs: improve api docs of {open_file, open_clan_folder} 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
743aa712f5 UI/Cubes: init circle positioning 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
9800e50ce1 UI/qubescene: add create animation 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
ef0b61ccd6 UI/qubescene: add delete and reposition animation 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
0d5dbb0fc5 UI/qubescene: dynamically recalculate the positions 2025-07-13 21:00:30 +02:00
Johannes Kirschbauer
9a647907e9 UI/cubescene: init delete cube 2025-07-13 21:00:30 +02:00
pinpox
5469ab0ae0 Add example for data-mesher service usage 2025-07-13 21:00:30 +02:00
pinpox
504533cf5a Migrate data-mesher to clan service 2025-07-13 21:00:30 +02:00
Qubasa
e9f21a01e9 clan-app: Make http server non blocking, add tests for the http server and for cancelling tasks 2025-07-13 21:00:30 +02:00
Qubasa
f81089930e stash 2025-07-13 21:00:30 +02:00
Qubasa
84a7dc7697 clan-app: Working swagger requests 2025-07-13 21:00:30 +02:00
Qubasa
0d851580e1 clan-lib: Fix @API.register_abstract not throwing correct error when called directly without implementation
clan-app: Fix mypy lint

clan-lib: Mark test as with_core
2025-07-13 21:00:30 +02:00
Qubasa
be384420d5 clan_lib: Add test for check_valid_clan function 2025-07-13 21:00:30 +02:00
Qubasa
5ebf5b6189 clan-app: Implement open_clan_folder api request 2025-07-13 21:00:30 +02:00
Qubasa
d7b476a311 clan-app: Moved thread handling up to the ApiBridge 2025-07-13 21:00:30 +02:00
39 changed files with 1169 additions and 1177 deletions

View File

@@ -11,7 +11,7 @@
roles.default = { roles.default = {
interface = interface =
{ config, lib, ... }: { lib, ... }:
{ {
options = { options = {
user = lib.mkOption { user = lib.mkOption {
@@ -28,8 +28,8 @@
Effects: Effects:
- *enabled* (`true`) - Prompt for a password during the machine installation or update workflow. - *enabled* (`true`) - Prompt for a passwort during the machine installation or update workflow.
- *disabled* (`false`) - Generate a password during the machine installation or update workflow. - *disabled* (`false`) - Generate a passwort during the machine installation or update workflow.
The password can be shown in two steps: The password can be shown in two steps:
@@ -39,8 +39,7 @@
}; };
regularUser = lib.mkOption { regularUser = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = config.user != "root"; default = true;
defaultText = lib.literalExpression "config.user != \"root\"";
example = false; example = false;
description = '' description = ''
Whether the user should be a regular user or a system user. Whether the user should be a regular user or a system user.

View File

@@ -13,6 +13,8 @@
roles.default.machines."server".settings = { roles.default.machines."server".settings = {
user = "root"; user = "root";
prompt = false; prompt = false;
# Important: 'root' must not be a regular user. See: https://github.com/NixOS/nixpkgs/issues/424404
regularUser = false;
}; };
}; };
user-password-test = { user-password-test = {

View File

@@ -51,7 +51,6 @@ nav:
- 🚀 Creating Your First Clan: guides/getting-started/index.md - 🚀 Creating Your First Clan: guides/getting-started/index.md
- 📀 Create USB Installer (optional): guides/getting-started/installer.md - 📀 Create USB Installer (optional): guides/getting-started/installer.md
- ⚙️ Add Machines: guides/getting-started/add-machines.md - ⚙️ Add Machines: guides/getting-started/add-machines.md
- ⚙️ Add User: guides/getting-started/add-user.md
- ⚙️ Add Services: guides/getting-started/add-services.md - ⚙️ Add Services: guides/getting-started/add-services.md
- 🔐 Secrets & Facts: guides/getting-started/secrets.md - 🔐 Secrets & Facts: guides/getting-started/secrets.md
- 🚢 Deploy Machine: guides/getting-started/deploy.md - 🚢 Deploy Machine: guides/getting-started/deploy.md

View File

@@ -29,13 +29,13 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from clan_lib.errors import ClanError from clan_lib.api.modules import (
from clan_lib.services.modules import (
CategoryInfo, CategoryInfo,
Frontmatter, Frontmatter,
extract_frontmatter, extract_frontmatter,
get_roles, get_roles,
) )
from clan_lib.errors import ClanError
# Get environment variables # Get environment variables
CLAN_CORE_PATH = Path(os.environ["CLAN_CORE_PATH"]) CLAN_CORE_PATH = Path(os.environ["CLAN_CORE_PATH"])

View File

@@ -10,26 +10,67 @@ See the complete [list](../../guides/more-machines.md#automatic-registration) of
## Create a machine ## Create a machine
=== "clan.nix (declarative)" === "flake.nix (flake-parts)"
```{.nix hl_lines="3-4"} ```{.nix hl_lines=12-15}
{ {
inventory.machines = { inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
# Define a machine inputs.nixpkgs.follows = "clan-core/nixpkgs";
jon = { }; inputs.flake-parts.follows = "clan-core/flake-parts";
}; inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
# Additional NixOS configuration can be added here. outputs =
# machines/jon/configuration.nix will be automatically imported. inputs@{ flake-parts, ... }:
# See: https://docs.clan.lol/guides/more-machines/#automatic-registration flake-parts.lib.mkFlake { inherit inputs; } {
machines = { imports = [ inputs.clan-core.flakeModules.default ];
# jon = { config, ... }: { clan = {
# environment.systemPackages = [ pkgs.asciinema ]; inventory.machines = {
# }; # Define a machine
jon = { };
};
};
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
}; };
} }
``` ```
=== "flake.nix (classic)"
```{.nix hl_lines=11-14}
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
clan = clan-core.lib.clan {
inherit self;
inventory.machines = {
# Define a machine
jon = { };
};
};
in
{
inherit (clan.config)
nixosConfigurations
nixosModules
clanInternals
darwinConfigurations
darwinModules
;
};
}
```
=== "CLI (imperative)" === "CLI (imperative)"
```sh ```sh
@@ -48,15 +89,16 @@ See the complete [list](../../guides/more-machines.md#automatic-registration) of
The option: `machines.<name>` is used to add extra *nixosConfiguration* to a machine The option: `machines.<name>` is used to add extra *nixosConfiguration* to a machine
Add the following to your `clan.nix` file for each machine. ```{.nix .annotate title="flake.nix" hl_lines="3-13 18-22"}
This example demonstrates what is needed based on a machine called `jon`: # Sometimes this attribute set is defined in clan.nix
clan = {
```{.nix .annotate title="clan.nix" hl_lines="3-6 15-19"}
{
inventory.machines = { inventory.machines = {
jon = { jon = {
# Define tags here (optional) # Define targetHost here
tags = [ ]; # (1) # Required before deployment
deploy.targetHost = "root@jon"; # (1)
# Define tags here
tags = [ ];
}; };
sara = { sara = {
deploy.targetHost = "root@sara"; deploy.targetHost = "root@sara";
@@ -75,24 +117,9 @@ This example demonstrates what is needed based on a machine called `jon`:
} }
``` ```
1. Tags can be used to automatically add this machine to services later on. - You dont need to set this now. 1. It is required to define a *targetHost* for each machine before deploying. Best practice has been, to use the zerotier ip/hostname or the ip from the from overlay network you decided to use.
2. Add your *ssh key* here - That will ensure you can always login to your machine via *ssh* in case something goes wrong. 2. Add your *ssh key* here - That will ensure you can always login to your machine via *ssh* in case something goes wrong.
### (Optional) Create a `configuration.nix`
```nix title="./machines/jon/configuration.nix"
{
imports = [
# enables GNOME desktop (optional)
../../modules/gnome.nix
];
# Set nixosOptions here
# Or import your own modules via 'imports'
# ...
}
```
### (Optional) Renaming a Machine ### (Optional) Renaming a Machine
Older templates included static machine folders like `jon` and `sara`. Older templates included static machine folders like `jon` and `sara`.

View File

@@ -17,61 +17,104 @@ To learn more: [Guide about clanService](../clanServices.md)
## Configure a Zerotier Network (recommended) ## Configure a Zerotier Network (recommended)
```{.nix title="clan.nix" hl_lines="8-16"} ```{.nix title="flake.nix" hl_lines="20-28"}
{ {
inventory.machines = { inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
jon = { }; inputs.nixpkgs.follows = "clan-core/nixpkgs";
sara = { }; inputs.flake-parts.follows = "clan-core/flake-parts";
}; inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
inventory.instances = { outputs =
zerotier = { # (1) inputs@{ flake-parts, ... }:
# Replace with the name (string) of your machine that you will use as zerotier-controller flake-parts.lib.mkFlake { inherit inputs; } {
# See: https://docs.zerotier.com/controller/ imports = [ inputs.clan-core.flakeModules.default ];
# Deploy this machine first to create the network secrets # Sometimes this attribute set is defined in clan.nix
roles.controller.machines."jon" = { }; # (2) clan = {
# Peers of the network inventory.machines = {
# this line means 'all' clan machines will be 'peers' jon = {
roles.peer.tags.all = { }; # (3) targetHost = "root@jon";
}; };
sara = {
targetHost = "root@jon";
};
};
inventory.instances = {
zerotier = { # (1)
# Defines 'jon' as the controller
roles.controller.machines.jon = {};
# Defines all machines as networking peer.
# The 'all' tag is a clan builtin.
roles.peer.tags.all = {};
};
}
};
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
}; };
# ...
# elided
} }
``` ```
1. See [reference/clanServices](../../reference/clanServices/index.md) for all available services and how to configure them. 1. See [reference/clanServices](../../reference/clanServices/index.md) for all available services and how to configure them.
Or read [authoring/clanServices](../authoring/clanServices/index.md) if you want to bring your own Or read [authoring/clanServices](../authoring/clanServices/index.md) if you want to bring your own
2. Replace `__YOUR_CONTROLLER_` with the *name* of your machine.
3. This line will add all machines of your clan as `peer` to zerotier
## Adding more recommended defaults ## Adding more recommended defaults
Adding the following services is recommended for most users: Adding the following services is recommended for most users:
```{.nix title="clan.nix" hl_lines="7-14"} ```{.nix title="flake.nix" hl_lines="25-35"}
{ {
inventory.machines = { inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
jon = { }; inputs.nixpkgs.follows = "clan-core/nixpkgs";
sara = { }; inputs.flake-parts.follows = "clan-core/flake-parts";
}; inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
inventory.instances = {
admin = { # (1) outputs =
roles.default.tags.all = { }; inputs@{ flake-parts, ... }:
roles.default.settings = { flake-parts.lib.mkFlake { inherit inputs; } {
allowedKeys = { imports = [ inputs.clan-core.flakeModules.default ];
"my-user" = "ssh-ed25519 AAAAC3N..."; # (2) # Sometimes this attribute set is defined in clan.nix
clan = {
inventory.machines = {
jon = {
targetHost = "root@jon";
};
sara = {
targetHost = "root@jon";
};
};
inventory.instances = {
zerotier = {
roles.controller.machines.jon = {};
roles.peer.tags.all = {};
};
admin = { # (1)
roles.default.tags.all = { };
roles.default.settings = {
allowedKeys = {
"my-user" = "ssh-ed25519 AAAAC3N..."; # elided
};
};
};
state-version = { # (2)
roles.default.tags.all = { };
};
}; };
}; };
}; systems = [
# ... "x86_64-linux"
# elided "aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
}; };
} }
``` ```
1. The `admin` service will generate a **root-password** and **add your ssh-key** that allows for convienient administration. 1. The `admin` service will generate a **root-password** and **add your ssh-key** that allows for convienient administration.
2. Equivalent to directly setting `authorizedKeys` like in [configuring a machine](./add-machines.md#configuring-a-machine)
3. Adds `user = jon` as a user on all machines. Will create a `home` directory, and prompt for a password before deployment. 2. The `state-version` service will generate a [nixos state version](https://wiki.nixos.org/wiki/FAQ/When_do_I_update_stateVersion) for each system once it is deployed.

View File

@@ -1,127 +0,0 @@
# How to add users
!!! Note "Under construction"
The users concept of clan is not done yet. This guide outlines some solutions from our community.
Defining users can be done in many different ways. We want to highlight two approaches:
- Using clan's [users](../../reference/clanServices/users.md) service.
- Using a custom approach.
## Adding Users using the [users](../../reference/clanServices/users.md) service
To add a first *user* this guide will be leveraging two things:
- [clanServices](../../reference/clanServices/index.md): Allows to bind arbitrary logic to something we call an `ìnstance`.
- [clanServices/users](../../reference/clanServices/users.md): Implements logic for adding a single user perInstance.
The example shows how to add a user called `jon`:
```{.nix title="clan.nix" hl_lines="7-21"}
{
inventory.machines = {
jon = { };
sara = { };
};
inventory.instances = {
jon-user = { # (1)
module.name = "users";
roles.default.tags.all = { }; # (2)
roles.default.settings = {
user = "jon"; # (3)
groups = [
"wheel" # Allow using 'sudo'
"networkmanager" # Allows to manage network connections.
"video" # Allows to access video devices.
"input" # Allows to access input devices.
];
};
};
# ...
# elided
};
}
```
1. Add `user = jon` as a user on all machines. Will create a `home` directory, and prompt for a password before deployment.
2. Add this user to `all` machines
3. Define the `name` of the user to be `jon`
The `users` service creates a `/home/jon` directory, allows `jon` to sign in and will take care of the users password as part of [deployment](./deploy.md).
For more information see [clanService/users](../../reference/clanServices/users.md)
## Using a custom approach
Some people like to define a `users` folder in their repository root.
That allows to bind all user specific logic to a single place (`default.nix`)
Which can be imported into individual machines to make the user avilable on that machine.
```bash
.
├── machines
│   ├── jon
# ......
├── users
│   ├── jon
│ │ └── default.nix # <- a NixOS module; sets some options
# ... ... ...
```
## using [home-manager](https://github.com/nix-community/home-manager)
When using clan's `users` service it is possible to define extraModules.
In fact this is always possible when using clan's services.
We can use this property of clan services to bind a nixosModule to the user, which configures home-manager.
```{.nix title="clan.nix" hl_lines="22"}
{
inventory.machines = {
jon = { };
sara = { };
};
inventory.instances = {
jon-user = {
module.name = "users";
roles.default.tags.all = { };
roles.default.settings = {
user = "jon",
groups = [
"wheel"
"networkmanager"
"video"
"input"
];
};
roles.default.extraModules = [ ./users/jon/home.nix ]; # (1)
};
# ...
# elided
};
}
```
1. Type `path` or `string`: Must point to a seperate file. Inlining a module is not possible
!!! Note "This is inspiration"
Our community might come up with better solutions soon.
We are seeking contributions to improve this pattern if you have a nicer solution in mind.
```nix title="users/jon/home.nix"
# NixOS module to import home-manager and the home-manager configuration of 'jon'
{ self, ...}:
{
imports = [ self.inputs.home-manager.nixosModules.default ];
home-manager.users.jon = {
imports = [
./home-configuration.nix
];
};
}
```

View File

@@ -1,6 +1,6 @@
# Deploy a machine # Deploy a machine
Now that you have created a machines, added some services and setup secrets. This guide will walk through how to deploy it. Now that you have created a new machine, we will walk through how to install it.
## Prerequisites ## Prerequisites
@@ -10,205 +10,256 @@ Now that you have created a machines, added some services and setup secrets. Thi
- [x] **Machine configuration**: See our basic [adding and configuring machine guide](./add-machines.md) - [x] **Machine configuration**: See our basic [adding and configuring machine guide](./add-machines.md)
- [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets. - [x] **Initialized secrets**: See [secrets](secrets.md) for how to initialize your secrets.
## Physical Hardware === "**Physical Hardware**"
!!! note "skip this if using a cloud VM" - [x] **USB Flash Drive**: See [Clan Installer](installer.md)
Steps: !!! Steps
- Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md). 1. Create a NixOS installer image and transfer it to a bootable USB drive as described in the [installer](./installer.md).
- Boot the target machine and connect it to a network that makes it reachable from your setup computer.
- Note down a reachable ip adress (*ipv4*, *ipv6* or *tor*)
--- 2. Boot the target machine and connect it to a network that makes it reachable from your setup computer.
The installer will generate a password and local addresses on boot, then run ssh with these preconfigured. === "**Cloud VMs**"
The installer shows it's deployment relevant information in two formats, a text form, as well as a QR code.
Sample boot screen shows: - [x] Any cloud machine if it is reachable via SSH and supports `kexec`.
- Root password !!! Warning "NixOS can cause strange issues when booting in certain cloud environments."
- IP address If on Linode: Make sure that the system uses Direct Disk boot kernel (found in the configuration pannel)
- Optional Tor and mDNS details
```{ .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 to wlan](./installer.md#optional-connect-to-wifi-manually).
!!! tip
Use [KDE Connect](https://apps.kde.org/de/kdeconnect/) for easyily sharing QR codes from phone to desktop
## Cloud VMs
!!! note "skip this if using a physical machine"
Clan supports any cloud machine if it is reachable via SSH and supports `kexec`.
Steps:
- Go to the configuration panel and note down how to connect to the machine via ssh.
!!! tip "NixOS can cause strange issues when booting in certain cloud environments."
If on Linode: Make sure that the system uses "Direct Disk boot kernel" (found in the configuration panel)
## Setting `targetHost` ## Setting `targetHost`
In your nix files set the targetHost (reachable ip) that you retrieved in the previous step. === "flake.nix (flake-parts)"
```{.nix title="clan.nix" hl_lines="9"} ```{.nix hl_lines="22"}
{ {
# Ensure this is unique among all clans you want to use. inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
meta.name = "my-clan"; inputs.nixpkgs.follows = "clan-core/nixpkgs";
inputs.flake-parts.follows = "clan-core/flake-parts";
inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
inventory.machines = { outputs =
# Define machines here. inputs@{ flake-parts, ... }:
# The machine name will be used as the hostname. flake-parts.lib.mkFlake { inherit inputs; } {
jon = { systems = [
deploy.targetHost = "root@192.168.192.4"; # (1) "x86_64-linux"
}; "aarch64-linux"
}; "x86_64-darwin"
# ... "aarch64-darwin"
# elided ];
} imports = [ inputs.clan-core.flakeModules.default ];
```
1. Use the ip address of your targetMachine that you want to deploy. If using the [flash-installer](./installer.md) it should display its local ip-address when booted. clan = {
inventory.machines = {
jon = {
# targetHost will get picked up by cli commands
deploy.targetHost = "root@jon";
};
};
};
};
}
```
=== "flake.nix (classic)"
```{.nix hl_lines="14"}
{
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
inputs.nixpkgs.follows = "clan-core/nixpkgs";
outputs =
{ self, clan-core, ... }:
let
clan = clan-core.lib.clan {
inherit self;
inventory.machines = {
jon = {
# targetHost will get picked up by cli commands
deploy.targetHost = "root@jon";
};
};
};
in
{
inherit (clan.config)
nixosConfigurations
nixosModules
clanInternals
darwinConfigurations
darwinModules
;
};
}
```
!!! warning !!! warning
The use of `root@` in the target address implies SSH access as the `root` user. 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. Ensure that the root login is secured and only used when necessary.
See also [how to set TargetHost](../target-host.md) for other methods. ## Identify the Target Disk
## Retrieve the hardware report On the setup computer, SSH into the target:
By default clan uses [nixos-facter](https://github.com/nix-community/nixos-facter) which captures detailed information about the machine or virtual environment. ```bash title="setup computer"
ssh root@<IP> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
To generate the hardware-report (`facter.json`) run:
```bash
clan machines update-hardware-config <machineName>
``` ```
Example output: Replace `<IP>` with the machine's IP or hostname if mDNS (i.e. Avahi) is available.
```shell-session Which should show something like:
$ clan machines update-hardware-config jon
[jon] $ nixos-facter ```{.shellSession hl_lines="6" .no-copy}
Successfully generated: ./machines/jon/facter.json NAME ID-LINK FSTYPE SIZE MOUNTPOINT
sda usb-ST_16GB_AA6271026J1000000509-0:0 14.9G
├─sda1 usb-ST_16GB_AA6271026J1000000509-0:0-part1 1M
├─sda2 usb-ST_16GB_AA6271026J1000000509-0:0-part2 vfat 100M /boot
└─sda3 usb-ST_16GB_AA6271026J1000000509-0:0-part3 ext4 2.9G /
nvme0n1 nvme-eui.e8238fa6bf530001001b448b4aec2929 476.9G
├─nvme0n1p1 nvme-eui.e8238fa6bf530001001b448b4aec2929-part1 vfat 512M
├─nvme0n1p2 nvme-eui.e8238fa6bf530001001b448b4aec2929-part2 ext4 459.6G
└─nvme0n1p3 nvme-eui.e8238fa6bf530001001b448b4aec2929-part3 swap 16.8G
``` ```
See [update-hardware-config cli reference](../../reference/cli/machines.md#machines-update-hardware-config) for further configuration possibilities if needed. Look for the top-level disk device (e.g., nvme0n1 or sda) and copy its `ID-LINK`. Avoid using partition IDs like `nvme0n1p1`.
## Configure your disk schema In this example we would copy `nvme-eui.e8238fa6bf530001001b448b4aec2929`
By default clan uses [disko](https://github.com/nix-community/disko) which allows for declarative disk partitioning.
To setup a disk schema for a machine run
```bash
clan templates apply disk single-disk jon --set mainDisk ""
```
Which should fail and give the valid options for the specific hardware:
```shellSession
Invalid value for placeholder mainDisk - Valid options:
/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368
```
Re-run the command with the correct disk:
```bash
clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368"
```
Should now be succesfull
```shellSession
Applied disk template 'single-disk' to machine 'jon'
```
A disko.nix file should be created in `machines/jon`
You can have a look and customize it if needed.
!!! tip !!! tip
For advanced partitioning, see [Disko templates](https://github.com/nix-community/disko-templates) or [Disko examples](https://github.com/nix-community/disko/tree/master/example). For advanced partitioning, see [Disko templates](https://github.com/nix-community/disko-templates) or [Disko examples](https://github.com/nix-community/disko/tree/master/example).
## Fill in hardware specific machine configuration
Edit the following fields inside the `./machines/<machine_name>/configuration.nix`
<!-- Note: Use "jon" instead of "<machine>" as "<" is not supported in title tag -->
```nix title="./machines/jon/configuration.nix" hl_lines="12 15 19"
{
imports = [
# contains your disk format and partitioning configuration.
../../modules/disko.nix
# this file is shared among all machines
../../modules/shared.nix
# enables GNOME desktop (optional)
../../modules/gnome.nix
];
# Put your username here for login
users.users.user.name = "__YOUR_USERNAME__";
# Replace this __CHANGE_ME__ with the copied result of the lsblk command
disko.devices.disk.main.device = "/dev/disk/by-id/__CHANGE_ME__";
# IMPORTANT! Add your SSH key here
# e.g. > cat ~/.ssh/id_ed25519.pub
users.users.root.openssh.authorizedKeys.keys = [ "__YOUR_SSH_KEY__" ];
# ...
}
```
!!! Info "Replace `__YOUR_USERNAME__` with the ip of your machine, if you use avahi you can also use your hostname"
!!! Info "Replace `__CHANGE_ME__` with the appropriate `ID-LINK` identifier, such as `nvme-eui.e8238fa6bf530001001b448b4aec2929`"
!!! Info "Replace `__YOUR_SSH_KEY__` with your personal key, like `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILoMI0NC5eT9pHlQExrvR5ASV3iW9+BXwhfchq0smXUJ jon@jon-desktop`"
## Deploy the machine ## Deploy the machine
**Finally deployment time!** Use one of the following commands to build and deploy the image via SSH onto your machine. **Finally deployment time!** Use the following command to build and deploy the image via SSH onto your machine.
=== "**Image Installer**"
The installer will 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.
Sample boot screen shows:
- Root password
- IP address
- Optional Tor and mDNS details
```{ .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 to wlan](./installer.md#optional-connect-to-wifi-manually).
!!! tip
Use [KDE Connect](https://apps.kde.org/de/kdeconnect/) for easyily sharing QR codes from phone to desktop
=== "**Cloud VM**"
Just run the command **Option B: Cloud VM** below
### Deployment Commands ### Deployment Commands
#### Using password auth #### Using password auth
```bash ```bash
clan machines install [MACHINE] --target-host <IP> clan machines install [MACHINE] --target-host <IP> --update-hardware-config nixos-facter
``` ```
#### Using QR JSON #### Using QR JSON
```bash ```bash
clan machines install [MACHINE] --json "[JSON]" clan machines install [MACHINE] --json "[JSON]" --update-hardware-config nixos-facter
``` ```
#### Using QR image file #### Using QR image file
```bash ```bash
clan machines install [MACHINE] --png [PATH] clan machines install [MACHINE] --png [PATH] --update-hardware-config nixos-facter
``` ```
#### Option B: Cloud VM #### Option B: Cloud VM
```bash ```bash
clan machines install [MACHINE] --target-host <IP> clan machines install [MACHINE] --target-host <IP> --update-hardware-config nixos-facter
``` ```
!!! success !!! success

View File

@@ -38,24 +38,31 @@ By the end of this guide, you'll have a fresh NixOS configuration ready to push
## Add Clan CLI to Your Shell ## Add Clan CLI to Your Shell
Create a new clan Add the Clan CLI into your environment:
```bash ```bash
nix run git+https://git.clan.lol/clan/clan-core#clan-cli --refresh -- flakes create nix shell git+https://git.clan.lol/clan/clan-core#clan-cli --refresh
``` ```
This should prompt for a *name*:
```terminalSession ```terminalSession
Enter a name for the new clan: my-clan clan --help
``` ```
Enter a *name*, confirm with *enter*. A directory with that name will be created and initialized. Should print the available commands.
!!! Note Also checkout the [cli-reference documentation](../../reference/cli/index.md).
This command uses the `default` template
See `clan templates list` and the `--help` reference for how to use other templates. ## Initialize Your Project
If you want to migrate an existing project, follow this [guide](../migrations/migration-guide.md).
Set the foundation of your Clan project by initializing it by running:
```bash
clan flakes create my-clan
```
This command creates a `flake.nix` and some other files for your project.
## Explore the Project Structure ## Explore the Project Structure
@@ -76,48 +83,36 @@ For example, you might see something like:
└── README.md └── README.md
``` ```
Dont worry if your output looks different — Clan templates evolve over time. Dont worry if your output looks different—the template evolves over time.
To interact with your newly created clan the you need to load the `clan` cli-package it into your environment by running: ??? info "Recommended way of sourcing the `clan` CLI tool"
=== "Automatic (direnv, recommended)" The default template adds the `clan` CLI tool to the development shell.
- prerequisite: [install nix-direnv](https://github.com/nix-community/nix-direnv) This means that you can access the `clan` CLI tool directly from the folder
you are in right now.
``` In the `my-clan` directory, run the following command:
direnv allow
```
=== "Manual (nix develop)"
``` ```
nix develop nix develop
``` ```
verify that you can run `clan` commands: This will ensure the `clan` CLI tool is available in your shell environment.
```bash To automatically add the `clan` CLI tool to your environment without having to
run `nix develop` every time, we recommend setting up [direnv](https://direnv.net/).
```
clan show clan show
``` ```
You should see something like this: You should see something like this:
```shellSession ```terminal-session
Name: __CHANGE_ME__ Name: my-clan
Description: None Description: None
``` ```
To change the name of your clan edit `meta.name` in the `clan.nix` or `flake.nix` file
```{.nix title="clan.nix" hl_lines="3"}
{
# Ensure this is unique among all clans you want to use.
meta.name = "__CHANGE_ME__";
# ...
# elided
}
```
--- ---
## Next Steps ## Next Steps
@@ -128,7 +123,6 @@ You can continue with **any** of the following steps at your own pace:
- [x] [Initialize Clan](./index.md#initialize-your-project) - [x] [Initialize Clan](./index.md#initialize-your-project)
- [ ] [Create USB Installer (optional)](./installer.md) - [ ] [Create USB Installer (optional)](./installer.md)
- [ ] [Add Machines](./add-machines.md) - [ ] [Add Machines](./add-machines.md)
- [ ] [Add a User](./add-user.md)
- [ ] [Add Services](./add-services.md) - [ ] [Add Services](./add-services.md)
- [ ] [Configure Secrets](./secrets.md) - [ ] [Configure Secrets](./secrets.md)
- [ ] [Deploy](./deploy.md) - Requires configured secrets - [ ] [Deploy](./deploy.md) - Requires configured secrets

View File

@@ -52,7 +52,7 @@ clan vars list <machineName>
Which should print the generated `disk-id/diskId` value in clear text Which should print the generated `disk-id/diskId` value in clear text
You should see output like: You should see output like:
```shellSession ```terminal-session
disk-id/diskId: fcef30a749f8451d8f60c46e1ead726f disk-id/diskId: fcef30a749f8451d8f60c46e1ead726f
# ... # ...
# elided # elided

View File

@@ -35,20 +35,10 @@ in
inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules inputName: v: lib.mapAttrs (inspectModule inputName) v.clan.modules
) inputsWithModules; ) inputsWithModules;
}; };
options.moduleSchemas = lib.mkOption { options.localModules = lib.mkOption {
# { sourceName :: { moduleName :: { roleName :: Schema }}}
readOnly = true; readOnly = true;
type = lib.types.raw; type = lib.types.raw;
default = lib.mapAttrs ( default = config.modulesPerSource.self;
_inputName: moduleSet:
lib.mapAttrs (
_moduleName: module:
(clanLib.evalService {
modules = [ module ];
prefix = [ ];
}).config.result.api.schema
) moduleSet
) config.modulesPerSource;
}; };
options.templatesPerSource = lib.mkOption { options.templatesPerSource = lib.mkOption {
# { sourceName :: { moduleName :: {} }} # { sourceName :: { moduleName :: {} }}

View File

@@ -3,7 +3,7 @@ import { render } from "solid-js/web";
import "./index.css"; import "./index.css";
import { QueryClient } from "@tanstack/solid-query"; import { QueryClient } from "@tanstack/solid-query";
import { CubeScene } from "./scene/cubes"; import { CubeScene } from "./scene/qubes";
export const client = new QueryClient(); export const client = new QueryClient();

View File

@@ -402,16 +402,24 @@ export function CubeScene() {
if (selected) { if (selected) {
// When selected, make all faces red-ish but maintain the lighting difference // When selected, make all faces red-ish but maintain the lighting difference
materials.forEach((material, index) => { materials.forEach((material, index) => {
if (index === 2) { (material as THREE.MeshBasicMaterial).color.set(
(material as THREE.MeshBasicMaterial).color.set(0xff6666); index === 2
} ? 0xff6666 // Top face - lighter red
: index === 0 || index === 4
? 0xdce4e5 // Front/right faces - keep
: 0xa4b3b5, // Shadow faces - keep
);
}); });
} else { } else {
// Normal colors - restore original face colors // Normal colors - restore original face colors
materials.forEach((material, index) => { materials.forEach((material, index) => {
if (index === 2) { (material as THREE.MeshBasicMaterial).color.set(
(material as THREE.MeshBasicMaterial).color.set(0xffffff); index === 2
} ? 0xffffff // Top face - light
: index === 0 || index === 4
? 0xdce4e5 // Front/right faces - medium
: 0xa4b3b5, // Shadow faces - dark
);
}); });
} }
} }

View File

@@ -6,7 +6,7 @@ sys.path.insert(
0, os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 0, os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
) )
from clan_cli.cli import main # NOQA from clan_cli import main # NOQA
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -6,7 +6,7 @@ sys.path.insert(
0, os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 0, os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
) )
from clan_cli.cli import config # NOQA from clan_cli import config # NOQA
if __name__ == "__main__": if __name__ == "__main__":
config.main() config.main()

View File

@@ -0,0 +1,486 @@
import argparse
import contextlib
import logging
import sys
from pathlib import Path
from types import ModuleType
from clan_lib.custom_logger import setup_logging
from clan_lib.dirs import get_clan_flake_toplevel_or_env
from clan_lib.errors import ClanError
from clan_lib.flake import Flake
from . import (
backups,
clan,
secrets,
select,
state,
templates,
vms,
)
from .arg_actions import AppendOptionAction
from .clan import show
from .facts import cli as facts
from .flash import cli as flash_cli
from .hyperlink import help_hyperlink
from .machines import cli as machines
from .profiler import profile
from .ssh import deploy_info as ssh_cli
from .vars import cli as vars_cli
log = logging.getLogger(__name__)
argcomplete: ModuleType | None = None
with contextlib.suppress(ImportError):
import argcomplete # type: ignore[no-redef]
def flake_path(arg: str) -> str:
flake_dir = Path(arg).resolve()
if flake_dir.exists() and flake_dir.is_dir():
return str(flake_dir)
return arg
def default_flake() -> str | None:
val = get_clan_flake_toplevel_or_env()
if val:
return str(val)
return None
def create_flake_from_args(args: argparse.Namespace) -> Flake:
"""Create a Flake object from parsed arguments, including nix_options."""
flake_path_str = args.flake
nix_options = getattr(args, "option", [])
return Flake(flake_path_str, nix_options=nix_options)
def add_common_flags(parser: argparse.ArgumentParser) -> None:
def argument_exists(parser: argparse.ArgumentParser, arg: str) -> bool:
"""
Check if an argparse argument already exists.
This is needed because the aliases subcommand doesn't *really*
create an alias - it duplicates the actual parser in the tree
making duplication inevitable while naively traversing.
The error that would be thrown by argparse:
- argparse.ArgumentError
"""
return any(
arg in action.option_strings
for action in parser._actions # noqa: SLF001
)
if not argument_exists(parser, "--debug"):
parser.add_argument(
"--debug",
help="Enable debug logging",
action="store_true",
default=False,
)
if not argument_exists(parser, "--option"):
parser.add_argument(
"--option",
help="Nix option to set",
nargs=2,
metavar=("name", "value"),
action=AppendOptionAction,
default=[],
)
if not argument_exists(parser, "--flake"):
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",
default=default_flake(),
metavar="PATH",
type=flake_path,
)
def register_common_flags(parser: argparse.ArgumentParser) -> None:
has_subparsers = False
for action in parser._actions: # noqa: SLF001
if isinstance(action, argparse._SubParsersAction): # noqa: SLF001
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,
usage="%(prog)s [-h] [SUBCOMMAND]",
description="The clan cli tool",
epilog=(
f"""
Online reference for the clan cli tool: {help_hyperlink("cli reference", "https://docs.clan.lol/reference/cli")}
For more detailed information, visit: {help_hyperlink("docs", "https://docs.clan.lol")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
subparsers = parser.add_subparsers()
show_parser = subparsers.add_parser(
"show",
help="Show meta information about the clan",
description="Show meta information about the clan",
epilog=(
"""
This command prints the metadata of a clan.
Examples:
$ clan show --flake [PATH]
Name: My Empty Clan
Description: some nice description
Icon: A path to the png
"""
),
)
show_parser.set_defaults(func=show.show_command)
parser_backups = subparsers.add_parser(
"backups",
aliases=["b"],
help="Manage backups of clan machines",
description="Manage backups of clan machines",
epilog=(
f"""
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: {help_hyperlink("backups", "https://docs.clan.lol/guides/backups")}.
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
backups.register_parser(parser_backups)
parser_flake = subparsers.add_parser(
"flakes",
aliases=["f"],
help="Create a clan flake inside the current directory",
description="Create a clan flake inside the current directory",
epilog=(
f"""
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: {help_hyperlink("getting-started", "https://docs.clan.lol/guides/getting-started/index.html")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
clan.register_parser(parser_flake)
parser_templates = subparsers.add_parser(
"templates",
help="Subcommands to interact with templates",
formatter_class=argparse.RawTextHelpFormatter,
)
templates.register_parser(parser_templates)
parser_flash = subparsers.add_parser(
"flash",
help="Flashes your machine to an USB drive",
description="Flashes your machine to an USB drive",
epilog=(
f"""
Examples:
$ clan flash write mymachine --disk main /dev/sd<X> --ssh-pubkey ~/.ssh/id_rsa.pub
Will flash the machine 'mymachine' to the disk '/dev/sd<X>' with the ssh public key '~/.ssh/id_rsa.pub'.
For more detailed information, visit: {help_hyperlink("getting-started", "https://docs.clan.lol/guides/getting-started/installer")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
flash_cli.register_parser(parser_flash)
parser_ssh = subparsers.add_parser(
"ssh",
help="Ssh to a remote machine",
description="Ssh to a remote machine",
epilog=(
f"""
This subcommand allows seamless ssh access to the nixos-image builders or a machine of your clan.
Examples:
$ clan ssh [ssh_args ...] berlin`
Will ssh in to the machine called `berlin`, using the
`clan.core.networking.targetHost` specified in its configuration
$ clan ssh [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: {help_hyperlink("deploy", "https://docs.clan.lol/guides/getting-started/deploy")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
ssh_cli.register_parser(parser_ssh)
parser_secrets = subparsers.add_parser(
"secrets",
help="Manage secrets",
description="Manage secrets",
epilog=(
f"""
This subcommand provides an interface to secrets.
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: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
secrets.register_parser(parser_secrets)
parser_facts = subparsers.add_parser(
"facts",
help="Manage facts",
description="Manage facts",
epilog=(
f"""
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: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
facts.register_parser(parser_facts)
# like facts but with vars instead of facts
parser_vars = subparsers.add_parser(
"vars",
aliases=["va"],
help="Manage vars",
description="Manage vars",
epilog=(
f"""
This subcommand provides an interface to `vars` of clan machines.
Vars are variables that a service can generate.
There are public and secret vars.
Public vars can be referenced by other machines directly.
Public vars can include: ip addresses, public keys.
Secret vars can include: passwords, private keys.
A service is an included clan-module that implements vars generation functionality.
For example the zerotier module will generate private and public vars.
In this case the public var will be the resulting zerotier-ip of the machine.
The secret var will be the zerotier-identity-secret, which is used by zerotier
to prove the machine has control of the zerotier-ip.
Examples:
$ clan vars generate
Will generate vars for all machines.
$ clan vars generate --service [SERVICE] --regenerate
Will regenerate vars, if they are already generated for a specific service.
This is especially useful for resetting certain passwords while leaving the rest
of the vars for a machine in place.
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
vars_cli.register_parser(parser_vars)
parser_machine = subparsers.add_parser(
"machines",
aliases=["m"],
help="Manage machines and their configuration",
description="Manage machines and their configuration",
epilog=(
f"""
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 machines [MACHINES], if [MACHINES] is omitted, the command
will attempt to update every configured machine.
$ clan machines install [MACHINE] --target-host [TARGET_HOST]
Will install the specified machine [MACHINE] to the specified [TARGET_HOST].
If the `--target-host` flag is omitted will try to find host information by
checking the deployment configuration inside the specified machine.
For more detailed information, visit: {help_hyperlink("deploy", "https://docs.clan.lol/guides/getting-started/deploy")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
machines.register_parser(parser_machine)
parser_vms = subparsers.add_parser(
"vms", help="Manage virtual machines", description="Manage virtual machines"
)
vms.register_parser(parser_vms)
parser_select = subparsers.add_parser(
"select",
aliases=["se"],
help="Select nixos values from the flake",
description="Select nixos values from the flake",
epilog=(
"""
This subcommand provides an interface nix values defined in the flake.
Examples:
$ clan select nixosConfigurations.*.config.networking.hostName
List hostnames of all nixos configurations as JSON.
$ clan select nixosConfigurations.{jon,alice}.config.clan.core.vars.generators.*.name
List all vars generators for jon and alice.
$ clan select nixosConfigurations.jon.config.envirnonment.systemPackages.1
List the first system package for jon.
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
select.register_parser(parser_select)
parser_state = subparsers.add_parser(
"state",
aliases=["st"],
help="Query state information about machines",
description="Query state information about machines",
epilog=(
f"""
This subcommand provides an interface to the state managed by Clan.
State can be folders and databases that modules depend on managed by Clan.
State directories can be added to on a per machine basis:
```
config.clan.core.state.[SERVICE_NAME].folders = [
"/home"
"/root"
];
```
Here [SERVICE_NAME] can be set freely, if the user sets them extra `userdata`
can be a good choice.
Examples:
$ clan state list [MACHINE]
List state of the machines managed by Clan.
For more detailed information, visit: {help_hyperlink("getting-started", "https://docs.clan.lol/guides/backups")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
state.register_parser(parser_state)
if argcomplete:
argcomplete.autocomplete(parser, exclude=["morph"])
register_common_flags(parser)
return parser
# this will be the entrypoint under /bin/clan (see pyproject.toml config)
@profile
def main() -> None:
parser = create_parser()
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
if debug := getattr(args, "debug", False):
setup_logging(logging.DEBUG)
log.debug("Debug log activated")
else:
setup_logging(logging.INFO)
if not hasattr(args, "func"):
return
# Convert flake path to Flake object with nix_options if flake argument exists
if hasattr(args, "flake") and args.flake is not None:
args.flake = create_flake_from_args(args)
try:
args.func(args)
except ClanError as e:
if debug:
log.exception("Exited with error")
else:
log.error("%s", e)
sys.exit(1)
except KeyboardInterrupt as ex:
log.warning("Interrupted by user", exc_info=ex)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,4 +1,4 @@
from .cli import main from . import main
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -4,7 +4,6 @@ import logging
from pathlib import Path from pathlib import Path
from clan_lib.clan.create import CreateOptions, create_clan from clan_lib.clan.create import CreateOptions, create_clan
from clan_lib.errors import ClanError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -27,10 +26,10 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
) )
parser.add_argument( parser.add_argument(
"name", "path",
type=str, type=Path,
nargs="?", help="Path where to write the clan template to",
help="Name of the clan to create. If not provided, will prompt for a name.", default=Path(),
) )
parser.add_argument( parser.add_argument(
@@ -41,18 +40,9 @@ def register_create_parser(parser: argparse.ArgumentParser) -> None:
) )
def create_flake_command(args: argparse.Namespace) -> None: def create_flake_command(args: argparse.Namespace) -> None:
# Ask for a path interactively if none provided
if args.name is None:
user_input = input("Enter a name for the new clan: ").strip()
if not user_input:
msg = "Error: name is required."
raise ClanError(msg)
args.name = Path(user_input)
create_clan( create_clan(
CreateOptions( CreateOptions(
dest=Path(args.name), dest=args.path,
template=args.template, template=args.template,
setup_git=not args.no_git, setup_git=not args.no_git,
src_flake=args.flake, src_flake=args.flake,

View File

@@ -1,508 +0,0 @@
import argparse
import contextlib
import logging
import sys
from pathlib import Path
from types import ModuleType
from clan_lib.custom_logger import setup_logging
from clan_lib.dirs import get_clan_flake_toplevel_or_env
from clan_lib.errors import ClanError
from clan_lib.flake import Flake
from . import (
backups,
clan,
secrets,
select,
state,
templates,
vms,
)
from .arg_actions import AppendOptionAction
from .clan import show
from .facts import cli as facts
from .flash import cli as flash_cli
from .hyperlink import help_hyperlink
from .machines import cli as machines
from .profiler import profile
from .ssh import deploy_info as ssh_cli
from .vars import cli as vars_cli
log = logging.getLogger(__name__)
argcomplete: ModuleType | None = None
with contextlib.suppress(ImportError):
import argcomplete # type: ignore[no-redef]
def flake_path(arg: str) -> str:
flake_dir = Path(arg).resolve()
if flake_dir.exists() and flake_dir.is_dir():
return str(flake_dir)
return arg
def default_flake() -> str | None:
val = get_clan_flake_toplevel_or_env()
if val:
return str(val)
return None
def create_flake_from_args(args: argparse.Namespace) -> Flake:
"""Create a Flake object from parsed arguments, including nix_options."""
flake_path_str = args.flake
nix_options = getattr(args, "option", [])
return Flake(flake_path_str, nix_options=nix_options)
def add_common_flags(parser: argparse.ArgumentParser) -> None:
def argument_exists(parser: argparse.ArgumentParser, arg: str) -> bool:
"""
Check if an argparse argument already exists.
This is needed because the aliases subcommand doesn't *really*
create an alias - it duplicates the actual parser in the tree
making duplication inevitable while naively traversing.
The error that would be thrown by argparse:
- argparse.ArgumentError
"""
return any(
arg in action.option_strings
for action in parser._actions # noqa: SLF001
)
if not argument_exists(parser, "--debug"):
parser.add_argument(
"--debug",
help="Enable debug logging",
action="store_true",
default=False,
)
if not argument_exists(parser, "--option"):
parser.add_argument(
"--option",
help="Nix option to set",
nargs=2,
metavar=("name", "value"),
action=AppendOptionAction,
default=[],
)
if not argument_exists(parser, "--flake"):
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",
default=default_flake(),
metavar="PATH",
type=flake_path,
)
def register_common_flags(parser: argparse.ArgumentParser) -> None:
has_subparsers = False
for action in parser._actions: # noqa: SLF001
if isinstance(action, argparse._SubParsersAction): # noqa: SLF001
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,
usage="%(prog)s [-h] [SUBCOMMAND]",
description="The clan cli tool",
epilog=(
f"""
Online reference for the clan cli tool: {help_hyperlink("cli reference", "https://docs.clan.lol/reference/cli")}
For more detailed information, visit: {help_hyperlink("docs", "https://docs.clan.lol")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
subparsers = parser.add_subparsers()
show_parser = subparsers.add_parser(
"show",
help="Show meta information about the clan",
description="Show meta information about the clan",
epilog=(
"""
This command prints the metadata of a clan.
Examples:
$ clan show --flake [PATH]
Name: My Empty Clan
Description: some nice description
Icon: A path to the png
"""
),
)
show_parser.set_defaults(func=show.show_command)
parser_backups = subparsers.add_parser(
"backups",
aliases=["b"],
help="Manage backups of clan machines",
description="Manage backups of clan machines",
epilog=(
f"""
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: {help_hyperlink("backups", "https://docs.clan.lol/guides/backups")}.
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
backups.register_parser(parser_backups)
parser_flake = subparsers.add_parser(
"flakes",
aliases=["f"],
help="Create a clan flake inside the current directory",
description="Create a clan flake inside the current directory",
epilog=(
f"""
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: {help_hyperlink("getting-started", "https://docs.clan.lol/guides/getting-started/index.html")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
clan.register_parser(parser_flake)
parser_templates = subparsers.add_parser(
"templates",
help="Interact with templates",
description="Interact with templates",
epilog=(
"""
This subcommand provides an interface to templates provided by clan.
Examples:
$ clan templates list
List all the machines managed by Clan.
$ clan templates apply disk [TEMPLATE] [MACHINE]
Will apply the specified [TEMPLATE] to the [MACHINE]
Many templates require to *set* variables via the `--set` flag.
$ clan templates apply disk [TEMPLATE] [MACHINE] --set key1 value1 --set key2 value2
Real world example
$ clan templates apply disk single-disk jon --set mainDisk "/dev/disk/by-id/nvme-WD_PC_SN740_SDDQNQD-512G-1201_232557804368"
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
templates.register_parser(parser_templates)
parser_flash = subparsers.add_parser(
"flash",
help="Flashes your machine to an USB drive",
description="Flashes your machine to an USB drive",
epilog=(
f"""
Examples:
$ clan flash write mymachine --disk main /dev/sd<X> --ssh-pubkey ~/.ssh/id_rsa.pub
Will flash the machine 'mymachine' to the disk '/dev/sd<X>' with the ssh public key '~/.ssh/id_rsa.pub'.
For more detailed information, visit: {help_hyperlink("getting-started", "https://docs.clan.lol/guides/getting-started/installer")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
flash_cli.register_parser(parser_flash)
parser_ssh = subparsers.add_parser(
"ssh",
help="Ssh to a remote machine",
description="Ssh to a remote machine",
epilog=(
f"""
This subcommand allows seamless ssh access to the nixos-image builders or a machine of your clan.
Examples:
$ clan ssh [ssh_args ...] berlin`
Will ssh in to the machine called `berlin`, using the
`clan.core.networking.targetHost` specified in its configuration
$ clan ssh [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: {help_hyperlink("deploy", "https://docs.clan.lol/guides/getting-started/deploy")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
ssh_cli.register_parser(parser_ssh)
parser_secrets = subparsers.add_parser(
"secrets",
help="Manage secrets",
description="Manage secrets",
epilog=(
f"""
This subcommand provides an interface to secrets.
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: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
secrets.register_parser(parser_secrets)
parser_facts = subparsers.add_parser(
"facts",
help="Manage facts",
description="Manage facts",
epilog=(
f"""
Note: Facts are being deprecated, please use Vars instead.
For a migration guide visit: {help_hyperlink("vars", "https://docs.clan.lol/guides/migrations/migration-facts-vars")}
This subcommand provides an interface to facts of clan machines.
Facts are artifacts that a service can generate.
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: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
facts.register_parser(parser_facts)
# like facts but with vars instead of facts
parser_vars = subparsers.add_parser(
"vars",
aliases=["va"],
help="Manage vars",
description="Manage vars",
epilog=(
f"""
This subcommand provides an interface to `vars` of clan machines.
Vars are variables that a service can generate.
There are public and secret vars.
Public vars can be referenced by other machines directly.
Public vars can include: ip addresses, public keys.
Secret vars can include: passwords, private keys.
A service is an included clan-module that implements vars generation functionality.
For example the zerotier module will generate private and public vars.
In this case the public var will be the resulting zerotier-ip of the machine.
The secret var will be the zerotier-identity-secret, which is used by zerotier
to prove the machine has control of the zerotier-ip.
Examples:
$ clan vars generate
Will generate vars for all machines.
$ clan vars generate --service [SERVICE] --regenerate
Will regenerate vars, if they are already generated for a specific service.
This is especially useful for resetting certain passwords while leaving the rest
of the vars for a machine in place.
For more detailed information, visit: {help_hyperlink("secrets", "https://docs.clan.lol/guides/getting-started/secrets")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
vars_cli.register_parser(parser_vars)
parser_machine = subparsers.add_parser(
"machines",
aliases=["m"],
help="Manage machines and their configuration",
description="Manage machines and their configuration",
epilog=(
f"""
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 machines [MACHINES], if [MACHINES] is omitted, the command
will attempt to update every configured machine.
$ clan machines install [MACHINE] --target-host [TARGET_HOST]
Will install the specified machine [MACHINE] to the specified [TARGET_HOST].
If the `--target-host` flag is omitted will try to find host information by
checking the deployment configuration inside the specified machine.
For more detailed information, visit: {help_hyperlink("deploy", "https://docs.clan.lol/guides/getting-started/deploy")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
machines.register_parser(parser_machine)
parser_vms = subparsers.add_parser(
"vms", help="Manage virtual machines", description="Manage virtual machines"
)
vms.register_parser(parser_vms)
parser_select = subparsers.add_parser(
"select",
aliases=["se"],
help="Select nixos values from the flake",
description="Select nixos values from the flake",
epilog=(
"""
This subcommand provides an interface nix values defined in the flake.
Examples:
$ clan select nixosConfigurations.*.config.networking.hostName
List hostnames of all nixos configurations as JSON.
$ clan select nixosConfigurations.{jon,alice}.config.clan.core.vars.generators.*.name
List all vars generators for jon and alice.
$ clan select nixosConfigurations.jon.config.envirnonment.systemPackages.1
List the first system package for jon.
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
select.register_parser(parser_select)
parser_state = subparsers.add_parser(
"state",
aliases=["st"],
help="Query state information about machines",
description="Query state information about machines",
epilog=(
f"""
This subcommand provides an interface to the state managed by Clan.
State can be folders and databases that modules depend on managed by Clan.
State directories can be added to on a per machine basis:
```
config.clan.core.state.[SERVICE_NAME].folders = [
"/home"
"/root"
];
```
Here [SERVICE_NAME] can be set freely, if the user sets them extra `userdata`
can be a good choice.
Examples:
$ clan state list [MACHINE]
List state of the machines managed by Clan.
For more detailed information, visit: {help_hyperlink("getting-started", "https://docs.clan.lol/guides/backups")}
"""
),
formatter_class=argparse.RawTextHelpFormatter,
)
state.register_parser(parser_state)
if argcomplete:
argcomplete.autocomplete(parser, exclude=["morph"])
register_common_flags(parser)
return parser
# this will be the entrypoint under /bin/clan (see pyproject.toml config)
@profile
def main() -> None:
parser = create_parser()
args = parser.parse_args()
if len(sys.argv) == 1:
parser.print_help()
if debug := getattr(args, "debug", False):
setup_logging(logging.DEBUG)
log.debug("Debug log activated")
else:
setup_logging(logging.INFO)
if not hasattr(args, "func"):
return
# Convert flake path to Flake object with nix_options if flake argument exists
if hasattr(args, "flake") and args.flake is not None:
args.flake = create_flake_from_args(args)
try:
args.func(args)
except ClanError as e:
if debug:
log.exception("Exited with error")
else:
log.error("%s", e)
sys.exit(1)
except KeyboardInterrupt as ex:
log.warning("Interrupted by user", exc_info=ex)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -249,6 +249,26 @@ def complete_groups(
return groups_dict return groups_dict
def complete_templates_disko(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]:
"""
Provides completion functionality for disko templates
"""
from clan_lib.templates import list_templates
flake = clan_dir_result if (clan_dir_result := clan_dir(None)) is not None else "."
list_all_templates = list_templates(Flake(flake))
disko_template_list = list_all_templates.builtins.get("disko")
if disko_template_list:
disko_templates = list(disko_template_list)
disko_dict = dict.fromkeys(disko_templates, "disko")
return disko_dict
return []
def complete_target_host( def complete_target_host(
prefix: str, parsed_args: argparse.Namespace, **kwargs: Any prefix: str, parsed_args: argparse.Namespace, **kwargs: Any
) -> Iterable[str]: ) -> Iterable[str]:

View File

@@ -3,13 +3,10 @@ import logging
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any from typing import Any
from clan_lib.api.disk import set_machine_disk_schema
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
from clan_lib.templates.disk import set_machine_disk_schema
from clan_cli.completions import ( from clan_cli.completions import add_dynamic_completer, complete_templates_disko
add_dynamic_completer,
complete_machines,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -37,27 +34,31 @@ def apply_command(args: argparse.Namespace) -> None:
placeholders = dict(set_tuples) placeholders = dict(set_tuples)
set_machine_disk_schema( set_machine_disk_schema(
Machine(args.machine, args.flake), Machine(args.to_machine, args.flake),
args.template, args.template,
placeholders, placeholders,
force=args.force, force=args.force,
check_hw=not args.skip_hardware_check, check_hw=not args.skip_hardware_check,
) )
log.info(f"Applied disk template '{args.template}' to machine '{args.machine}' ") log.info(f"Applied disk template '{args.template}' to machine '{args.to_machine}' ")
def register_apply_disk_template_parser(parser: argparse.ArgumentParser) -> None: def register_apply_disk_template_parser(parser: argparse.ArgumentParser) -> None:
parser.add_argument( parser.add_argument(
"template", "--to-machine",
type=str,
help="The name of the disk template to apply",
)
machine_action = parser.add_argument(
"machine",
type=str, type=str,
required=True,
help="The machine to apply the template to", help="The machine to apply the template to",
) )
add_dynamic_completer(machine_action, complete_machines)
template_action = parser.add_argument(
"--template",
type=str,
required=True,
help="The name of the disk template to apply",
)
add_dynamic_completer(template_action, complete_templates_disko)
parser.add_argument( parser.add_argument(
"--set", "--set",
help="Set a placeholder in the template to a value", help="Set a placeholder in the template to a value",

View File

@@ -2,7 +2,7 @@ import argparse
import logging import logging
import shlex import shlex
from clan_cli.cli import create_flake_from_args, create_parser from clan_cli import create_flake_from_args, create_parser
from clan_lib.custom_logger import print_trace from clan_lib.custom_logger import print_trace
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
import pytest import pytest
from clan_cli.machines.create import CreateOptions, create_machine from clan_cli.machines.create import CreateOptions, create_machine
from clan_cli.tests.fixtures_flakes import FlakeForTest from clan_cli.tests.fixtures_flakes import FlakeForTest
from clan_lib.api.modules import list_modules
from clan_lib.flake import Flake from clan_lib.flake import Flake
from clan_lib.nix import nix_eval, run from clan_lib.nix import nix_eval, run
from clan_lib.nix_models.clan import ( from clan_lib.nix_models.clan import (
@@ -15,7 +16,6 @@ from clan_lib.nix_models.clan import (
) )
from clan_lib.persist.inventory_store import InventoryStore from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import set_value_by_path from clan_lib.persist.util import set_value_by_path
from clan_lib.services.modules import list_service_modules
if TYPE_CHECKING: if TYPE_CHECKING:
from .age_keys import KeyPair from .age_keys import KeyPair
@@ -27,9 +27,10 @@ from clan_lib.machines.machines import Machine as MachineMachine
@pytest.mark.with_core @pytest.mark.with_core
def test_list_modules(test_flake_with_core: FlakeForTest) -> None: def test_list_modules(test_flake_with_core: FlakeForTest) -> None:
base_path = test_flake_with_core.path base_path = test_flake_with_core.path
modules_info = list_service_modules(Flake(str(base_path))) modules_info = list_modules(str(base_path))
assert "modules" in modules_info assert "localModules" in modules_info
assert "modulesPerSource" in modules_info
@pytest.mark.impure @pytest.mark.impure

View File

@@ -6,12 +6,12 @@ from typing import Any, TypedDict
from uuid import uuid4 from uuid import uuid4
from clan_lib.api import API from clan_lib.api import API
from clan_lib.api.modules import Frontmatter, extract_frontmatter
from clan_lib.dirs import TemplateType, clan_templates from clan_lib.dirs import TemplateType, clan_templates
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
from clan_lib.git import commit_file from clan_lib.git import commit_file
from clan_lib.machines.hardware import HardwareConfig, get_machine_hardware_config from clan_lib.machines.hardware import HardwareConfig, get_machine_hardware_config
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
from clan_lib.services.modules import Frontmatter, extract_frontmatter
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -188,7 +188,7 @@ def set_machine_disk_schema(
# check that all required placeholders are present # check that all required placeholders are present
for placeholder_name, schema_placeholder in disk_schema.placeholders.items(): for placeholder_name, schema_placeholder in disk_schema.placeholders.items():
if schema_placeholder.required and placeholder_name not in placeholders: if schema_placeholder.required and placeholder_name not in placeholders:
msg = f"Required to set template variable {placeholder_name}" msg = f"Required placeholder {placeholder_name} - {schema_placeholder} missing"
raise ClanError(msg) raise ClanError(msg)
# For every placeholder check that the value is valid # For every placeholder check that the value is valid

View File

@@ -4,10 +4,10 @@ from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, TypedDict from typing import Any, TypedDict
from clan_lib.api import API
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
from clan_lib.flake import Flake from clan_lib.flake import Flake
from clan_lib.nix_models.clan import InventoryInstanceModuleType
from . import API
class CategoryInfo(TypedDict): class CategoryInfo(TypedDict):
@@ -154,97 +154,23 @@ class ModuleInfo(TypedDict):
roles: dict[str, None] roles: dict[str, None]
class ModuleList(TypedDict): class ModuleLists(TypedDict):
modules: dict[str, dict[str, ModuleInfo]] modulesPerSource: dict[str, dict[str, ModuleInfo]]
localModules: dict[str, ModuleInfo]
@API.register @API.register
def list_service_modules(flake: Flake) -> ModuleList: def list_modules(base_path: str) -> ModuleLists:
""" """
Show information about a module Show information about a module
""" """
modules = flake.select("clanInternals.inventoryClass.modulesPerSource") flake = Flake(base_path)
modules = flake.select(
return ModuleList({"modules": modules}) "clanInternals.inventoryClass.{?modulesPerSource,?localModules}"
@API.register
def get_service_module(
flake: Flake, module_ref: InventoryInstanceModuleType
) -> ModuleInfo:
"""
Returns the module information for a given module reference
:param module_ref: The module reference to get the information for
:return: Dict of module information
:raises ClanError: If the module_ref is invalid or missing required fields
"""
input_name, module_name = check_service_module_ref(flake, module_ref)
avilable_modules = list_service_modules(flake)
module_set = avilable_modules.get("modules", {}).get(input_name)
assert module_set is not None # Since check_service_module_ref already checks this
module = module_set.get(module_name)
assert module is not None # Since check_service_module_ref already checks this
return module
def check_service_module_ref(
flake: Flake,
module_ref: InventoryInstanceModuleType,
) -> tuple[str, str]:
"""
Checks if the module reference is valid
:param module_ref: The module reference to check
:raises ClanError: If the module_ref is invalid or missing required fields
"""
avilable_modules = list_service_modules(flake)
input_ref = module_ref.get("input", None)
if input_ref is None:
msg = "Setting module_ref.input is currently required"
raise ClanError(msg)
module_set = avilable_modules.get("modules", {}).get(input_ref)
if module_set is None:
msg = f"module set for input '{input_ref}' not found"
msg += f"\nAvilable input_refs: {avilable_modules.get('modules', {}).keys()}"
raise ClanError(msg)
module_name = module_ref.get("name")
assert module_name
module = module_set.get(module_name)
if module is None:
msg = f"module with name '{module_name}' not found"
raise ClanError(msg)
return (input_ref, module_name)
@API.register
def get_service_module_schema(
flake: Flake, module_ref: InventoryInstanceModuleType
) -> dict[str, Any]:
"""
Returns the schema for a service module
:param module_ref: The module reference to get the schema for
:return: Dict of schemas for the service module roles
:raises ClanError: If the module_ref is invalid or missing required fields
"""
input_name, module_name = check_service_module_ref(flake, module_ref)
return flake.select(
f"clanInternals.inventoryClass.moduleSchemas.{input_name}.{module_name}"
) )
return modules
@dataclass @dataclass
class LegacyModuleInfo: class LegacyModuleInfo:

View File

@@ -5,13 +5,13 @@ from dataclasses import dataclass
from clan_cli.machines.hardware import HardwareConfig from clan_cli.machines.hardware import HardwareConfig
from clan_lib.api import API from clan_lib.api import API
from clan_lib.api.disk import MachineDiskMatter
from clan_lib.api.modules import parse_frontmatter
from clan_lib.dirs import specific_machine_dir from clan_lib.dirs import specific_machine_dir
from clan_lib.flake import Flake from clan_lib.flake import Flake
from clan_lib.machines.actions import get_machine, list_machines from clan_lib.machines.actions import get_machine, list_machines
from clan_lib.machines.machines import Machine from clan_lib.machines.machines import Machine
from clan_lib.nix_models.clan import InventoryMachine from clan_lib.nix_models.clan import InventoryMachine
from clan_lib.services.modules import parse_frontmatter
from clan_lib.templates.disk import MachineDiskMatter
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@@ -1,115 +0,0 @@
from typing import TypedDict
from clan_lib.api import API
from clan_lib.errors import ClanError
from clan_lib.flake.flake import Flake
from clan_lib.nix_models.clan import (
InventoryInstanceModule,
InventoryInstanceRolesType,
InventoryInstancesType,
InventoryMachinesType,
)
from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import set_value_by_path
from clan_lib.services.modules import (
get_service_module,
)
# TODO: move imports out of cli/__init__.py causing import cycles
# from clan_lib.machines.actions import list_machines
@API.register
def list_service_instances(flake: Flake) -> InventoryInstancesType:
"""
Returns all currently present service instances including their full configuration
"""
inventory_store = InventoryStore(flake)
inventory = inventory_store.read()
instances = inventory.get("instances", {})
return instances
def collect_tags(machines: InventoryMachinesType) -> set[str]:
res = set()
for _, machine in machines.items():
res |= set(machine.get("tags", []))
return res
# Removed 'module' ref - Needs to be passed seperately
class InstanceConfig(TypedDict):
roles: InventoryInstanceRolesType
@API.register
def create_service_instance(
flake: Flake,
module_ref: InventoryInstanceModule,
instance_name: str,
instance_config: InstanceConfig,
) -> None:
module = get_service_module(flake, module_ref)
inventory_store = InventoryStore(flake)
inventory = inventory_store.read()
instances = inventory.get("instances", {})
if instance_name in instances:
msg = f"service instance '{instance_name}' already exists."
raise ClanError(msg)
target_roles = instance_config.get("roles")
if not target_roles:
msg = "Creating a service instance requires adding roles"
raise ClanError(msg)
available_roles = set(module.get("roles", {}).keys())
unavailable_roles = list(filter(lambda r: r not in available_roles, target_roles))
if unavailable_roles:
msg = f"Unknown roles: {unavailable_roles}. Use one of {available_roles}"
raise ClanError(msg)
role_configs = instance_config.get("roles")
if not role_configs:
return
## Validate machine references
all_machines = inventory.get("machines", {})
available_machine_refs = set(all_machines.keys())
available_tag_refs = collect_tags(all_machines)
for role_name, role_members in role_configs.items():
machine_refs = role_members.get("machines")
msg = f"Role: '{role_name}' - "
if machine_refs:
unavailable_machines = list(
filter(lambda m: m not in available_machine_refs, machine_refs)
)
if unavailable_machines:
msg += f"Unknown machine reference: {unavailable_machines}. Use one of {available_machine_refs}"
raise ClanError(msg)
tag_refs = role_members.get("tags")
if tag_refs:
unavailable_tags = list(
filter(lambda m: m not in available_tag_refs, tag_refs)
)
if unavailable_tags:
msg += (
f"Unknown tags: {unavailable_tags}. Use one of {available_tag_refs}"
)
raise ClanError(msg)
# TODO:
# Validate instance_config roles settings against role schema
set_value_by_path(inventory, f"instances.{instance_name}", instance_config)
set_value_by_path(inventory, f"instances.{instance_name}.module", module_ref)
inventory_store.write(
inventory, message=f"services: instance '{instance_name}' init"
)

View File

@@ -1,4 +1,3 @@
import shutil
from pathlib import Path from pathlib import Path
from clan_lib.flake import Flake from clan_lib.flake import Flake
@@ -36,7 +35,7 @@ def copy_from_nixstore(src: Path, dest: Path) -> None:
Uses `cp -r` to recursively copy the directory. Uses `cp -r` to recursively copy the directory.
Ensures the destination directory is writable by the user. Ensures the destination directory is writable by the user.
""" """
shutil.copytree(src, dest, dirs_exist_ok=True, symlinks=True) run(["cp", "-r", str(src / "."), str(dest)]) # Copy contents of src to dest
run( run(
["chmod", "-R", "u+w", str(dest)] ["chmod", "-R", "u+w", str(dest)]
) # Ensure the destination is writable by the user ) # Ensure the destination is writable by the user

View File

@@ -16,6 +16,8 @@ from clan_cli.secrets.sops import maybe_get_admin_public_keys
from clan_cli.secrets.users import add_user from clan_cli.secrets.users import add_user
from clan_cli.vars.generate import get_generators, run_generators from clan_cli.vars.generate import get_generators, run_generators
from clan_lib.api.disk import hw_main_disk_options, set_machine_disk_schema
from clan_lib.api.modules import list_modules
from clan_lib.cmd import RunOpts, run from clan_lib.cmd import RunOpts, run
from clan_lib.dirs import specific_machine_dir from clan_lib.dirs import specific_machine_dir
from clan_lib.errors import ClanError from clan_lib.errors import ClanError
@@ -31,9 +33,7 @@ from clan_lib.nix_models.clan import (
from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy from clan_lib.nix_models.clan import InventoryMachineDeploy as MachineDeploy
from clan_lib.persist.inventory_store import InventoryStore from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import set_value_by_path from clan_lib.persist.util import set_value_by_path
from clan_lib.services.modules import list_service_modules
from clan_lib.ssh.remote import Remote, check_machine_ssh_login from clan_lib.ssh.remote import Remote, check_machine_ssh_login
from clan_lib.templates.disk import hw_main_disk_options, set_machine_disk_schema
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -206,9 +206,9 @@ def test_clan_create_api(
store = InventoryStore(clan_dir_flake) store = InventoryStore(clan_dir_flake)
inventory = store.read() inventory = store.read()
modules = list_service_modules(clan_dir_flake) modules = list_modules(str(clan_dir_flake.path))
assert ( assert (
modules["modules"]["clan-core"]["admin"]["manifest"]["name"] modules["modulesPerSource"]["clan-core"]["admin"]["manifest"]["name"]
== "clan-core/admin" == "clan-core/admin"
) )

View File

@@ -4,7 +4,7 @@ import sys
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from clan_cli.cli import create_parser from clan_cli import create_parser
hidden_subcommands = ["machine", "b", "f", "m", "se", "st", "va"] hidden_subcommands = ["machine", "b", "f", "m", "se", "st", "va"]

View File

@@ -41,7 +41,7 @@ TOP_LEVEL_RESOURCES = {
"secret", # sops & key operations "secret", # sops & key operations
"log", # log operations "log", # log operations
"generator", # vars generators operations "generator", # vars generators operations
"service", # clan.service management "module", # module (clan.service) management
"system", # system operations "system", # system operations
} }

View File

@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
name = "clan" name = "clan"
description = "clan cli tool" description = "clan cli tool"
dynamic = ["version"] dynamic = ["version"]
scripts = { clan = "clan_cli.cli:main" } scripts = { clan = "clan_cli:main" }
license = { text = "MIT" } license = { text = "MIT" }
[project.urls] [project.urls]

View File

@@ -2,11 +2,6 @@
# Ensure this is unique among all clans you want to use. # Ensure this is unique among all clans you want to use.
meta.name = "__CHANGE_ME__"; meta.name = "__CHANGE_ME__";
inventory.machines = {
# Define machines here.
# jon = { };
};
# Docs: See https://docs.clan.lol/reference/clanServices # Docs: See https://docs.clan.lol/reference/clanServices
inventory.instances = { inventory.instances = {

View File

@@ -0,0 +1,61 @@
{
lib,
clan-core,
config,
...
}:
let
suffix = config.clan.core.vars.generators.disk-id.files.diskId.value;
in
{
imports = [
clan-core.clanModules.disk-id
];
# DO NOT EDIT THIS FILE AFTER INSTALLATION of a machine
# Otherwise your system might not boot because of missing partitions / filesystems
boot.loader.grub.efiSupport = lib.mkDefault true;
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
disko.devices = {
disk = {
"main" = {
# suffix is to prevent disk name collisions
name = "main-" + suffix;
type = "disk";
# Set the following in flake.nix for each maschine:
# device = <uuid>;
content = {
type = "gpt";
partitions = {
"boot" = {
size = "1M";
type = "EF02"; # for grub MBR
priority = 1;
};
"ESP" = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "nofail" ];
};
};
"root" = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
# format = "btrfs";
# format = "bcachefs";
mountpoint = "/";
};
};
};
};
};
};
};
}

View File

@@ -0,0 +1,27 @@
{
clan-core,
# Optional, if you want to access other flakes:
# self,
...
}:
{
imports = [
clan-core.clanModules.user-password
];
# Locale service discovery and mDNS
services.avahi.enable = true;
# generate a random password for our user below
# can be read using `clan secrets get <machine-name>-user-password` command
clan.user-password.user = "user";
users.users.user = {
isNormalUser = true;
extraGroups = [
"wheel"
"networkmanager"
"video"
"input"
];
};
}

View File

@@ -2,11 +2,6 @@
# Ensure this is unique among all clans you want to use. # Ensure this is unique among all clans you want to use.
meta.name = "__CHANGE_ME__"; meta.name = "__CHANGE_ME__";
inventory.machines = {
# Define machines here.
# jon = { };
};
# Docs: See https://docs.clan.lol/reference/clanServices # Docs: See https://docs.clan.lol/reference/clanServices
inventory.instances = { inventory.instances = {

View File

@@ -0,0 +1,38 @@
{ config, ... }:
let
username = config.networking.hostName;
in
{
imports = [ ./hardware-configuration.nix ];
# Locale service discovery and mDNS
services.avahi.enable = true;
services.xserver.enable = true;
services.xserver.desktopManager.gnome.enable = true;
services.xserver.displayManager.gdm.enable = true;
# Disable the default gnome apps to speed up deployment
services.gnome.core-utilities.enable = false;
# Enable automatic login for the user.
services.displayManager.autoLogin = {
enable = true;
user = username;
};
users.users.${username} = {
initialPassword = username;
isNormalUser = true;
extraGroups = [
"wheel"
"networkmanager"
"video"
"audio"
"input"
"dialout"
"disk"
];
uid = 1000;
openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys;
};
}

View File

@@ -0,0 +1,39 @@
{ config, ... }:
let
username = config.networking.hostName;
in
{
imports = [ ./hardware-configuration.nix ];
# Locale service discovery and mDNS
services.avahi.enable = true;
services.xserver.enable = true;
services.xserver.desktopManager.gnome.enable = true;
services.xserver.displayManager.gdm.enable = true;
# Disable the default gnome apps to speed up deployment
services.gnome.core-utilities.enable = false;
# Enable automatic login for the user.
services.displayManager.autoLogin = {
enable = true;
user = username;
};
users.users.${username} = {
initialPassword = username;
isNormalUser = true;
extraGroups = [
"wheel"
"networkmanager"
"video"
"audio"
"input"
"dialout"
"disk"
];
uid = 1000;
openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys;
};
}

View File

@@ -0,0 +1,51 @@
{ lib, clan-core, ... }:
let
suffix = config.clan.core.vars.generators.disk-id.files.diskId.value;
in
{
imports = [
clan-core.clanModules.disk-id
];
boot.loader.grub.efiSupport = lib.mkDefault true;
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
disko.devices = {
disk = {
"main" = {
# suffix is to prevent disk name collisions
name = "main-" + suffix;
type = "disk";
# Set the following in flake.nix for each maschine:
# device = <uuid>;
content = {
type = "gpt";
partitions = {
"boot" = {
size = "1M";
type = "EF02"; # for grub MBR
priority = 1;
};
"ESP" = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
"root" = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
};
}