Compare commits
38 Commits
ke-facts-c
...
ke-disko-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc915387d9 | ||
|
|
a890b586b4 | ||
|
|
81da1e8b1d | ||
|
|
74c5f71fd7 | ||
|
|
c3f26b3728 | ||
|
|
2b3c5b0524 | ||
|
|
6918a6f1e3 | ||
|
|
71f8948a17 | ||
|
|
44d2a6485e | ||
|
|
2e82109688 | ||
|
|
c3a2891929 | ||
|
|
2e2156bc86 | ||
|
|
802ef94798 | ||
|
|
2ce4f8bf37 | ||
|
|
24d82776e7 | ||
|
|
ff41903e47 | ||
|
|
31e3a37da4 | ||
|
|
690072e29e | ||
|
|
d39fc575c6 | ||
|
|
6019efe40a | ||
|
|
02d35395a8 | ||
|
|
e90ea62ab7 | ||
|
|
5bf1f06244 | ||
|
|
1403f47b0d | ||
|
|
743aa712f5 | ||
|
|
9800e50ce1 | ||
|
|
ef0b61ccd6 | ||
|
|
0d5dbb0fc5 | ||
|
|
9a647907e9 | ||
|
|
5469ab0ae0 | ||
|
|
504533cf5a | ||
|
|
e9f21a01e9 | ||
|
|
f81089930e | ||
|
|
84a7dc7697 | ||
|
|
0d851580e1 | ||
|
|
be384420d5 | ||
|
|
5ebf5b6189 | ||
|
|
d7b476a311 |
@@ -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.
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"])
|
||||||
|
|||||||
@@ -10,22 +10,63 @@ 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}
|
||||||
{
|
{
|
||||||
|
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||||
|
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||||
|
inputs.flake-parts.follows = "clan-core/flake-parts";
|
||||||
|
inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs@{ flake-parts, ... }:
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
imports = [ inputs.clan-core.flakeModules.default ];
|
||||||
|
clan = {
|
||||||
inventory.machines = {
|
inventory.machines = {
|
||||||
# Define a machine
|
# Define a machine
|
||||||
jon = { };
|
jon = { };
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# Additional NixOS configuration can be added here.
|
systems = [
|
||||||
# machines/jon/configuration.nix will be automatically imported.
|
"x86_64-linux"
|
||||||
# See: https://docs.clan.lol/guides/more-machines/#automatic-registration
|
"aarch64-linux"
|
||||||
machines = {
|
"x86_64-darwin"
|
||||||
# jon = { config, ... }: {
|
"aarch64-darwin"
|
||||||
# environment.systemPackages = [ pkgs.asciinema ];
|
];
|
||||||
# };
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "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
|
||||||
|
;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -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`.
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs@{ flake-parts, ... }:
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
imports = [ inputs.clan-core.flakeModules.default ];
|
||||||
|
# Sometimes this attribute set is defined in clan.nix
|
||||||
|
clan = {
|
||||||
|
inventory.machines = {
|
||||||
|
jon = {
|
||||||
|
targetHost = "root@jon";
|
||||||
|
};
|
||||||
|
sara = {
|
||||||
|
targetHost = "root@jon";
|
||||||
|
};
|
||||||
|
};
|
||||||
inventory.instances = {
|
inventory.instances = {
|
||||||
zerotier = { # (1)
|
zerotier = { # (1)
|
||||||
# Replace with the name (string) of your machine that you will use as zerotier-controller
|
# Defines 'jon' as the controller
|
||||||
# See: https://docs.zerotier.com/controller/
|
roles.controller.machines.jon = {};
|
||||||
# Deploy this machine first to create the network secrets
|
# Defines all machines as networking peer.
|
||||||
roles.controller.machines."jon" = { }; # (2)
|
# The 'all' tag is a clan builtin.
|
||||||
# Peers of the network
|
roles.peer.tags.all = {};
|
||||||
# this line means 'all' clan machines will be 'peers'
|
|
||||||
roles.peer.tags.all = { }; # (3)
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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"}
|
||||||
{
|
{
|
||||||
|
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||||
|
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||||
|
inputs.flake-parts.follows = "clan-core/flake-parts";
|
||||||
|
inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs@{ flake-parts, ... }:
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
imports = [ inputs.clan-core.flakeModules.default ];
|
||||||
|
# Sometimes this attribute set is defined in clan.nix
|
||||||
|
clan = {
|
||||||
inventory.machines = {
|
inventory.machines = {
|
||||||
jon = { };
|
jon = {
|
||||||
sara = { };
|
targetHost = "root@jon";
|
||||||
|
};
|
||||||
|
sara = {
|
||||||
|
targetHost = "root@jon";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
inventory.instances = {
|
inventory.instances = {
|
||||||
|
zerotier = {
|
||||||
|
roles.controller.machines.jon = {};
|
||||||
|
roles.peer.tags.all = {};
|
||||||
|
};
|
||||||
admin = { # (1)
|
admin = { # (1)
|
||||||
roles.default.tags.all = { };
|
roles.default.tags.all = { };
|
||||||
roles.default.settings = {
|
roles.default.settings = {
|
||||||
allowedKeys = {
|
allowedKeys = {
|
||||||
"my-user" = "ssh-ed25519 AAAAC3N..."; # (2)
|
"my-user" = "ssh-ed25519 AAAAC3N..."; # elided
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
# ...
|
state-version = { # (2)
|
||||||
# elided
|
roles.default.tags.all = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
systems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"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.
|
||||||
@@ -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
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -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,17 +10,165 @@ 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.
|
||||||
|
|
||||||
|
=== "**Cloud VMs**"
|
||||||
|
|
||||||
|
- [x] Any cloud machine if it is reachable via SSH and supports `kexec`.
|
||||||
|
|
||||||
|
!!! Warning "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 pannel)
|
||||||
|
|
||||||
|
## Setting `targetHost`
|
||||||
|
|
||||||
|
=== "flake.nix (flake-parts)"
|
||||||
|
|
||||||
|
```{.nix hl_lines="22"}
|
||||||
|
{
|
||||||
|
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||||
|
inputs.nixpkgs.follows = "clan-core/nixpkgs";
|
||||||
|
inputs.flake-parts.follows = "clan-core/flake-parts";
|
||||||
|
inputs.flake-parts.inputs.nixpkgs-lib.follows = "clan-core/nixpkgs";
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
inputs@{ flake-parts, ... }:
|
||||||
|
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||||
|
systems = [
|
||||||
|
"x86_64-linux"
|
||||||
|
"aarch64-linux"
|
||||||
|
"x86_64-darwin"
|
||||||
|
"aarch64-darwin"
|
||||||
|
];
|
||||||
|
imports = [ inputs.clan-core.flakeModules.default ];
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Identify the Target Disk
|
||||||
|
|
||||||
|
On the setup computer, SSH into the target:
|
||||||
|
|
||||||
|
```bash title="setup computer"
|
||||||
|
ssh root@<IP> lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `<IP>` with the machine's IP or hostname if mDNS (i.e. Avahi) is available.
|
||||||
|
|
||||||
|
Which should show something like:
|
||||||
|
|
||||||
|
```{.shellSession hl_lines="6" .no-copy}
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for the top-level disk device (e.g., nvme0n1 or sda) and copy its `ID-LINK`. Avoid using partition IDs like `nvme0n1p1`.
|
||||||
|
|
||||||
|
In this example we would copy `nvme-eui.e8238fa6bf530001001b448b4aec2929`
|
||||||
|
|
||||||
|
!!! 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).
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
**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 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.
|
The installer shows it's deployment relevant information in two formats, a text form, as well as a QR code.
|
||||||
@@ -84,131 +232,34 @@ Sample boot screen shows:
|
|||||||
!!! tip
|
!!! tip
|
||||||
Use [KDE Connect](https://apps.kde.org/de/kdeconnect/) for easyily sharing QR codes from phone to desktop
|
Use [KDE Connect](https://apps.kde.org/de/kdeconnect/) for easyily sharing QR codes from phone to desktop
|
||||||
|
|
||||||
## Cloud VMs
|
=== "**Cloud VM**"
|
||||||
|
|
||||||
!!! note "skip this if using a physical machine"
|
Just run the command **Option B: Cloud VM** below
|
||||||
|
|
||||||
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`
|
|
||||||
|
|
||||||
In your nix files set the targetHost (reachable ip) that you retrieved in the previous step.
|
|
||||||
|
|
||||||
```{.nix title="clan.nix" hl_lines="9"}
|
|
||||||
{
|
|
||||||
# Ensure this is unique among all clans you want to use.
|
|
||||||
meta.name = "my-clan";
|
|
||||||
|
|
||||||
inventory.machines = {
|
|
||||||
# Define machines here.
|
|
||||||
# The machine name will be used as the hostname.
|
|
||||||
jon = {
|
|
||||||
deploy.targetHost = "root@192.168.192.4"; # (1)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
# ...
|
|
||||||
# elided
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
!!! warning
|
|
||||||
The use of `root@` in the target address implies SSH access as the `root` user.
|
|
||||||
Ensure that the root login is secured and only used when necessary.
|
|
||||||
|
|
||||||
See also [how to set TargetHost](../target-host.md) for other methods.
|
|
||||||
|
|
||||||
## Retrieve the hardware report
|
|
||||||
|
|
||||||
By default clan uses [nixos-facter](https://github.com/nix-community/nixos-facter) which captures detailed information about the machine or virtual environment.
|
|
||||||
|
|
||||||
To generate the hardware-report (`facter.json`) run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
clan machines update-hardware-config <machineName>
|
|
||||||
```
|
|
||||||
|
|
||||||
Example output:
|
|
||||||
|
|
||||||
```shell-session
|
|
||||||
$ clan machines update-hardware-config jon
|
|
||||||
[jon] $ nixos-facter
|
|
||||||
Successfully generated: ./machines/jon/facter.json
|
|
||||||
```
|
|
||||||
|
|
||||||
See [update-hardware-config cli reference](../../reference/cli/machines.md#machines-update-hardware-config) for further configuration possibilities if needed.
|
|
||||||
|
|
||||||
## Configure your disk schema
|
|
||||||
|
|
||||||
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
|
|
||||||
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).
|
|
||||||
|
|
||||||
## Deploy the machine
|
|
||||||
|
|
||||||
**Finally deployment time!** Use one of the following commands to build and deploy the image via SSH onto your machine.
|
|
||||||
|
|
||||||
### 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
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
Don’t worry if your output looks different — Clan templates evolve over time.
|
Don’t 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 :: {} }}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from .cli import main
|
from . import main
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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:
|
||||||
@@ -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__)
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
)
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|
||||||
|
|||||||
61
templates/clan/default/modules/disko.nix
Normal file
61
templates/clan/default/modules/disko.nix
Normal 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 = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
27
templates/clan/default/modules/shared.nix
Normal file
27
templates/clan/default/modules/shared.nix
Normal 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"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 = {
|
||||||
|
|
||||||
|
|||||||
38
templates/clan/flake-parts/machines/jon/configuration.nix
Normal file
38
templates/clan/flake-parts/machines/jon/configuration.nix
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
39
templates/clan/flake-parts/machines/sara/configuration.nix
Normal file
39
templates/clan/flake-parts/machines/sara/configuration.nix
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
51
templates/clan/flake-parts/modules/disko.nix
Normal file
51
templates/clan/flake-parts/modules/disko.nix
Normal 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 = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user