Compare commits
No commits in common. "main" and "flake-update-2024-06-24" have entirely different histories.
main
...
flake-upda
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -3,7 +3,6 @@
|
|||
out.log
|
||||
.coverage.*
|
||||
**/qubeclan
|
||||
pkgs/repro-hook
|
||||
**/testdir
|
||||
democlan
|
||||
example_clan
|
||||
|
@ -36,4 +35,4 @@ repo
|
|||
# node
|
||||
node_modules
|
||||
dist
|
||||
.webui
|
||||
.webui
|
12
README.md
12
README.md
|
@ -1,6 +1,6 @@
|
|||
# Clan core repository
|
||||
# Clan Core Repository
|
||||
|
||||
Welcome to the Clan core repository, the heart of the [clan.lol](https://clan.lol/) project! This monorepo is the foundation of Clan, a revolutionary open-source project aimed at restoring fun, freedom, and functionality to computing. Here, you'll find all the essential packages, NixOS modules, CLI tools, and tests needed to contribute to and work with the Clan project. Clan leverages the Nix system to ensure reliability, security, and seamless management of digital environments, putting the power back into the hands of users.
|
||||
Welcome to the Clan Core Repository, the heart of the [clan.lol](https://clan.lol/) project! This monorepo is the foundation of Clan, a revolutionary open-source project aimed at restoring fun, freedom, and functionality to computing. Here, you'll find all the essential packages, NixOS modules, CLI tools, and tests needed to contribute to and work with the Clan project. Clan leverages the Nix system to ensure reliability, security, and seamless management of digital environments, putting the power back into the hands of users.
|
||||
|
||||
## Why Clan?
|
||||
|
||||
|
@ -14,13 +14,13 @@ Our mission is simple: to democratize computing by providing tools that empower
|
|||
- **Robust Backup Management:** Long-term, self-hosted data preservation.
|
||||
- **Intuitive Secret Management:** Simplified encryption and password management processes.
|
||||
|
||||
## Getting started with Clan
|
||||
## Getting Started with Clan
|
||||
|
||||
If you're new to Clan and eager to dive in, start with our quickstart guide and explore the core functionalities that Clan offers:
|
||||
|
||||
- **Quickstart Guide**: Check out [getting started](https://docs.clan.lol/#starting-with-a-new-clan-project)<!-- [docs/site/index.md](docs/site/index.md) --> to get up and running with Clan in no time.
|
||||
|
||||
### Managing secrets
|
||||
### Managing Secrets
|
||||
|
||||
In the Clan ecosystem, security is paramount. Learn how to handle secrets effectively:
|
||||
|
||||
|
@ -32,11 +32,11 @@ The Clan project thrives on community contributions. We welcome everyone to cont
|
|||
|
||||
- **Contribution Guidelines**: Make a meaningful impact by following the steps in [contributing](https://docs.clan.lol/contributing/contributing/)<!-- [contributing.md](docs/CONTRIBUTING.md) -->.
|
||||
|
||||
## Join the revolution
|
||||
## Join the Revolution
|
||||
|
||||
Clan is more than a tool; it's a movement towards a better digital future. By contributing to the Clan project, you're part of changing technology for the better, together.
|
||||
|
||||
### Community and support
|
||||
### Community and Support
|
||||
|
||||
Connect with us and the Clan community for support and discussion:
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
{ lib, modulesPath, ... }:
|
||||
{
|
||||
imports = [
|
||||
"${self}/nixosModules/disk-layouts"
|
||||
self.clanModules.disk-layouts
|
||||
(modulesPath + "/testing/test-instrumentation.nix") # we need these 2 modules always to be able to run the tests
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
];
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
Statically configure borgbackup with sane defaults.
|
||||
---
|
||||
description = "Statically configure borgbackup with sane defaults."
|
||||
---
|
||||
This module implements the `borgbackup` backend and implements sane defaults
|
||||
This module implements the `borgbackup` backend and implements sane defaults
|
||||
for backup management through `borgbackup` for members of the clan.
|
||||
|
||||
Configure target machines where the backups should be sent to through `targets`.
|
||||
|
||||
Configure machines that should be backuped either through `includeMachines`
|
||||
which will exclusively add the included machines to be backuped, or through
|
||||
which will exclusively add the included machines to be backuped, or through
|
||||
`excludeMachines`, which will add every machine except the excluded machine to the backup.
|
||||
|
|
|
@ -3,7 +3,7 @@ let
|
|||
clanDir = config.clan.core.clanDir;
|
||||
machineDir = clanDir + "/machines/";
|
||||
in
|
||||
lib.warn "This module is deprecated use the service via the inventory interface instead." {
|
||||
{
|
||||
imports = [ ../borgbackup ];
|
||||
|
||||
options.clan.borgbackup-static = {
|
||||
|
|
|
@ -1,13 +1,2 @@
|
|||
---
|
||||
description = "Efficient, deduplicating backup program with optional compression and secure encryption."
|
||||
categories = ["backup"]
|
||||
---
|
||||
BorgBackup (short: Borg) gives you:
|
||||
|
||||
- Space efficient storage of backups.
|
||||
- Secure, authenticated encryption.
|
||||
- Compression: lz4, zstd, zlib, lzma or none.
|
||||
- Mountable backups with FUSE.
|
||||
- Easy installation on multiple platforms: Linux, macOS, BSD, ...
|
||||
- Free software (BSD license).
|
||||
- Backed by a large and active open source community.
|
||||
Efficient, deduplicating backup program with optional compression and secure encryption.
|
||||
---
|
|
@ -28,51 +28,7 @@ let
|
|||
fi
|
||||
'';
|
||||
in
|
||||
# Each .nix file in the roles directory is a role
|
||||
# TODO: Helper function to set available roles within module meta.
|
||||
# roles =
|
||||
# if builtins.pathExists ./roles then
|
||||
# lib.pipe ./roles [
|
||||
# builtins.readDir
|
||||
# (lib.filterAttrs (_n: v: v == "regular"))
|
||||
# lib.attrNames
|
||||
# (map (fileName: lib.removeSuffix ".nix" fileName))
|
||||
# ]
|
||||
# else
|
||||
# null;
|
||||
# TODO: make this an interface of every module
|
||||
# Maybe load from readme.md
|
||||
# metaInfoOption = lib.mkOption {
|
||||
# readOnly = true;
|
||||
# description = ''
|
||||
# Meta is used to retrieve information about this module.
|
||||
# - `availableRoles` is a list of roles that can be assigned via the inventory.
|
||||
# - `category` is used to group services in the clan marketplace.
|
||||
# - `description` is a short description of the service for the clan marketplace.
|
||||
# '';
|
||||
# default = {
|
||||
# description = "Borgbackup is a backup program. Optionally, it supports compression and authenticated encryption.";
|
||||
# availableRoles = roles;
|
||||
# category = "backup";
|
||||
# };
|
||||
# type = lib.types.submodule {
|
||||
# options = {
|
||||
# description = lib.mkOption { type = lib.types.str; };
|
||||
# availableRoles = lib.mkOption { type = lib.types.nullOr (lib.types.listOf lib.types.str); };
|
||||
# category = lib.mkOption {
|
||||
# description = "A category for the service. This is used to group services in the clan ui";
|
||||
# type = lib.types.enum [
|
||||
# "backup"
|
||||
# "network"
|
||||
# ];
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
# };
|
||||
{
|
||||
|
||||
# options.clan.borgbackup.meta = metaInfoOption;
|
||||
|
||||
options.clan.borgbackup.destinations = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
|
@ -120,7 +76,7 @@ in
|
|||
lib.nameValuePair "borgbackup-job-${dest.name}" {
|
||||
# since borgbackup mounts the system read-only, we need to run in a ExecStartPre script, so we can generate additional files.
|
||||
serviceConfig.ExecStartPre = [
|
||||
''+${pkgs.writeShellScript "borgbackup-job-${dest.name}-pre-backup-commands" preBackupScript}''
|
||||
(''+${pkgs.writeShellScript "borgbackup-job-${dest.name}-pre-backup-commands" preBackupScript}'')
|
||||
];
|
||||
}
|
||||
) cfg.destinations;
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
{ config, lib, ... }:
|
||||
let
|
||||
instances = config.clan.inventory.services.borgbackup;
|
||||
# roles = { ${role_name} :: { machines :: [string] } }
|
||||
allServers = lib.foldlAttrs (
|
||||
acc: _instanceName: instanceConfig:
|
||||
acc
|
||||
++ (
|
||||
if builtins.elem machineName instanceConfig.roles.client.machines then
|
||||
instanceConfig.roles.server.machines
|
||||
else
|
||||
[ ]
|
||||
)
|
||||
) [ ] instances;
|
||||
|
||||
inherit (config.clan.core) machineName;
|
||||
in
|
||||
{
|
||||
config.clan.borgbackup.destinations =
|
||||
let
|
||||
|
||||
destinations = builtins.map (serverName: {
|
||||
name = serverName;
|
||||
value = {
|
||||
repo = "borg@${serverName}:/var/lib/borgbackup/${machineName}";
|
||||
};
|
||||
}) allServers;
|
||||
in
|
||||
(builtins.listToAttrs destinations);
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
{ config, lib, ... }:
|
||||
let
|
||||
clanDir = config.clan.core.clanDir;
|
||||
machineDir = clanDir + "/machines/";
|
||||
inherit (config.clan.core) machineName;
|
||||
|
||||
instances = config.clan.inventory.services.borgbackup;
|
||||
|
||||
# roles = { ${role_name} :: { machines :: [string] } }
|
||||
|
||||
allClients = lib.foldlAttrs (
|
||||
acc: _instanceName: instanceConfig:
|
||||
acc
|
||||
++ (
|
||||
if (builtins.elem machineName instanceConfig.roles.server.machines) then
|
||||
instanceConfig.roles.client.machines
|
||||
else
|
||||
[ ]
|
||||
)
|
||||
) [ ] instances;
|
||||
in
|
||||
{
|
||||
config.services.borgbackup.repos =
|
||||
let
|
||||
borgbackupIpMachinePath = machines: machineDir + machines + "/facts/borgbackup.ssh.pub";
|
||||
machinesMaybeKey = builtins.map (
|
||||
machine:
|
||||
let
|
||||
fullPath = borgbackupIpMachinePath machine;
|
||||
in
|
||||
if builtins.pathExists fullPath then machine else null
|
||||
) allClients;
|
||||
|
||||
machinesWithKey = lib.filter (x: x != null) machinesMaybeKey;
|
||||
|
||||
hosts = builtins.map (machine: {
|
||||
name = machine;
|
||||
value = {
|
||||
path = "/var/lib/borgbackup/${machine}";
|
||||
authorizedKeys = [ (builtins.readFile (borgbackupIpMachinePath machine)) ];
|
||||
};
|
||||
}) machinesWithKey;
|
||||
in
|
||||
if (builtins.listToAttrs hosts) != [ ] then builtins.listToAttrs hosts else { };
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
description = "Email-based instant messaging for Desktop."
|
||||
Email-based instant messaging for Desktop.
|
||||
---
|
||||
|
||||
!!! warning "Under construction"
|
||||
|
|
2
clanModules/disk-layouts/README.md
Normal file
2
clanModules/disk-layouts/README.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
Automatically format a disk drive on clan installation
|
||||
---
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "A modern IRC server"
|
||||
A modern IRC server
|
||||
---
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
{ ... }:
|
||||
{
|
||||
flake.clanModules = {
|
||||
disk-layouts = {
|
||||
imports = [ ./disk-layouts ];
|
||||
};
|
||||
borgbackup = ./borgbackup;
|
||||
borgbackup-static = ./borgbackup-static;
|
||||
deltachat = ./deltachat;
|
||||
ergochat = ./ergochat;
|
||||
localbackup = ./localbackup;
|
||||
localsend = ./localsend;
|
||||
single-disk = ./single-disk;
|
||||
matrix-synapse = ./matrix-synapse;
|
||||
moonlight = ./moonlight;
|
||||
packages = ./packages;
|
||||
postgresql = ./postgresql;
|
||||
root-password = ./root-password;
|
||||
sshd = ./sshd;
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "Automatically backups current machine to local directory."
|
||||
Automatically backups current machine to local directory.
|
||||
---
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "Securely sharing files and messages over a local network without internet connectivity."
|
||||
Securely sharing files and messages over a local network without internet connectivity.
|
||||
---
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "A federated messaging server with end-to-end encryption."
|
||||
A federated messaging server with end-to-end encryption.
|
||||
---
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "A desktop streaming client optimized for remote gaming and synchronized movie viewing."
|
||||
A desktop streaming client optimized for remote gaming and synchronized movie viewing.
|
||||
---
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
description = "Define package sets from nixpkgs and install them on one or more machines"
|
||||
categories = ["packages"]
|
||||
---
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
options.clan.packages = {
|
||||
packages = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "The packages to install on the machine";
|
||||
};
|
||||
};
|
||||
config = {
|
||||
environment.systemPackages = map (
|
||||
pName: lib.getAttrFromPath (lib.splitString "." pName) pkgs
|
||||
) config.clan.packages.packages;
|
||||
};
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{ }
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "A free and open-source relational database management system (RDBMS) emphasizing extensibility and SQL compliance."
|
||||
A free and open-source relational database management system (RDBMS) emphasizing extensibility and SQL compliance.
|
||||
---
|
||||
|
|
|
@ -91,7 +91,6 @@ in
|
|||
options.clan.postgresql = {
|
||||
# we are reimplemeting ensureDatabase and ensureUser options here to allow to create databases with options
|
||||
databases = lib.mkOption {
|
||||
description = "Databases to create";
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
|
@ -115,7 +114,6 @@ in
|
|||
description = "Create the database if it does not exist.";
|
||||
};
|
||||
create.options = lib.mkOption {
|
||||
description = "Options to pass to the CREATE DATABASE command.";
|
||||
type = lib.types.lazyAttrsOf lib.types.str;
|
||||
default = { };
|
||||
example = {
|
||||
|
@ -137,14 +135,12 @@ in
|
|||
);
|
||||
};
|
||||
users = lib.mkOption {
|
||||
description = "Users to create";
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options.name = lib.mkOption {
|
||||
description = "User name";
|
||||
type = lib.types.str;
|
||||
default = name;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
description = "Automatically generates and configures a password for the root user."
|
||||
Automatically generates and configures a password for the root user.
|
||||
---
|
||||
|
||||
After the system was installed/deployed the following command can be used to display the root-password:
|
||||
|
|
|
@ -13,8 +13,8 @@
|
|||
mkpasswd
|
||||
];
|
||||
generator.script = ''
|
||||
xkcdpass --numwords 3 --delimiter - --count 1 | tr -d "\n" > $secrets/password
|
||||
cat $secrets/password | mkpasswd -s -m sha-512 | tr -d "\n" > $secrets/password-hash
|
||||
xkcdpass --numwords 3 --delimiter - --count 1 > $secrets/password
|
||||
cat $secrets/password | mkpasswd -s -m sha-512 > $secrets/password-hash
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
---
|
||||
description = "Configures partitioning of the main disk"
|
||||
categories = ["disk-layout"]
|
||||
---
|
||||
# Primary Disk Layout
|
||||
|
||||
A module for the "disk-layout" category MUST be choosen.
|
||||
|
||||
There is exactly one slot for this type of module in the UI, if you don't fill the slot, your machine cannot boot
|
||||
|
||||
This module is a good choice for most machines. In the future clan will offer a broader choice of disk-layouts
|
||||
|
||||
The UI will ask for the options of this module:
|
||||
|
||||
`device: "/dev/null"`
|
||||
|
||||
# Usage example
|
||||
|
||||
`inventory.json`
|
||||
```json
|
||||
"services": {
|
||||
"single-disk": {
|
||||
"default": {
|
||||
"meta": {
|
||||
"name": "single-disk"
|
||||
},
|
||||
"roles": {
|
||||
"default": {
|
||||
"machines": ["jon"]
|
||||
}
|
||||
},
|
||||
"machines": {
|
||||
"jon": {
|
||||
"config": {
|
||||
"device": "/dev/null"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
|
@ -1,52 +0,0 @@
|
|||
{ lib, config, ... }:
|
||||
{
|
||||
options.clan.single-disk = {
|
||||
device = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "The primary disk device to install the system on";
|
||||
# Question: should we set a default here?
|
||||
# default = "/dev/null";
|
||||
};
|
||||
};
|
||||
config = {
|
||||
boot.loader.grub.efiSupport = lib.mkDefault true;
|
||||
boot.loader.grub.efiInstallAsRemovable = lib.mkDefault true;
|
||||
disko.devices = {
|
||||
disk = {
|
||||
main = {
|
||||
type = "disk";
|
||||
# This is set through the UI
|
||||
device = config.clan.single-disk.device;
|
||||
|
||||
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 = "/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{ }
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "Enables secure remote access to the machine over ssh"
|
||||
Enables secure remote access to the machine over ssh
|
||||
---
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
{
|
||||
services.openssh.enable = true;
|
||||
services.openssh.settings.PasswordAuthentication = false;
|
||||
# We might want to remove this once, openssh is fixed everywhere:
|
||||
# Workaround for CVE-2024-6387
|
||||
# https://github.com/NixOS/nixpkgs/pull/323753#issuecomment-2199762128
|
||||
services.openssh.settings.LoginGraceTime = 0;
|
||||
|
||||
services.openssh.hostKeys = [
|
||||
{
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "Statically configure the host names of machines based on their respective zerotier-ip."
|
||||
Statically configure the host names of machines based on their respective zerotier-ip.
|
||||
---
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "A desktop streaming server optimized for remote gaming and synchronized movie viewing."
|
||||
A desktop streaming server optimized for remote gaming and synchronized movie viewing.
|
||||
---
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "Statically configure syncthing peers through clan"
|
||||
Statically configure syncthing peers through clan
|
||||
---
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
description = "A secure, file synchronization app for devices over networks, offering a private alternative to cloud services."
|
||||
A secure, file synchronization app for devices over networks, offering a private alternative to cloud services.
|
||||
---
|
||||
## Usage
|
||||
|
||||
|
|
|
@ -7,10 +7,6 @@
|
|||
{
|
||||
options.clan.syncthing = {
|
||||
id = lib.mkOption {
|
||||
description = ''
|
||||
The ID of the machine.
|
||||
It is generated automatically by default.
|
||||
'';
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
example = "BABNJY4-G2ICDLF-QQEG7DD-N3OBNGF-BCCOFK6-MV3K7QJ-2WUZHXS-7DTW4AS";
|
||||
default = config.clan.core.facts.services.syncthing.public."syncthing.pub".value or null;
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "Modern web IRC client"
|
||||
Modern web IRC client
|
||||
---
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "This module sets the `clan.lol` and `nix-community` cache up as a trusted cache."
|
||||
This module sets the `clan.lol` and `nix-community` cache up as a trusted cache.
|
||||
----
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
description = "Automatically generates and configures a password for the specified user account."
|
||||
Automatically generates and configures a password for the specified user account.
|
||||
---
|
||||
|
||||
If setting the option prompt to true, the user will be prompted to type in their desired password.
|
||||
|
|
|
@ -37,12 +37,12 @@
|
|||
mkpasswd
|
||||
];
|
||||
generator.script = ''
|
||||
if [[ -n ''${prompt_value-} ]]; then
|
||||
echo $prompt_value | tr -d "\n" > $secrets/user-password
|
||||
if [[ -n $prompt_value ]]; then
|
||||
echo $prompt_value > $secrets/user-password
|
||||
else
|
||||
xkcdpass --numwords 3 --delimiter - --count 1 | tr -d "\n" > $secrets/user-password
|
||||
xkcdpass --numwords 3 --delimiter - --count 1 > $secrets/user-password
|
||||
fi
|
||||
cat $secrets/user-password | mkpasswd -s -m sha-512 | tr -d "\n" > $secrets/user-password-hash
|
||||
cat $secrets/user-password | mkpasswd -s -m sha-512 > $secrets/user-password-hash
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "A lightweight desktop manager"
|
||||
A lightweight desktop manager
|
||||
---
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
---
|
||||
description = "Statically configure the `zerotier` peers of a clan network."
|
||||
Statically configure the `zerotier` peers of a clan network.
|
||||
---
|
||||
Statically configure the `zerotier` peers of a clan network.
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
|
@ -60,11 +61,13 @@ in
|
|||
lib.mkIf (config.clan.networking.zerotier.controller.enable) {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "zerotierone.service" ];
|
||||
path = [ config.clan.core.clanPkgs.zerotierone ];
|
||||
path = [ pkgs.zerotierone ];
|
||||
serviceConfig.ExecStart = pkgs.writeScript "static-zerotier-peers-autoaccept" ''
|
||||
#!/bin/sh
|
||||
${lib.concatMapStringsSep "\n" (host: ''
|
||||
${config.clan.core.clanPkgs.zerotier-members}/bin/zerotier-members allow --member-ip ${host}
|
||||
${
|
||||
inputs.clan-core.packages.${pkgs.system}.zerotier-members
|
||||
}/bin/zerotier-members allow --member-ip ${host}
|
||||
'') hosts}
|
||||
'';
|
||||
};
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
---
|
||||
description = "Enable ZeroTier VPN over TCP for networks where UDP is blocked."
|
||||
Enable ZeroTier VPN over TCP for networks where UDP is blocked.
|
||||
---
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
devShells.default = pkgs.mkShell {
|
||||
packages = [
|
||||
select-shell
|
||||
pkgs.nix-unit
|
||||
pkgs.tea
|
||||
# Better error messages than nix 2.18
|
||||
pkgs.nixVersions.latest
|
||||
|
|
|
@ -44,14 +44,6 @@ Let's get your development environment up and running:
|
|||
```bash
|
||||
git remote add upstream gitea@git.clan.lol:clan/clan-core.git
|
||||
```
|
||||
5. **Create an access token**:
|
||||
- Log in to Gitea.
|
||||
- Go to your account settings.
|
||||
- Navigate to the Applications section.
|
||||
- Click Generate New Token.
|
||||
- Name your token and select all available scopes.
|
||||
- Generate the token and copy it for later use.
|
||||
- Your access token is now ready to use with all permissions.
|
||||
|
||||
5. **Register Your Gitea Account Locally**:
|
||||
|
||||
|
@ -62,8 +54,9 @@ Let's get your development environment up and running:
|
|||
- Fill out the prompt as follows:
|
||||
- URL of Gitea instance: `https://git.clan.lol`
|
||||
- Name of new Login [git.clan.lol]:
|
||||
- Do you have an access token? Yes
|
||||
- Token: <yourtoken>
|
||||
- Do you have an access token? No
|
||||
- Username: YourUsername
|
||||
- Password: YourPassword
|
||||
- Set Optional settings: No
|
||||
|
||||
|
||||
|
|
|
@ -49,20 +49,19 @@ nav:
|
|||
- Mesh VPN: getting-started/mesh-vpn.md
|
||||
- Backup & Restore: getting-started/backups.md
|
||||
- Flake-parts: getting-started/flake-parts.md
|
||||
- Reference:
|
||||
- Modules:
|
||||
- Clan Modules:
|
||||
- reference/clanModules/borgbackup-static.md
|
||||
- reference/clanModules/borgbackup.md
|
||||
- reference/clanModules/deltachat.md
|
||||
- reference/clanModules/disk-layouts.md
|
||||
- reference/clanModules/ergochat.md
|
||||
- reference/clanModules/localbackup.md
|
||||
- reference/clanModules/localsend.md
|
||||
- reference/clanModules/matrix-synapse.md
|
||||
- reference/clanModules/moonlight.md
|
||||
- reference/clanModules/packages.md
|
||||
- reference/clanModules/postgresql.md
|
||||
- reference/clanModules/root-password.md
|
||||
- reference/clanModules/single-disk.md
|
||||
- reference/clanModules/sshd.md
|
||||
- reference/clanModules/static-hosts.md
|
||||
- reference/clanModules/sunshine.md
|
||||
|
@ -75,18 +74,17 @@ nav:
|
|||
- reference/clanModules/zerotier-static-peers.md
|
||||
- reference/clanModules/zt-tcp-relay.md
|
||||
- CLI:
|
||||
- reference/cli/index.md
|
||||
- reference/cli/backups.md
|
||||
- reference/cli/config.md
|
||||
- reference/cli/facts.md
|
||||
- reference/cli/flakes.md
|
||||
- reference/cli/flash.md
|
||||
- reference/cli/history.md
|
||||
- reference/cli/index.md
|
||||
- reference/cli/machines.md
|
||||
- reference/cli/secrets.md
|
||||
- reference/cli/show.md
|
||||
- reference/cli/ssh.md
|
||||
- reference/cli/state.md
|
||||
- reference/cli/vms.md
|
||||
- Clan Core:
|
||||
- reference/clan-core/index.md
|
||||
|
|
|
@ -12,14 +12,13 @@
|
|||
# { clanCore = «derivation JSON»; clanModules = { ${name} = «derivation JSON» }; }
|
||||
jsonDocs = import ./get-module-docs.nix {
|
||||
inherit (inputs) nixpkgs;
|
||||
inherit pkgs;
|
||||
inherit pkgs self;
|
||||
inherit (self.nixosModules) clanCore;
|
||||
inherit (self) clanModules;
|
||||
};
|
||||
|
||||
clanModulesFileInfo = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModules);
|
||||
# clanModulesReadmes = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesReadmes);
|
||||
# clanModulesMeta = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesMeta);
|
||||
clanModulesReadmes = pkgs.writeText "info.json" (builtins.toJSON jsonDocs.clanModulesReadmes);
|
||||
|
||||
# Simply evaluated options (JSON)
|
||||
renderOptions =
|
||||
|
@ -30,7 +29,6 @@
|
|||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
pkgs.mypy
|
||||
self'.packages.clan-cli
|
||||
];
|
||||
}
|
||||
''
|
||||
|
@ -51,35 +49,24 @@
|
|||
sha256 = "sha256-GZMeZFFGvP5GMqqh516mjJKfQaiJ6bL38bSYOXkaohc=";
|
||||
};
|
||||
|
||||
module-docs =
|
||||
pkgs.runCommand "rendered"
|
||||
{
|
||||
nativeBuildInputs = [
|
||||
pkgs.python3
|
||||
self'.packages.clan-cli
|
||||
];
|
||||
}
|
||||
''
|
||||
export CLAN_CORE_PATH=${self}
|
||||
export CLAN_CORE_DOCS=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
||||
# A file that contains the links to all clanModule docs
|
||||
export CLAN_MODULES=${clanModulesFileInfo}
|
||||
module-docs = pkgs.runCommand "rendered" { nativeBuildInputs = [ pkgs.python3 ]; } ''
|
||||
export CLAN_CORE=${jsonDocs.clanCore}/share/doc/nixos/options.json
|
||||
# A file that contains the links to all clanModule docs
|
||||
export CLAN_MODULES=${clanModulesFileInfo}
|
||||
export CLAN_MODULES_READMES=${clanModulesReadmes}
|
||||
|
||||
mkdir $out
|
||||
mkdir $out
|
||||
|
||||
# The python script will place mkDocs files in the output directory
|
||||
python3 ${renderOptions}
|
||||
'';
|
||||
# The python script will place mkDocs files in the output directory
|
||||
python3 ${renderOptions}
|
||||
'';
|
||||
in
|
||||
{
|
||||
devShells.docs = pkgs.callPackage ./shell.nix {
|
||||
inherit (self'.packages) docs clan-cli-docs;
|
||||
inherit
|
||||
asciinema-player-js
|
||||
asciinema-player-css
|
||||
module-docs
|
||||
self'
|
||||
;
|
||||
inherit module-docs;
|
||||
inherit asciinema-player-js;
|
||||
inherit asciinema-player-css;
|
||||
};
|
||||
packages = {
|
||||
docs = pkgs.python3.pkgs.callPackage ./default.nix {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
pkgs,
|
||||
clanCore,
|
||||
clanModules,
|
||||
self,
|
||||
}:
|
||||
let
|
||||
allNixosModules = (import "${nixpkgs}/nixos/modules/module-list.nix") ++ [
|
||||
|
@ -30,7 +31,7 @@ let
|
|||
options:
|
||||
pkgs.nixosOptionsDoc {
|
||||
options = options;
|
||||
warningsAreErrors = true;
|
||||
warningsAreErrors = false;
|
||||
};
|
||||
|
||||
# clanModules docs
|
||||
|
@ -38,10 +39,15 @@ let
|
|||
name: module: (evalDocs ((getOptionsWithoutCore [ module ]).clan.${name} or { })).optionsJSON
|
||||
) clanModules;
|
||||
|
||||
clanModulesReadmes = builtins.mapAttrs (
|
||||
module_name: _module: self.lib.modules.getReadme module_name
|
||||
) clanModules;
|
||||
|
||||
# clanCore docs
|
||||
clanCoreDocs = (evalDocs (getOptions [ ]).clan.core).optionsJSON;
|
||||
in
|
||||
{
|
||||
inherit clanModulesReadmes;
|
||||
clanCore = clanCoreDocs;
|
||||
clanModules = clanModulesDocs;
|
||||
}
|
||||
|
|
|
@ -28,12 +28,10 @@ import os
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from clan_cli.api.modules import Frontmatter, extract_frontmatter, get_roles
|
||||
|
||||
# Get environment variables
|
||||
CLAN_CORE_PATH = os.getenv("CLAN_CORE_PATH")
|
||||
CLAN_CORE_DOCS = os.getenv("CLAN_CORE_DOCS")
|
||||
CLAN_CORE = os.getenv("CLAN_CORE")
|
||||
CLAN_MODULES = os.environ.get("CLAN_MODULES")
|
||||
CLAN_MODULES_READMES = os.environ.get("CLAN_MODULES_READMES")
|
||||
|
||||
OUT = os.environ.get("out")
|
||||
|
||||
|
@ -78,9 +76,7 @@ def render_option(name: str, option: dict[str, Any], level: int = 3) -> str:
|
|||
|
||||
res = f"""
|
||||
{"#" * level} {sanitize(name)}
|
||||
|
||||
{"**Readonly**" if read_only else ""}
|
||||
|
||||
{"Readonly" if read_only else ""}
|
||||
{option.get("description", "No description available.")}
|
||||
|
||||
**Type**: `{option["type"]}`
|
||||
|
@ -113,19 +109,18 @@ def render_option(name: str, option: dict[str, Any], level: int = 3) -> str:
|
|||
"""
|
||||
|
||||
decls = option.get("declarations", [])
|
||||
if decls:
|
||||
source_path, name = replace_store_path(decls[0])
|
||||
print(source_path, name)
|
||||
res += f"""
|
||||
source_path, name = replace_store_path(decls[0])
|
||||
print(source_path, name)
|
||||
res += f"""
|
||||
:simple-git: [{name}]({source_path})
|
||||
"""
|
||||
res += "\n"
|
||||
res += "\n"
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def module_header(module_name: str) -> str:
|
||||
return f"# {module_name}\n\n"
|
||||
return f"# {module_name}\n"
|
||||
|
||||
|
||||
def module_usage(module_name: str) -> str:
|
||||
|
@ -151,9 +146,9 @@ options_head = "\n## Module Options\n"
|
|||
|
||||
|
||||
def produce_clan_core_docs() -> None:
|
||||
if not CLAN_CORE_DOCS:
|
||||
if not CLAN_CORE:
|
||||
raise ValueError(
|
||||
f"Environment variables are not set correctly: $CLAN_CORE_DOCS={CLAN_CORE_DOCS}"
|
||||
f"Environment variables are not set correctly: $CLAN_CORE={CLAN_CORE}"
|
||||
)
|
||||
|
||||
if not OUT:
|
||||
|
@ -161,7 +156,7 @@ def produce_clan_core_docs() -> None:
|
|||
|
||||
# A mapping of output file to content
|
||||
core_outputs: dict[str, str] = {}
|
||||
with open(CLAN_CORE_DOCS) as f:
|
||||
with open(CLAN_CORE) as f:
|
||||
options: dict[str, dict[str, Any]] = json.load(f)
|
||||
module_name = "clan-core"
|
||||
for option_name, info in options.items():
|
||||
|
@ -193,42 +188,14 @@ def produce_clan_core_docs() -> None:
|
|||
of.write(output)
|
||||
|
||||
|
||||
def render_roles(roles: list[str] | None, module_name: str) -> str:
|
||||
if roles:
|
||||
roles_list = "\n".join([f" - `{r}`" for r in roles])
|
||||
return f"""
|
||||
???+ tip "Inventory usage"
|
||||
|
||||
Predefined roles:
|
||||
|
||||
{roles_list}
|
||||
|
||||
Usage:
|
||||
|
||||
```{{.nix hl_lines="4"}}
|
||||
buildClan {{
|
||||
inventory.services = {{
|
||||
{module_name}.instance_1 = {{
|
||||
roles.{roles[0]}.machines = [ "sara_machine" ];
|
||||
# ...
|
||||
}};
|
||||
}};
|
||||
}}
|
||||
```
|
||||
|
||||
"""
|
||||
return ""
|
||||
|
||||
|
||||
def produce_clan_modules_docs() -> None:
|
||||
if not CLAN_MODULES:
|
||||
raise ValueError(
|
||||
f"Environment variables are not set correctly: $CLAN_MODULES={CLAN_MODULES}"
|
||||
)
|
||||
|
||||
if not CLAN_CORE_PATH:
|
||||
if not CLAN_MODULES_READMES:
|
||||
raise ValueError(
|
||||
f"Environment variables are not set correctly: $CLAN_CORE_PATH={CLAN_CORE_PATH}"
|
||||
f"Environment variables are not set correctly: $CLAN_MODULES_READMES={CLAN_MODULES_READMES}"
|
||||
)
|
||||
|
||||
if not OUT:
|
||||
|
@ -237,36 +204,18 @@ def produce_clan_modules_docs() -> None:
|
|||
with open(CLAN_MODULES) as f:
|
||||
links: dict[str, str] = json.load(f)
|
||||
|
||||
# with open(CLAN_MODULES_READMES) as readme:
|
||||
# readme_map: dict[str, str] = json.load(readme)
|
||||
|
||||
# with open(CLAN_MODULES_META) as f:
|
||||
# meta_map: dict[str, Any] = json.load(f)
|
||||
# print(meta_map)
|
||||
with open(CLAN_MODULES_READMES) as readme:
|
||||
readme_map: dict[str, str] = json.load(readme)
|
||||
|
||||
# {'borgbackup': '/nix/store/hi17dwgy7963ddd4ijh81fv0c9sbh8sw-options.json', ... }
|
||||
for module_name, options_file in links.items():
|
||||
readme_file = Path(CLAN_CORE_PATH) / "clanModules" / module_name / "README.md"
|
||||
print(module_name, readme_file)
|
||||
with open(readme_file) as f:
|
||||
readme = f.read()
|
||||
frontmatter: Frontmatter
|
||||
frontmatter, readme_content = extract_frontmatter(readme, str(readme_file))
|
||||
print(frontmatter, readme_content)
|
||||
|
||||
with open(Path(options_file) / "share/doc/nixos/options.json") as f:
|
||||
options: dict[str, dict[str, Any]] = json.load(f)
|
||||
print(f"Rendering options for {module_name}...")
|
||||
output = module_header(module_name)
|
||||
|
||||
if frontmatter.description:
|
||||
output += f"**{frontmatter.description}**\n\n"
|
||||
output += f"{readme_content}\n"
|
||||
|
||||
# get_roles(str) -> list[str] | None
|
||||
roles = get_roles(str(Path(CLAN_CORE_PATH) / "clanModules" / module_name))
|
||||
if roles:
|
||||
output += render_roles(roles, module_name)
|
||||
if readme_map.get(module_name, None):
|
||||
output += f"{readme_map[module_name]}\n"
|
||||
|
||||
output += module_usage(module_name)
|
||||
|
||||
|
|
|
@ -7,14 +7,10 @@
|
|||
asciinema-player-css,
|
||||
roboto,
|
||||
fira-code,
|
||||
self',
|
||||
...
|
||||
}:
|
||||
pkgs.mkShell {
|
||||
inputsFrom = [
|
||||
docs
|
||||
self'.devShells.default
|
||||
];
|
||||
inputsFrom = [ docs ];
|
||||
shellHook = ''
|
||||
mkdir -p ./site/reference/cli
|
||||
cp -af ${module-docs}/* ./site/reference/
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
---
|
||||
title: "Dev Report: Declarative Backups and Restore"
|
||||
description: "An extension to the NixOS module system to declaratively describe how application state is backed up and restored."
|
||||
authors:
|
||||
- Mic92
|
||||
date: 2024-06-24
|
||||
slug: backups
|
||||
---
|
||||
|
||||
Our goal with [Clan](https://clan.lol/) is to give users control over their data.
|
||||
However, with great power comes great responsibility, and owning your data means you also need to take care of backups yourself.
|
||||
|
||||
In our experience, setting up automatic backups is often a tedious process as it requires custom integration of the backup software and
|
||||
the services that produce the state. More important than the backup is the restore.
|
||||
Restores are often not well tested or documented, and if not working correctly, they can render the backup useless.
|
||||
|
||||
In Clan, we want to make backup and restore a first-class citizen.
|
||||
Every service should describe what state it produces and how it can be backed up and restored.
|
||||
|
||||
In this article, we will discuss how our backup interface in Clan works.
|
||||
The interface allows different backup software to be used interchangeably and
|
||||
allows module authors to define custom backup and restore logic for their services.
|
||||
|
||||
|
||||
## First Comes the State
|
||||
|
||||
Our services are built from Clan modules. Clan modules are essentially [NixOS modules](https://wiki.nixos.org/wiki/NixOS_modules), the basic configuration components of NixOS.
|
||||
However, we have enhanced them with additional features provided by Clan and restricted certain option types to enable configuration through a [graphical interface](https://docs.clan.lol/blog/2024/05/25/jsonschema-converter/).
|
||||
|
||||
In a simple case, this can be just a bunch of directories, such as what we define for our [ZeroTier](https://www.zerotier.com/) VPN service.
|
||||
|
||||
```nix
|
||||
{
|
||||
clan.core.state.zerotier.folders = [ "/var/lib/zerotier-one" ];
|
||||
}
|
||||
```
|
||||
|
||||
For other systems, we need more complex backup and restore logic.
|
||||
For each state, we can provide custom command hooks for backing up and restoring.
|
||||
|
||||
In our PostgreSQL module, for example, we define `preBackupCommand` and `postRestoreCommand` to use `pg_dump` and `pg_restore` to backup and restore individual databases:
|
||||
|
||||
```nix
|
||||
preBackupCommand = ''
|
||||
# ...
|
||||
runuser -u postgres -- pg_dump ${compression} --dbname=${db.name} -Fc -c > "${current}.tmp"
|
||||
# ...
|
||||
'';
|
||||
postRestoreCommand = ''
|
||||
# ...
|
||||
runuser -u postgres -- dropdb "${db.name}"
|
||||
runuser -u postgres -- pg_restore -C -d postgres "${current}"
|
||||
# ...
|
||||
'';
|
||||
```
|
||||
|
||||
## Then the Backup
|
||||
|
||||
Our CLI unifies the different backup providers in one [interface](https://docs.clan.lol/reference/cli/backups/).
|
||||
|
||||
As of now, we support backups using [BorgBackup](https://www.borgbackup.org/) and
|
||||
a backup module called "localbackup" based on [rsnapshot](https://rsnapshot.org/), optimized for backup on locally attached storage media.
|
||||
|
||||
To use different backup software, a module needs to set the options provided by our backup interface.
|
||||
The following Nix code is a toy example that uses the `tar` program to perform backup and restore to illustrate how the backup interface works:
|
||||
|
||||
```nix
|
||||
clan.core.backups.providers.tar = {
|
||||
list = ''
|
||||
echo /var/lib/system-back.tar
|
||||
'';
|
||||
create = let
|
||||
uniqueFolders = lib.unique (
|
||||
lib.flatten (lib.mapAttrsToList (_name: state: state.folders) config.clan.core.state)
|
||||
);
|
||||
in ''
|
||||
# FIXME: a proper implementation should also run `state.preBackupCommand` of each state
|
||||
if [ -f /var/lib/system-back.tar ]; then
|
||||
tar -uvpf /var/lib/system-back.tar ${builtins.toString uniqueFolders}
|
||||
else
|
||||
tar -cvpf /var/lib/system-back.tar ${builtins.toString uniqueFolders}
|
||||
fi
|
||||
'';
|
||||
restore = ''
|
||||
IFS=':' read -ra FOLDER <<< "''$FOLDERS"
|
||||
echo "${FOLDER[@]}" > /run/folders-to-restore.txt
|
||||
tar -xvpf /var/lib/system-back.tar -C / -T /run/folders-to-restore.txt
|
||||
'';
|
||||
};
|
||||
```
|
||||
|
||||
For better real-world implementations, check out the implementations for [BorgBackup](https://git.clan.lol/clan/clan-core/src/branch/main/clanModules/borgbackup/default.nix)
|
||||
and [localbackup](https://git.clan.lol/clan/clan-core/src/branch/main/clanModules/localbackup/default.nix).
|
||||
|
||||
## What It Looks Like to the End User
|
||||
|
||||
After following the guide for configuring a [backup](https://docs.clan.lol/getting-started/backups/),
|
||||
users can use the CLI to create backups, list them, and restore them.
|
||||
|
||||
Backups can be created through the CLI like this:
|
||||
|
||||
```
|
||||
clan backups create web01
|
||||
```
|
||||
|
||||
BorgBackup will also create backups itself every day by default.
|
||||
|
||||
Completed backups can be listed like this:
|
||||
|
||||
```
|
||||
clan backups list web01
|
||||
...
|
||||
web01::u366395@u366395.your-storagebox.de:/./borgbackup::web01-web01-2024-06-18T01:00:00
|
||||
web03::u366395@u366395.your-storagebox.de:/./borgbackup::web01-web01-2024-06-19T01:00:00
|
||||
```
|
||||
One cool feature of our backup system is that it is aware of individual services/applications.
|
||||
Let's say we want to restore the state of our [Matrix](https://matrix.org/) chat server; we can just specify it like this:
|
||||
|
||||
```
|
||||
clan backups restore --service matrix-synapse web01 borgbackup web03::u366395@u366395.your-storagebox.de:/./borgbackup::web01-web01-2024-06-19T01:00:00
|
||||
```
|
||||
|
||||
In this case, it will first stop the matrix-synapse systemd service, then delete the [PostgreSQL](https://www.postgresql.org/) database, restore the database from the backup, and then start the matrix-synapse service again.
|
||||
|
||||
## Future work
|
||||
|
||||
As of now we implemented our backup and restore for a handful of services and we expect to refine the interface as we test the interface for more applications.
|
||||
|
||||
Currently, our backup implementation backs up filesystem state from running services.
|
||||
This can lead to inconsistencies if applications change the state while the backup is running.
|
||||
In the future, we hope to make backups more atomic by backing up a filesystem snapshot instead of normal directories.
|
||||
This, however, requires the use of modern filesystems that support these features.
|
|
@ -4,14 +4,14 @@
|
|||
|
||||
In the `flake.nix` file:
|
||||
|
||||
- [x] set a unique `name`.
|
||||
- [x] set a unique `clanName`.
|
||||
|
||||
=== "**buildClan**"
|
||||
|
||||
```nix title="clan-core.lib.buildClan"
|
||||
buildClan {
|
||||
# Set a unique name
|
||||
meta.name = "Lobsters";
|
||||
clanName = "Lobsters";
|
||||
# Should usually point to the directory of flake.nix
|
||||
directory = ./.;
|
||||
|
||||
|
@ -31,7 +31,7 @@ In the `flake.nix` file:
|
|||
```nix title="clan-core.flakeModules.default"
|
||||
clan = {
|
||||
# Set a unique name
|
||||
meta.name = "Lobsters";
|
||||
clanName = "Lobsters";
|
||||
|
||||
machines = {
|
||||
jon = {
|
||||
|
|
|
@ -16,7 +16,7 @@ inputs = {
|
|||
# New flake-parts input
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
|
||||
|
||||
|
||||
clan-core = {
|
||||
url = "git+https://git.clan.lol/clan/clan-core";
|
||||
inputs.nixpkgs.follows = "nixpkgs"; # Needed if your configuration uses nixpkgs unstable.
|
||||
|
@ -35,7 +35,7 @@ After updating your flake inputs, the next step is to import the `clan-core` fla
|
|||
inputs@{ flake-parts, ... }:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } (
|
||||
{
|
||||
#
|
||||
#
|
||||
imports = [
|
||||
inputs.clan-core.flakeModules.default
|
||||
];
|
||||
|
@ -63,7 +63,7 @@ Below is a guide on how to structure this in your flake.nix:
|
|||
# Define your clan
|
||||
clan = {
|
||||
# Clan wide settings. (Required)
|
||||
meta.name = ""; # Ensure to choose a unique name.
|
||||
clanName = ""; # Ensure to choose a unique name.
|
||||
|
||||
machines = {
|
||||
jon = {
|
||||
|
@ -84,7 +84,7 @@ Below is a guide on how to structure this in your flake.nix:
|
|||
|
||||
# There needs to be exactly one controller per clan
|
||||
clan.networking.zerotier.controller.enable = true;
|
||||
|
||||
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
25
flake.lock
25
flake.lock
|
@ -40,11 +40,24 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixlib": {
|
||||
"locked": {
|
||||
"lastModified": 1712450863,
|
||||
"narHash": "sha256-K6IkdtMtq9xktmYPj0uaYc8NsIqHuaAoRBaMgu9Fvrw=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "3c62b6a12571c9a7f65ab037173ee153d539905f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixos-generators": {
|
||||
"inputs": {
|
||||
"nixlib": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixlib": "nixlib",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
|
@ -86,11 +99,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1719451888,
|
||||
"narHash": "sha256-Ky0sgEEJMcBmNEJztY6KcVn+6bq74EKM7pd1CR1wnPQ=",
|
||||
"lastModified": 1719146883,
|
||||
"narHash": "sha256-DAyIfQgyqalov0DcEKRvDOUin7axELasaP6NCPt7UQA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3664857c48feacb35770c00abfdc671e55849be5",
|
||||
"rev": "084f8df2f3ff80cdec6f515931036f63c5d2f36c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
sops-nix.inputs.nixpkgs-stable.follows = "";
|
||||
nixos-generators.url = "github:nix-community/nixos-generators";
|
||||
nixos-generators.inputs.nixpkgs.follows = "nixpkgs";
|
||||
nixos-generators.inputs.nixlib.follows = "nixpkgs";
|
||||
nixos-images.url = "github:nix-community/nixos-images";
|
||||
nixos-images.inputs.nixos-unstable.follows = "nixpkgs";
|
||||
# unused input
|
||||
|
@ -32,7 +31,7 @@
|
|||
{ ... }:
|
||||
{
|
||||
clan = {
|
||||
meta.name = "clan-core";
|
||||
# meta.name = "clan-core";
|
||||
directory = self;
|
||||
};
|
||||
systems = [
|
||||
|
@ -52,9 +51,10 @@
|
|||
./formatter.nix
|
||||
./lib/flake-module.nix
|
||||
./nixosModules/flake-module.nix
|
||||
./nixosModules/clanCore/vars/flake-module.nix
|
||||
./pkgs/flake-module.nix
|
||||
./templates/flake-module.nix
|
||||
|
||||
./inventory/flake-module.nix
|
||||
];
|
||||
}
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ let
|
|||
inherit lib clan-core;
|
||||
inherit (inputs) nixpkgs;
|
||||
};
|
||||
|
||||
cfg = config.clan;
|
||||
in
|
||||
{
|
||||
|
@ -90,11 +91,6 @@ in
|
|||
clanInternals = lib.mkOption {
|
||||
type = lib.types.submodule {
|
||||
options = {
|
||||
inventory = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
||||
inventoryFile = lib.mkOption { type = lib.types.unspecified; };
|
||||
|
||||
clanModules = lib.mkOption { type = lib.types.attrsOf lib.types.path; };
|
||||
source = lib.mkOption { type = lib.types.path; };
|
||||
meta = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
||||
all-machines-json = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; };
|
||||
machines = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf lib.types.unspecified); };
|
||||
|
|
|
@ -45,53 +45,5 @@
|
|||
];
|
||||
includes = [ "*.py" ];
|
||||
};
|
||||
# FIXME: currently broken in CI
|
||||
#treefmt.settings.formatter.vale =
|
||||
# let
|
||||
# vocab = "cLAN";
|
||||
# style = "Docs";
|
||||
# config = pkgs.writeText "vale.ini" ''
|
||||
# StylesPath = ${styles}
|
||||
# Vocab = ${vocab}
|
||||
|
||||
# [*.md]
|
||||
# BasedOnStyles = Vale, ${style}
|
||||
# Vale.Terms = No
|
||||
# '';
|
||||
# styles = pkgs.symlinkJoin {
|
||||
# name = "vale-style";
|
||||
# paths = [
|
||||
# accept
|
||||
# headings
|
||||
# ];
|
||||
# };
|
||||
# accept = pkgs.writeTextDir "config/vocabularies/${vocab}/accept.txt" ''
|
||||
# Nix
|
||||
# NixOS
|
||||
# Nixpkgs
|
||||
# clan.lol
|
||||
# Clan
|
||||
# monorepo
|
||||
# '';
|
||||
# headings = pkgs.writeTextDir "${style}/headings.yml" ''
|
||||
# extends: capitalization
|
||||
# message: "'%s' should be in sentence case"
|
||||
# level: error
|
||||
# scope: heading
|
||||
# # $title, $sentence, $lower, $upper, or a pattern.
|
||||
# match: $sentence
|
||||
# '';
|
||||
# in
|
||||
# {
|
||||
# command = "${pkgs.vale}/bin/vale";
|
||||
# options = [ "--config=${config}" ];
|
||||
# includes = [ "*.md" ];
|
||||
# # TODO: too much at once, fix piecemeal
|
||||
# excludes = [
|
||||
# "docs/*"
|
||||
# "clanModules/*"
|
||||
# "pkgs/*"
|
||||
# ];
|
||||
# };
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
{
|
||||
"machines": {
|
||||
"minimal-inventory-machine": {
|
||||
"name": "foo",
|
||||
"system": "x86_64-linux",
|
||||
"description": "A nice thing",
|
||||
"icon": "./path/to/icon.png",
|
||||
"tags": ["1", "2", "3"]
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"packages": {
|
||||
"editors": {
|
||||
"meta": {
|
||||
"name": "Some editor packages"
|
||||
},
|
||||
"roles": {
|
||||
"default": {
|
||||
"machines": ["minimal-inventory-machine"]
|
||||
}
|
||||
},
|
||||
"machines": {
|
||||
"minimal-inventory-machine": {
|
||||
"config": {
|
||||
"packages": ["zed-editor"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"packages": ["vim"]
|
||||
}
|
||||
},
|
||||
"browsing": {
|
||||
"meta": {
|
||||
"name": "Web browsing packages"
|
||||
},
|
||||
"roles": {
|
||||
"default": {
|
||||
"machines": ["minimal-inventory-machine"]
|
||||
}
|
||||
},
|
||||
"machines": {
|
||||
"minimal-inventory-machine": {
|
||||
"config": {
|
||||
"packages": ["chromium"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"packages": ["firefox"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"single-disk": {
|
||||
"default": {
|
||||
"meta": {
|
||||
"name": "single-disk"
|
||||
},
|
||||
"roles": {
|
||||
"default": {
|
||||
"machines": ["minimal-inventory-machine"]
|
||||
}
|
||||
},
|
||||
"machines": {
|
||||
"minimal-inventory-machine": {
|
||||
"config": {
|
||||
"device": "/dev/null"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
inventory/README.md
Normal file
57
inventory/README.md
Normal file
|
@ -0,0 +1,57 @@
|
|||
# Inventory
|
||||
|
||||
This part provides a specification for the inventory.
|
||||
|
||||
It is used for design phase and as validation helper.
|
||||
|
||||
> Cue is less verbose and easier to understand and maintain than json-schema.
|
||||
> Json-schema, if needed can be easily generated on-the fly.
|
||||
|
||||
## Checking validity
|
||||
|
||||
Directly check a json against the schema
|
||||
|
||||
`cue vet inventory.json root.cue -d '#Root'`
|
||||
|
||||
## Json schema
|
||||
|
||||
Export the json-schema i.e. for usage in python / javascript / nix
|
||||
|
||||
`cue export --out openapi root.cue`
|
||||
|
||||
## Usage
|
||||
|
||||
Comments are rendered as descriptions in the json schema.
|
||||
|
||||
```cue
|
||||
// A name of the clan (primarily shown by the UI)
|
||||
name: string
|
||||
```
|
||||
|
||||
Cue open sets. In the following `foo = {...}` means that the key `foo` can contain any arbitrary json object.
|
||||
|
||||
```cue
|
||||
foo: { ... }
|
||||
```
|
||||
|
||||
Cue dynamic keys.
|
||||
|
||||
```cue
|
||||
[string]: {
|
||||
attr: string
|
||||
}
|
||||
```
|
||||
|
||||
This is the schema of
|
||||
|
||||
```json
|
||||
{
|
||||
"a": {
|
||||
"attr": "foo"
|
||||
},
|
||||
"b": {
|
||||
"attr": "bar"
|
||||
}
|
||||
// ... Indefinitely more dynamic keys of type "string"
|
||||
}
|
||||
```
|
137
inventory/example_flake.nix
Normal file
137
inventory/example_flake.nix
Normal file
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
description = "<Put your description here>";
|
||||
|
||||
inputs.clan-core.url = "https://git.clan.lol/clan/clan-core/archive/main.tar.gz";
|
||||
|
||||
outputs =
|
||||
{ clan-core, ... }:
|
||||
let
|
||||
pkgs = clan-core.inputs.nixpkgs.legacyPackages.${system};
|
||||
system = "x86_64-linux";
|
||||
in
|
||||
# Usage see: https://docs.clan.lol
|
||||
# nice_flake_interface -> buildInventory() -> Inventory -> buildClanFromInventory() -> nixosConfigurations
|
||||
# buildClanFromInventory = inventory: evalModules {
|
||||
# extraAttrs = { inherit inventory; };
|
||||
# # (attrNames inventory.machines)
|
||||
# };
|
||||
# clan =
|
||||
# clan-core.lib.buildClanFromInventory [
|
||||
# # Inventory 0 (loads the json file managed by the Python API)
|
||||
# (builtins.fromJSON (builtins.readFile ./inventory.json))
|
||||
# # ->
|
||||
# # {
|
||||
# # services."backups_1".autoIncludeMachines = true;
|
||||
# # services."backups_1".module = "borgbackup";
|
||||
# # ... etc.
|
||||
# # }
|
||||
# ]
|
||||
# ++ (buildInventory {
|
||||
# clanName = "nice_flake_interface";
|
||||
# description = "A nice flake interface";
|
||||
# icon = "assets/icon.png";
|
||||
# machines = {
|
||||
# jon = {
|
||||
# # Just regular nixos/clan configuration ?
|
||||
# # config = {
|
||||
# # imports = [
|
||||
# # ./modules/shared.nix
|
||||
# # ./machines/jon/configuration.nix
|
||||
# # ];
|
||||
# # nixpkgs.hostPlatform = system;
|
||||
# # # Set this for clan commands use ssh i.e. `clan machines update`
|
||||
# # # If you change the hostname, you need to update this line to root@<new-hostname>
|
||||
# # # This only works however if you have avahi running on your admin machine else use IP
|
||||
# # clan.networking.targetHost = pkgs.lib.mkDefault "root@jon";
|
||||
# # # ssh root@flash-installer.local lsblk --output NAME,ID-LINK,FSTYPE,SIZE,MOUNTPOINT
|
||||
# # 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 = throw ''
|
||||
# # Don't forget to add your SSH key here!
|
||||
# # users.users.root.openssh.authorizedKeys.keys = [ "<YOUR SSH_KEY>" ]
|
||||
# # '';
|
||||
# # # Zerotier needs one controller to accept new nodes. Once accepted
|
||||
# # # the controller can be offline and routing still works.
|
||||
# # clan.networking.zerotier.controller.enable = true;
|
||||
# # };
|
||||
# };
|
||||
# };
|
||||
# })
|
||||
# ++ [
|
||||
# # Low level inventory overrides (comes at the end)
|
||||
# {
|
||||
# services."backups_2".autoIncludeMachines = true;
|
||||
# services."backups_2".module = "borgbackup";
|
||||
# }
|
||||
# ];
|
||||
# # buildClan :: [ Partial<Inventory> ] -> Inventory
|
||||
# # foldl' (acc: v: lib.recursiveUpdate acc v) {} []
|
||||
# inventory = [
|
||||
# # import json
|
||||
# {...}
|
||||
# # power user flake
|
||||
# {...}
|
||||
# ]
|
||||
# # With Module system
|
||||
# # Pros: Easy to understand,
|
||||
# # Cons: Verbose, hard to maintain
|
||||
# # buildClan :: { modules = [ { config = Partial<Inventory>; options :: InventoryOptions; } } ]; } -> Inventory
|
||||
# eval = lib.evalModules {
|
||||
# modules = [
|
||||
# {
|
||||
# # Inventory Schema
|
||||
# # Python validation
|
||||
# options = {...}
|
||||
# }
|
||||
# {
|
||||
# config = map lib.mkDefault
|
||||
# (builtins.fromJSON (builtins.readFile ./inventory.json))
|
||||
# }
|
||||
# {
|
||||
# # User provided
|
||||
# config = {...}
|
||||
# }
|
||||
# # Later overrides.
|
||||
# {
|
||||
# lib.mkForce ...
|
||||
# }
|
||||
# ];
|
||||
# }
|
||||
# nixosConfigurations = lib.evalModules inventory;
|
||||
# eval.config.inventory
|
||||
# #
|
||||
# eval.config.machines.jon#nixosConfig
|
||||
# eval.config.machines.sara#nixosConfig
|
||||
#
|
||||
# {inventory, config, ...}:{
|
||||
# hostname = config.machines.sara # Invalid
|
||||
# hostname = inventory.machines.sara.hostname # Valid
|
||||
# }
|
||||
/*
|
||||
# Type
|
||||
|
||||
buildInventory :: {
|
||||
clanName :: string
|
||||
machines :: {
|
||||
${name} :: {
|
||||
config :: {
|
||||
# NixOS configuration
|
||||
};
|
||||
};
|
||||
};
|
||||
# ... More mapped inventory options
|
||||
# i.e. shared config for all machines
|
||||
} -> Inventory
|
||||
*/
|
||||
{
|
||||
# all machines managed by Clan
|
||||
inherit (clan) nixosConfigurations clanInternals;
|
||||
# add the Clan cli tool to the dev shell
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
packages = [ clan-core.packages.${system}.clan-cli ];
|
||||
};
|
||||
};
|
||||
}
|
46
inventory/flake-module.nix
Normal file
46
inventory/flake-module.nix
Normal file
|
@ -0,0 +1,46 @@
|
|||
{ ... }:
|
||||
{
|
||||
perSystem =
|
||||
{ pkgs, config, ... }:
|
||||
{
|
||||
packages.inventory-schema = pkgs.stdenv.mkDerivation {
|
||||
name = "inventory-schema";
|
||||
src = ./src;
|
||||
|
||||
buildInputs = [ pkgs.cue ];
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
'';
|
||||
};
|
||||
devShells.inventory-schema = pkgs.mkShell { inputsFrom = [ config.packages.inventory-schema ]; };
|
||||
|
||||
checks.inventory-schema-checks = pkgs.stdenv.mkDerivation {
|
||||
name = "inventory-schema-checks";
|
||||
src = ./src;
|
||||
buildInputs = [ pkgs.cue ];
|
||||
buildPhase = ''
|
||||
echo "Running inventory tests..."
|
||||
|
||||
echo "Export cue as json-schema..."
|
||||
cue export --out openapi root.cue
|
||||
|
||||
echo "Validate test/*.json against inventory-schema..."
|
||||
|
||||
test_dir="test"
|
||||
for file in "$test_dir"/*; do
|
||||
# Check if the item is a file
|
||||
if [ -f "$file" ]; then
|
||||
# Print the filename
|
||||
echo "Running test on: $file"
|
||||
|
||||
# Run the cue vet command
|
||||
cue vet "$file" root.cue -d "#Root"
|
||||
fi
|
||||
done
|
||||
|
||||
touch $out
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
8
inventory/src/machines/machines.cue
Normal file
8
inventory/src/machines/machines.cue
Normal file
|
@ -0,0 +1,8 @@
|
|||
package machines
|
||||
|
||||
|
||||
#machine: machines: [string]: {
|
||||
name: string,
|
||||
description?: string,
|
||||
icon?: string
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
package inventory
|
||||
|
||||
import (
|
||||
"clan.lol/inventory/schema"
|
||||
"clan.lol/inventory/services"
|
||||
"clan.lol/inventory/machines"
|
||||
)
|
||||
|
||||
@jsonschema(schema="http://json-schema.org/schema#")
|
||||
|
@ -15,9 +16,9 @@ import (
|
|||
icon?: string
|
||||
}
|
||||
|
||||
// // A map of services
|
||||
schema.#service
|
||||
// A map of services
|
||||
services.#service
|
||||
|
||||
// // A map of machines
|
||||
schema.#machine
|
||||
// A map of machines
|
||||
machines.#machine
|
||||
}
|
|
@ -1,39 +1,32 @@
|
|||
package schema
|
||||
package services
|
||||
|
||||
#machine: machines: [string]: {
|
||||
name: string,
|
||||
description?: string,
|
||||
icon?: string
|
||||
tags: [...string]
|
||||
}
|
||||
#ServiceRole: "server" | "client" | "both"
|
||||
|
||||
#role: string
|
||||
|
||||
#service: services: [string]: [string]: {
|
||||
#service: services: [string]: {
|
||||
// Required meta fields
|
||||
meta: {
|
||||
name: string,
|
||||
icon?: string
|
||||
description?: string,
|
||||
},
|
||||
// Required module specifies the behavior of the service.
|
||||
module: string,
|
||||
|
||||
// We moved the machine sepcific config to "machines".
|
||||
// It may be moved back depending on what makes more sense in the future.
|
||||
roles: [#role]: {
|
||||
machines: [...string],
|
||||
tags: [...string],
|
||||
}
|
||||
machines?: {
|
||||
machineConfig: {
|
||||
[string]: {
|
||||
roles?: [ ...#ServiceRole ],
|
||||
config?: {
|
||||
...
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Global Configuration for the service
|
||||
config?: {
|
||||
// Configuration for the service
|
||||
config: {
|
||||
// Schema depends on the module.
|
||||
// It declares the interface how the service can be configured.
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
38
inventory/src/tests/borgbackup.json
Normal file
38
inventory/src/tests/borgbackup.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"machines": {
|
||||
"camina_machine": {
|
||||
"name": "camina"
|
||||
},
|
||||
"vyr_machine": {
|
||||
"name": "vyr"
|
||||
},
|
||||
"vi_machine": {
|
||||
"name": "vi"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"name": "kenjis clan"
|
||||
},
|
||||
"services": {
|
||||
"backup": {
|
||||
"meta": {
|
||||
"name": "My backup"
|
||||
},
|
||||
"module": "borbackup-static",
|
||||
"machineConfig": {
|
||||
"vyr": {
|
||||
"roles": ["server"]
|
||||
},
|
||||
"vi": {
|
||||
"roles": ["client"]
|
||||
},
|
||||
"camina_machine": {
|
||||
"roles": ["client"]
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"folders": ["/home", "/root", "/var", "/etc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
45
inventory/src/tests/syncthing.json
Normal file
45
inventory/src/tests/syncthing.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"machines": {
|
||||
"camina_machine": {
|
||||
"name": "camina"
|
||||
},
|
||||
"vyr": {
|
||||
"name": "vyr"
|
||||
},
|
||||
"vi": {
|
||||
"name": "vi"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"name": "kenjis clan"
|
||||
},
|
||||
"services": {
|
||||
"sync_files": {
|
||||
"meta": {
|
||||
"name": "My sync"
|
||||
},
|
||||
"module": "syncthing-static-peers",
|
||||
"machineConfig": {
|
||||
"vyr": {},
|
||||
"vi": {},
|
||||
"camina_machine": {}
|
||||
},
|
||||
"config": {
|
||||
"folders": {
|
||||
"test": {
|
||||
"path": "~/data/docs",
|
||||
"devices": ["camina", "vyr", "vi"]
|
||||
},
|
||||
"videos": {
|
||||
"path": "~/data/videos",
|
||||
"devices": ["camina", "vyr", "ezra"]
|
||||
},
|
||||
"playlist": {
|
||||
"path": "~/data/playlist",
|
||||
"devices": ["camina", "vyr", "ezra"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
inventory/src/tests/zerotier.json
Normal file
36
inventory/src/tests/zerotier.json
Normal file
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"machines": {
|
||||
"camina_machine": {
|
||||
"name": "camina"
|
||||
},
|
||||
"vyr_machine": {
|
||||
"name": "vyr"
|
||||
},
|
||||
"vi_machine": {
|
||||
"name": "vi"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"name": "kenjis clan"
|
||||
},
|
||||
"services": {
|
||||
"backup": {
|
||||
"meta": {
|
||||
"name": "My backup"
|
||||
},
|
||||
"module": "borbackup-static",
|
||||
"machineConfig": {
|
||||
"vyr_machine": {
|
||||
"roles": ["server"]
|
||||
},
|
||||
"vi_machine": {
|
||||
"roles": ["peer"]
|
||||
},
|
||||
"camina_machine": {
|
||||
"roles": ["peer"]
|
||||
}
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,131 +12,73 @@
|
|||
# DEPRECATED: use meta.icon instead
|
||||
clanIcon ? null, # A path to an icon to be used for the clan, should be the same for all machines
|
||||
meta ? { }, # A set containing clan meta: name :: string, icon :: string, description :: string
|
||||
# A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
|
||||
# This improves performance, but all nipxkgs.* options will be ignored.
|
||||
pkgsForSystem ? (_system: null),
|
||||
/*
|
||||
Low level inventory configuration.
|
||||
Overrides the services configuration.
|
||||
*/
|
||||
inventory ? { },
|
||||
pkgsForSystem ? (_system: null), # A map from arch to pkgs, if specified this nixpkgs will be only imported once for each system.
|
||||
# This improves performance, but all nipxkgs.* options will be ignored.
|
||||
}:
|
||||
let
|
||||
# Internal inventory, this is the result of merging all potential inventory sources:
|
||||
# - Default instances configured via 'services'
|
||||
# - The inventory overrides
|
||||
# - Machines that exist in inventory.machines
|
||||
# - Machines explicitly configured via 'machines' argument
|
||||
# - Machines that exist in the machines directory
|
||||
# Checks on the module level:
|
||||
# - Each service role must reference a valid machine after all machines are merged
|
||||
mergedInventory =
|
||||
(lib.evalModules {
|
||||
modules = [
|
||||
clan-core.lib.inventory.interface
|
||||
{ inherit meta; }
|
||||
(
|
||||
if
|
||||
builtins.pathExists "${directory}/inventory.json"
|
||||
# Is recursively applied. Any explicit nix will override.
|
||||
then
|
||||
(builtins.fromJSON (builtins.readFile "${directory}/inventory.json"))
|
||||
else
|
||||
{ }
|
||||
)
|
||||
inventory
|
||||
# Machines explicitly configured via 'machines' argument
|
||||
{
|
||||
# { ${name} :: meta // { name, tags } }
|
||||
machines = lib.mapAttrs (
|
||||
name: config:
|
||||
(lib.attrByPath [
|
||||
"clan"
|
||||
"meta"
|
||||
] { } config)
|
||||
// {
|
||||
# meta.name default is the attribute name of the machine
|
||||
name = lib.mkDefault (
|
||||
lib.attrByPath [
|
||||
"clan"
|
||||
"meta"
|
||||
"name"
|
||||
] name config
|
||||
);
|
||||
tags = lib.attrByPath [
|
||||
"clan"
|
||||
"tags"
|
||||
] [ ] config;
|
||||
|
||||
system = lib.attrByPath [
|
||||
"nixpkgs"
|
||||
"hostSystem"
|
||||
] null config;
|
||||
}
|
||||
) machines;
|
||||
}
|
||||
|
||||
# Will be deprecated
|
||||
{
|
||||
machines = lib.mapAttrs (
|
||||
name: _:
|
||||
# Use mkForce to make sure users migrate to the inventory system.
|
||||
# When the settings.json exists the evaluation will print the deprecation warning.
|
||||
lib.mkForce {
|
||||
inherit name;
|
||||
system = (machineSettings name).nixpkgs.hostSystem or null;
|
||||
}
|
||||
) machinesDirs;
|
||||
}
|
||||
|
||||
# Deprecated interface
|
||||
(if clanName != null then { meta.name = clanName; } else { })
|
||||
(if clanIcon != null then { meta.icon = clanIcon; } else { })
|
||||
];
|
||||
}).config;
|
||||
|
||||
inherit (clan-core.lib.inventory) buildInventory;
|
||||
|
||||
# map from machine name to service configuration
|
||||
# { ${machineName} :: Config }
|
||||
serviceConfigs = buildInventory mergedInventory;
|
||||
deprecationWarnings = [
|
||||
(lib.warnIf (
|
||||
clanName != null
|
||||
) "clanName is deprecated, please use meta.name instead. ${clanName}" null)
|
||||
(lib.warnIf (clanIcon != null) "clanIcon is deprecated, please use meta.icon instead" null)
|
||||
];
|
||||
|
||||
machinesDirs = lib.optionalAttrs (builtins.pathExists "${directory}/machines") (
|
||||
builtins.readDir (directory + /machines)
|
||||
);
|
||||
|
||||
mergedMeta =
|
||||
let
|
||||
metaFromFile =
|
||||
if (builtins.pathExists "${directory}/clan/meta.json") then
|
||||
let
|
||||
settings = builtins.fromJSON (builtins.readFile "${directory}/clan/meta.json");
|
||||
in
|
||||
settings
|
||||
else
|
||||
{ };
|
||||
legacyMeta = lib.filterAttrs (_: v: v != null) {
|
||||
name = clanName;
|
||||
icon = clanIcon;
|
||||
};
|
||||
optionsMeta = lib.filterAttrs (_: v: v != null) meta;
|
||||
|
||||
warnings =
|
||||
builtins.map (
|
||||
name:
|
||||
if
|
||||
metaFromFile.${name} or null != optionsMeta.${name} or null && optionsMeta.${name} or null != null
|
||||
then
|
||||
lib.warn "meta.${name} is set in different places. (exlicit option meta.${name} overrides ${directory}/clan/meta.json)" null
|
||||
else
|
||||
null
|
||||
) (builtins.attrNames metaFromFile)
|
||||
++ [ (if (res.name or null == null) then (throw "meta.name should be set") else null) ];
|
||||
res = metaFromFile // legacyMeta // optionsMeta;
|
||||
in
|
||||
# Print out warnings before returning the merged result
|
||||
builtins.deepSeq warnings res;
|
||||
|
||||
machineSettings =
|
||||
machineName:
|
||||
let
|
||||
warn = lib.warn ''
|
||||
Usage of Settings.json is only supported for test compatibility.
|
||||
!!! Consider using the inventory system. !!!
|
||||
|
||||
File: ${directory + /machines/${machineName}/settings.json}
|
||||
|
||||
If there are still features missing in the inventory system, please open an issue on the clan-core repository.
|
||||
'';
|
||||
in
|
||||
# CLAN_MACHINE_SETTINGS_FILE allows to override the settings file temporarily
|
||||
# This is useful for doing a dry-run before writing changes into the settings.json
|
||||
# Using CLAN_MACHINE_SETTINGS_FILE requires passing --impure to nix eval
|
||||
if builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE" != "" then
|
||||
warn (builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE")))
|
||||
builtins.fromJSON (builtins.readFile (builtins.getEnv "CLAN_MACHINE_SETTINGS_FILE"))
|
||||
else
|
||||
lib.optionalAttrs (builtins.pathExists "${directory}/machines/${machineName}/settings.json") (
|
||||
warn (builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json)))
|
||||
builtins.fromJSON (builtins.readFile (directory + /machines/${machineName}/settings.json))
|
||||
);
|
||||
|
||||
# Read additional imports specified via a config option in settings.json
|
||||
# This is not an infinite recursion, because the imports are discovered here
|
||||
# before calling evalModules.
|
||||
# It is still useful to have the imports as an option, as this allows for type
|
||||
# checking and easy integration with the config frontend(s)
|
||||
machineImports =
|
||||
machineSettings: map (module: clan-core.clanModules.${module}) (machineSettings.clanImports or [ ]);
|
||||
|
||||
deprecationWarnings = [
|
||||
(lib.warnIf (
|
||||
clanName != null
|
||||
) "clanName in buildClan is deprecated, please use meta.name instead." null)
|
||||
(lib.warnIf (clanIcon != null) "clanIcon is deprecated, please use meta.icon instead" null)
|
||||
];
|
||||
|
||||
# TODO: remove default system once we have a hardware-config mechanism
|
||||
nixosConfiguration =
|
||||
{
|
||||
|
@ -156,9 +98,6 @@ let
|
|||
clan-core.nixosModules.clanCore
|
||||
extraConfig
|
||||
(machines.${name} or { })
|
||||
# Inherit the inventory assertions ?
|
||||
{ inherit (mergedInventory) assertions; }
|
||||
{ imports = serviceConfigs.${name} or { }; }
|
||||
(
|
||||
{
|
||||
# Settings
|
||||
|
@ -186,7 +125,7 @@ let
|
|||
} // specialArgs;
|
||||
};
|
||||
|
||||
allMachines = mergedInventory.machines or { };
|
||||
allMachines = machinesDirs // machines;
|
||||
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
|
@ -238,13 +177,9 @@ builtins.deepSeq deprecationWarnings {
|
|||
inherit nixosConfigurations;
|
||||
|
||||
clanInternals = {
|
||||
inherit (clan-core) clanModules;
|
||||
source = "${clan-core}";
|
||||
|
||||
meta = mergedInventory.meta;
|
||||
inventory = mergedInventory;
|
||||
|
||||
inventoryFile = "${directory}/inventory.json";
|
||||
# Evaluated clan meta
|
||||
# Merged /clan/meta.json with overrides from buildClan
|
||||
meta = mergedMeta;
|
||||
|
||||
# machine specifics
|
||||
machines = configsPerSystem;
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
...
|
||||
}:
|
||||
{
|
||||
evalClanModules = import ./eval-clan-modules { inherit clan-core nixpkgs lib; };
|
||||
buildClan = import ./build-clan { inherit clan-core lib nixpkgs; };
|
||||
facts = import ./facts.nix { inherit lib; };
|
||||
inventory = import ./inventory { inherit lib clan-core; };
|
||||
jsonschema = import ./jsonschema { inherit lib; };
|
||||
modules = import ./description.nix { inherit clan-core lib; };
|
||||
buildClan = import ./build-clan { inherit clan-core lib nixpkgs; };
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{ clan-core, lib }:
|
||||
{ lib, clan-core, ... }:
|
||||
|
||||
rec {
|
||||
getReadme =
|
||||
modulename:
|
||||
|
@ -15,20 +16,18 @@ rec {
|
|||
getShortDescription =
|
||||
modulename:
|
||||
let
|
||||
content = getReadme modulename;
|
||||
content = (getReadme modulename);
|
||||
parts = lib.splitString "---" content;
|
||||
# Partition the parts into the first part (the readme content) and the rest (the metadata)
|
||||
parsed = builtins.partition ({ index, ... }: if index >= 2 then false else true) (
|
||||
lib.filter ({ index, ... }: index != 0) (lib.imap0 (index: part: { inherit index part; }) parts)
|
||||
);
|
||||
|
||||
# Use this if the content is needed
|
||||
# readmeContent = lib.concatMapStrings (v: "---" + v.part) parsed.wrong;
|
||||
|
||||
meta = builtins.fromTOML (builtins.head parsed.right).part;
|
||||
description = builtins.head parts;
|
||||
number_of_newlines = builtins.length (lib.splitString "\n" description);
|
||||
in
|
||||
if (builtins.length parts >= 3) then
|
||||
meta.description
|
||||
if (builtins.length parts) > 1 then
|
||||
if number_of_newlines > 4 then
|
||||
throw "Short description in README.md for module ${modulename} is too long. Max 3 newlines."
|
||||
else if number_of_newlines <= 1 then
|
||||
throw "Missing short description in README.md for module ${modulename}."
|
||||
else
|
||||
description
|
||||
else
|
||||
throw "Short description delimiter `---` not found in README.md for module ${modulename}";
|
||||
}
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
nixpkgs,
|
||||
clan-core,
|
||||
lib,
|
||||
}:
|
||||
let
|
||||
inherit (import nixpkgs { system = "x86_64-linux"; }) pkgs;
|
||||
|
||||
inherit (clan-core) clanModules;
|
||||
|
||||
baseModule = {
|
||||
imports = (import (pkgs.path + "/nixos/modules/module-list.nix")) ++ [
|
||||
{
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
clan.core.clanName = "dummy";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# This function takes a list of module names and evaluates them
|
||||
# evalClanModules :: [ String ] -> { config, options, ... }
|
||||
evalClanModules =
|
||||
modulenames:
|
||||
let
|
||||
evaled = lib.evalModules {
|
||||
modules = [
|
||||
baseModule
|
||||
clan-core.nixosModules.clanCore
|
||||
] ++ (map (name: clanModules.${name}) modulenames);
|
||||
};
|
||||
in
|
||||
evaled;
|
||||
in
|
||||
evalClanModules
|
|
@ -1,71 +0,0 @@
|
|||
{ lib, ... }:
|
||||
machineDir:
|
||||
let
|
||||
|
||||
allMachineNames = lib.mapAttrsToList (name: _: name) (builtins.readDir machineDir);
|
||||
|
||||
getFactPath = fact: machine: "${machineDir}/${machine}/facts/${fact}";
|
||||
|
||||
readFact =
|
||||
fact: machine:
|
||||
let
|
||||
path = getFactPath fact machine;
|
||||
in
|
||||
if builtins.pathExists path then builtins.readFile path else null;
|
||||
|
||||
# Example:
|
||||
#
|
||||
# readFactFromAllMachines zerotier-ip
|
||||
# => {
|
||||
# machineA = "1.2.3.4";
|
||||
# machineB = "5.6.7.8";
|
||||
# };
|
||||
readFactFromAllMachines =
|
||||
fact:
|
||||
let
|
||||
machines = allMachineNames;
|
||||
facts = lib.genAttrs machines (readFact fact);
|
||||
filteredFacts = lib.filterAttrs (_machine: fact: fact != null) facts;
|
||||
in
|
||||
filteredFacts;
|
||||
|
||||
# all given facts are are set and factvalues are never null.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# readFactsFromAllMachines [ "zerotier-ip" "syncthing.pub" ]
|
||||
# => {
|
||||
# machineA =
|
||||
# {
|
||||
# "zerotier-ip" = "1.2.3.4";
|
||||
# "synching.pub" = "1234";
|
||||
# };
|
||||
# machineB =
|
||||
# {
|
||||
# "zerotier-ip" = "5.6.7.8";
|
||||
# "synching.pub" = "23456719";
|
||||
# };
|
||||
# };
|
||||
readFactsFromAllMachines =
|
||||
facts:
|
||||
let
|
||||
# machine -> fact -> factvalue
|
||||
machinesFactsAttrs = lib.genAttrs allMachineNames (
|
||||
machine: lib.genAttrs facts (fact: readFact fact machine)
|
||||
);
|
||||
# remove all machines which don't have all facts set
|
||||
filteredMachineFactAttrs = lib.filterAttrs (
|
||||
_machine: values: builtins.all (fact: values.${fact} != null) facts
|
||||
) machinesFactsAttrs;
|
||||
in
|
||||
filteredMachineFactAttrs;
|
||||
in
|
||||
{
|
||||
inherit
|
||||
allMachineNames
|
||||
getFactPath
|
||||
readFact
|
||||
readFactFromAllMachines
|
||||
readFactsFromAllMachines
|
||||
;
|
||||
}
|
|
@ -5,12 +5,9 @@
|
|||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
./jsonschema/flake-module.nix
|
||||
./inventory/flake-module.nix
|
||||
];
|
||||
imports = [ ./jsonschema/flake-module.nix ];
|
||||
flake.lib = import ./default.nix {
|
||||
inherit lib inputs;
|
||||
inherit lib;
|
||||
inherit (inputs) nixpkgs;
|
||||
clan-core = self;
|
||||
};
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
# Inventory
|
||||
|
||||
The inventory is our concept for distributed services. Users can configure multiple machines with minimal effort.
|
||||
|
||||
- The inventory acts as a declarative source of truth for all machine configurations.
|
||||
- Users can easily add or remove machines to/from services.
|
||||
- Ensures that all machines and services are configured consistently, across multiple nixosConfigs.
|
||||
- Defaults and predefined roles in our modules minimizes the need for manual configuration.
|
||||
|
||||
Open questions:
|
||||
|
||||
- [ ] How do we set default role, description and other metadata?
|
||||
- It must be accessible from Python.
|
||||
- It must set the value in the module system.
|
||||
|
||||
- [ ] Inventory might use assertions. Should each machine inherit the inventory assertions ?
|
||||
|
||||
- [ ] Is the service config interface the same as the module config interface ?
|
||||
|
||||
- [ ] As a user do I want to see borgbackup as the high level category?
|
||||
|
||||
|
||||
Architecture
|
||||
|
||||
```
|
||||
nixosConfig < machine_module < inventory
|
||||
---------------------------------------------
|
||||
nixos < borgbackup <- inventory <-> UI
|
||||
|
||||
creates the config Maps from high level services to the borgbackup clan module
|
||||
for ONE machine Inventory is completely serializable.
|
||||
UI can interact with the inventory to define machines, and services
|
||||
Defining Users is out of scope for the first prototype.
|
||||
```
|
||||
|
||||
## Provides a specification for the inventory
|
||||
|
||||
It is used for design phase and as validation helper.
|
||||
|
||||
> Cue is less verbose and easier to understand and maintain than json-schema.
|
||||
> Json-schema, if needed can be easily generated on-the fly.
|
||||
|
||||
## Checking validity
|
||||
|
||||
Directly check a json against the schema
|
||||
|
||||
`cue vet inventory.json root.cue -d '#Root'`
|
||||
|
||||
## Json schema
|
||||
|
||||
Export the json-schema i.e. for usage in python / javascript / nix
|
||||
|
||||
`cue export --out openapi root.cue`
|
||||
|
||||
## Usage
|
||||
|
||||
Comments are rendered as descriptions in the json schema.
|
||||
|
||||
```cue
|
||||
// A name of the clan (primarily shown by the UI)
|
||||
name: string
|
||||
```
|
||||
|
||||
Cue open sets. In the following `foo = {...}` means that the key `foo` can contain any arbitrary json object.
|
||||
|
||||
```cue
|
||||
foo: { ... }
|
||||
```
|
||||
|
||||
Cue dynamic keys.
|
||||
|
||||
```cue
|
||||
[string]: {
|
||||
attr: string
|
||||
}
|
||||
```
|
||||
|
||||
This is the schema of
|
||||
|
||||
```json
|
||||
{
|
||||
"a": {
|
||||
"attr": "foo"
|
||||
},
|
||||
"b": {
|
||||
"attr": "bar"
|
||||
}
|
||||
// ... Indefinitely more dynamic keys of type "string"
|
||||
}
|
||||
```
|
|
@ -1,117 +0,0 @@
|
|||
# Generate partial NixOS configurations for every machine in the inventory
|
||||
# This function is responsible for generating the module configuration for every machine in the inventory.
|
||||
{ lib, clan-core }:
|
||||
inventory:
|
||||
let
|
||||
machines = machinesFromInventory inventory;
|
||||
|
||||
resolveTags =
|
||||
# Inventory, { machines :: [string], tags :: [string] }
|
||||
inventory: members: {
|
||||
machines =
|
||||
members.machines or [ ]
|
||||
++ (builtins.foldl' (
|
||||
acc: tag:
|
||||
let
|
||||
# For error printing
|
||||
availableTags = lib.foldlAttrs (
|
||||
acc: _: v:
|
||||
v.tags or [ ] ++ acc
|
||||
) [ ] inventory.machines;
|
||||
|
||||
tagMembers = builtins.attrNames (
|
||||
lib.filterAttrs (_n: v: builtins.elem tag v.tags or [ ]) inventory.machines
|
||||
);
|
||||
in
|
||||
if tagMembers == [ ] then
|
||||
throw "Tag: '${tag}' not found. Available tags: ${builtins.toJSON (lib.unique availableTags)}"
|
||||
else
|
||||
acc ++ tagMembers
|
||||
) [ ] members.tags or [ ]);
|
||||
};
|
||||
|
||||
/*
|
||||
Returns a NixOS configuration for every machine in the inventory.
|
||||
|
||||
machinesFromInventory :: Inventory -> { ${machine_name} :: NixOSConfiguration }
|
||||
*/
|
||||
machinesFromInventory =
|
||||
inventory:
|
||||
# For every machine in the inventory, build a NixOS configuration
|
||||
# For each machine generate config, forEach service, if the machine is used.
|
||||
builtins.mapAttrs (
|
||||
machineName: machineConfig:
|
||||
lib.foldlAttrs (
|
||||
# [ Modules ], String, { ${instance_name} :: ServiceConfig }
|
||||
acc: moduleName: serviceConfigs:
|
||||
acc
|
||||
# Collect service config
|
||||
++ (lib.foldlAttrs (
|
||||
# [ Modules ], String, ServiceConfig
|
||||
acc2: instanceName: serviceConfig:
|
||||
let
|
||||
resolvedRoles = builtins.mapAttrs (
|
||||
_roleName: members: resolveTags inventory members
|
||||
) serviceConfig.roles;
|
||||
|
||||
isInService = builtins.any (members: builtins.elem machineName members.machines) (
|
||||
builtins.attrValues resolvedRoles
|
||||
);
|
||||
|
||||
# Inverse map of roles. Allows for easy lookup of roles for a given machine.
|
||||
# { ${machine_name} :: [roles]
|
||||
inverseRoles = lib.foldlAttrs (
|
||||
acc: roleName:
|
||||
{ machines }:
|
||||
acc
|
||||
// builtins.foldl' (
|
||||
acc2: machineName: acc2 // { ${machineName} = (acc.${machineName} or [ ]) ++ [ roleName ]; }
|
||||
) { } machines
|
||||
) { } resolvedRoles;
|
||||
|
||||
machineServiceConfig = (serviceConfig.machines.${machineName} or { }).config or { };
|
||||
globalConfig = serviceConfig.config or { };
|
||||
|
||||
# TODO: maybe optimize this dont lookup the role in inverse roles. Imports are not lazy
|
||||
roleModules = builtins.map (
|
||||
role:
|
||||
let
|
||||
path = "${clan-core.clanModules.${moduleName}}/roles/${role}.nix";
|
||||
in
|
||||
if builtins.pathExists path then
|
||||
path
|
||||
else
|
||||
throw "Module doesn't have role: '${role}'. Path: ${path} not found."
|
||||
) inverseRoles.${machineName} or [ ];
|
||||
in
|
||||
if isInService then
|
||||
acc2
|
||||
++ [
|
||||
{
|
||||
imports = [ clan-core.clanModules.${moduleName} ] ++ roleModules;
|
||||
config.clan.${moduleName} = lib.mkMerge [
|
||||
globalConfig
|
||||
machineServiceConfig
|
||||
];
|
||||
}
|
||||
{
|
||||
config.clan.inventory.services.${moduleName}.${instanceName} = {
|
||||
roles = resolvedRoles;
|
||||
# TODO: Add inverseRoles to the service config if needed
|
||||
# inherit inverseRoles;
|
||||
};
|
||||
}
|
||||
]
|
||||
else
|
||||
acc2
|
||||
) [ ] serviceConfigs)
|
||||
) [ ] inventory.services
|
||||
# Append each machine config
|
||||
++ [
|
||||
(lib.optionalAttrs (machineConfig.system or null != null) {
|
||||
config.nixpkgs.hostPlatform = machineConfig.system;
|
||||
})
|
||||
]
|
||||
) inventory.machines or { };
|
||||
in
|
||||
machines
|
|
@ -1,136 +0,0 @@
|
|||
{ config, lib, ... }:
|
||||
let
|
||||
t = lib.types;
|
||||
|
||||
metaOptions = {
|
||||
name = lib.mkOption { type = t.str; };
|
||||
description = lib.mkOption {
|
||||
default = null;
|
||||
type = t.nullOr t.str;
|
||||
};
|
||||
icon = lib.mkOption {
|
||||
default = null;
|
||||
type = t.nullOr t.str;
|
||||
};
|
||||
};
|
||||
|
||||
machineRef = lib.mkOptionType {
|
||||
name = "machineRef";
|
||||
description = "Machine :: [${builtins.concatStringsSep " | " (builtins.attrNames config.machines)}]";
|
||||
check = v: lib.isString v && builtins.elem v (builtins.attrNames config.machines);
|
||||
merge = lib.mergeEqualOption;
|
||||
};
|
||||
|
||||
allTags = lib.unique (
|
||||
lib.foldlAttrs (
|
||||
tags: _: m:
|
||||
tags ++ m.tags or [ ]
|
||||
) [ ] config.machines
|
||||
);
|
||||
|
||||
tagRef = lib.mkOptionType {
|
||||
name = "tagRef";
|
||||
description = "Tags :: [${builtins.concatStringsSep " | " allTags}]";
|
||||
check = v: lib.isString v && builtins.elem v allTags;
|
||||
merge = lib.mergeEqualOption;
|
||||
};
|
||||
in
|
||||
{
|
||||
options.assertions = lib.mkOption {
|
||||
type = t.listOf t.unspecified;
|
||||
internal = true;
|
||||
default = [ ];
|
||||
};
|
||||
config.assertions =
|
||||
let
|
||||
serviceAssertions = lib.foldlAttrs (
|
||||
ass1: serviceName: c:
|
||||
ass1
|
||||
++ lib.foldlAttrs (
|
||||
ass2: instanceName: instanceConfig:
|
||||
let
|
||||
serviceMachineNames = lib.attrNames instanceConfig.machines;
|
||||
topLevelMachines = lib.attrNames config.machines;
|
||||
# All machines must be defined in the top-level machines
|
||||
assertions = builtins.map (m: {
|
||||
assertion = builtins.elem m topLevelMachines;
|
||||
message = "${serviceName}.${instanceName}.machines.${m}. Should be one of [ ${builtins.concatStringsSep " | " topLevelMachines} ]";
|
||||
}) serviceMachineNames;
|
||||
in
|
||||
ass2 ++ assertions
|
||||
) [ ] c
|
||||
) [ ] config.services;
|
||||
machineAssertions = map (
|
||||
{ name, value }:
|
||||
{
|
||||
assertion = true;
|
||||
message = "Machine ${name} should define its host system in the inventory. ()";
|
||||
}
|
||||
) (lib.attrsToList (lib.filterAttrs (_n: v: v.system or null == null) config.machines));
|
||||
in
|
||||
machineAssertions ++ serviceAssertions;
|
||||
|
||||
options.meta = metaOptions;
|
||||
|
||||
options.machines = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.submodule {
|
||||
options = {
|
||||
inherit (metaOptions) name description icon;
|
||||
tags = lib.mkOption {
|
||||
default = [ ];
|
||||
apply = lib.unique;
|
||||
type = t.listOf t.str;
|
||||
};
|
||||
system = lib.mkOption {
|
||||
default = null;
|
||||
type = t.nullOr t.str;
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
options.services = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.attrsOf (
|
||||
t.submodule {
|
||||
options.meta = metaOptions;
|
||||
options.config = lib.mkOption {
|
||||
default = { };
|
||||
type = t.anything;
|
||||
};
|
||||
options.machines = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.submodule {
|
||||
options.config = lib.mkOption {
|
||||
default = { };
|
||||
type = t.anything;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
options.roles = lib.mkOption {
|
||||
default = { };
|
||||
type = t.attrsOf (
|
||||
t.submodule {
|
||||
options.machines = lib.mkOption {
|
||||
default = [ ];
|
||||
type = t.listOf machineRef;
|
||||
};
|
||||
options.tags = lib.mkOption {
|
||||
default = [ ];
|
||||
apply = lib.unique;
|
||||
type = t.listOf tagRef;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
{ lib, clan-core }:
|
||||
{
|
||||
buildInventory = import ./build-inventory { inherit lib clan-core; };
|
||||
interface = ./build-inventory/interface.nix;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
{ self, ... }:
|
||||
self.lib.buildClan {
|
||||
# Name of the clan in the UI, should be unique
|
||||
meta.name = "Inventory clan";
|
||||
|
||||
# Should usually point to the directory of flake.nix
|
||||
directory = self;
|
||||
|
||||
inventory = {
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.server.machines = [ "backup_server" ];
|
||||
roles.client.tags = [ "backup" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# merged with
|
||||
machines = {
|
||||
"backup_server" = {
|
||||
clan.tags = [ "all" ];
|
||||
# ... rest of the machine config
|
||||
};
|
||||
"client_1_machine" = {
|
||||
clan.tags = [
|
||||
"all"
|
||||
"backup"
|
||||
];
|
||||
};
|
||||
"client_2_machine" = {
|
||||
clan.tags = [
|
||||
"all"
|
||||
"backup"
|
||||
];
|
||||
# Name of the machine in the UI
|
||||
clan.meta.name = "camina";
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
{
|
||||
"machines": {
|
||||
"camina_machine": {
|
||||
"name": "camina",
|
||||
"tags": ["laptop"]
|
||||
},
|
||||
"vyr_machine": {
|
||||
"name": "vyr"
|
||||
},
|
||||
"vi_machine": {
|
||||
"name": "vi",
|
||||
"tags": ["laptop"]
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"name": "kenjis clan"
|
||||
},
|
||||
"services": {
|
||||
"borgbackup": {
|
||||
"instance_1": {
|
||||
"meta": {
|
||||
"name": "My backup"
|
||||
},
|
||||
"roles": {
|
||||
"server": {
|
||||
"machines": ["vyr_machine"]
|
||||
},
|
||||
"client": {
|
||||
"machines": ["vyr_machine"],
|
||||
"tags": ["laptop"]
|
||||
}
|
||||
},
|
||||
"machines": {},
|
||||
"config": {}
|
||||
},
|
||||
"instance_2": {
|
||||
"meta": {
|
||||
"name": "My backup"
|
||||
},
|
||||
"roles": {
|
||||
"server": {
|
||||
"machines": ["vi_machine"]
|
||||
},
|
||||
"client": {
|
||||
"machines": ["camina_machine"]
|
||||
}
|
||||
},
|
||||
"machines": {},
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
{
|
||||
"machines": {
|
||||
"camina_machine": {
|
||||
"name": "camina"
|
||||
},
|
||||
"vyr_machine": {
|
||||
"name": "vyr"
|
||||
},
|
||||
"vi_machine": {
|
||||
"name": "vi"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"name": "kenjis clan"
|
||||
},
|
||||
"services": {
|
||||
"syncthing": {
|
||||
"instance_1": {
|
||||
"meta": {
|
||||
"name": "My sync"
|
||||
},
|
||||
"roles": {
|
||||
"peer": {
|
||||
"machines": ["vyr_machine", "vi_machine", "camina_machine"]
|
||||
}
|
||||
},
|
||||
"machines": {},
|
||||
"config": {
|
||||
"folders": {
|
||||
"test": {
|
||||
"path": "~/data/docs",
|
||||
"devices": ["camina_machine", "vyr_machine", "vi_machine"]
|
||||
},
|
||||
"videos": {
|
||||
"path": "~/data/videos",
|
||||
"devices": ["camina_machine", "vyr_machine"]
|
||||
},
|
||||
"playlist": {
|
||||
"path": "~/data/playlist",
|
||||
"devices": ["camina_machine", "vi_machine"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"machines": {
|
||||
"camina_machine": {
|
||||
"name": "camina"
|
||||
},
|
||||
"vyr_machine": {
|
||||
"name": "vyr"
|
||||
},
|
||||
"vi_machine": {
|
||||
"name": "vi"
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"name": "kenjis clan"
|
||||
},
|
||||
"services": {
|
||||
"zerotier": {
|
||||
"instance_1": {
|
||||
"meta": {
|
||||
"name": "My Network"
|
||||
},
|
||||
"roles": {
|
||||
"controller": { "machines": ["vyr_machine"] },
|
||||
"moon": { "machines": ["vyr_machine"] },
|
||||
"peer": { "machines": ["vi_machine", "camina_machine"] }
|
||||
},
|
||||
"machines": {
|
||||
"vyr_machine": {
|
||||
"config": {}
|
||||
}
|
||||
},
|
||||
"config": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
{ self, inputs, ... }:
|
||||
let
|
||||
inputOverrides = builtins.concatStringsSep " " (
|
||||
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
|
||||
);
|
||||
in
|
||||
{
|
||||
flake.inventory = import ./example.nix { inherit self; };
|
||||
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
system,
|
||||
self',
|
||||
...
|
||||
}:
|
||||
let
|
||||
buildInventory = import ./build-inventory {
|
||||
clan-core = self;
|
||||
inherit lib;
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.inventory-schema = pkgs.mkShell {
|
||||
inputsFrom = with config.checks; [
|
||||
lib-inventory-schema
|
||||
lib-inventory-eval
|
||||
self'.devShells.default
|
||||
];
|
||||
};
|
||||
|
||||
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
|
||||
legacyPackages.evalTests-inventory = import ./tests {
|
||||
inherit buildInventory;
|
||||
clan-core = self;
|
||||
};
|
||||
|
||||
checks = {
|
||||
lib-inventory-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
|
||||
export HOME="$(realpath .)"
|
||||
|
||||
nix-unit --eval-store "$HOME" \
|
||||
--extra-experimental-features flakes \
|
||||
${inputOverrides} \
|
||||
--flake ${self}#legacyPackages.${system}.evalTests-inventory
|
||||
|
||||
touch $out
|
||||
'';
|
||||
|
||||
lib-inventory-schema = pkgs.stdenv.mkDerivation {
|
||||
name = "inventory-schema-checks";
|
||||
src = ./.;
|
||||
buildInputs = [ pkgs.cue ];
|
||||
buildPhase = ''
|
||||
echo "Running inventory tests..."
|
||||
# Cue is easier to run in the same directory as the schema
|
||||
cd spec
|
||||
|
||||
echo "Export cue as json-schema..."
|
||||
cue export --out openapi root.cue
|
||||
|
||||
echo "Validate test/*.json against inventory-schema..."
|
||||
|
||||
test_dir="../examples"
|
||||
for file in "$test_dir"/*; do
|
||||
# Check if the item is a file
|
||||
if [ -f "$file" ]; then
|
||||
# Print the filename
|
||||
echo "Running test on: $file"
|
||||
|
||||
# Run the cue vet command
|
||||
cue vet "$file" root.cue -d "#Root"
|
||||
fi
|
||||
done
|
||||
|
||||
touch $out
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
{ buildInventory, clan-core, ... }:
|
||||
{
|
||||
test_inventory_empty = {
|
||||
# Empty inventory should return an empty module
|
||||
expr = buildInventory { };
|
||||
expected = { };
|
||||
};
|
||||
test_inventory_role_imports =
|
||||
let
|
||||
configs = buildInventory {
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.server.machines = [ "backup_server" ];
|
||||
roles.client.machines = [
|
||||
"client_1_machine"
|
||||
"client_2_machine"
|
||||
];
|
||||
};
|
||||
};
|
||||
machines = {
|
||||
"backup_server" = { };
|
||||
"client_1_machine" = { };
|
||||
"client_2_machine" = { };
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
expr = {
|
||||
server_imports = (builtins.head configs."backup_server").imports;
|
||||
client_1_imports = (builtins.head configs."client_1_machine").imports;
|
||||
client_2_imports = (builtins.head configs."client_2_machine").imports;
|
||||
};
|
||||
|
||||
expected = {
|
||||
server_imports = [
|
||||
clan-core.clanModules.borgbackup
|
||||
"${clan-core.clanModules.borgbackup}/roles/server.nix"
|
||||
];
|
||||
client_1_imports = [
|
||||
clan-core.clanModules.borgbackup
|
||||
"${clan-core.clanModules.borgbackup}/roles/client.nix"
|
||||
];
|
||||
client_2_imports = [
|
||||
clan-core.clanModules.borgbackup
|
||||
"${clan-core.clanModules.borgbackup}/roles/client.nix"
|
||||
];
|
||||
};
|
||||
};
|
||||
test_inventory_tag_resolve =
|
||||
let
|
||||
configs = buildInventory {
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.client.tags = [ "backup" ];
|
||||
};
|
||||
};
|
||||
machines = {
|
||||
"not_used_machine" = { };
|
||||
"client_1_machine" = {
|
||||
tags = [ "backup" ];
|
||||
};
|
||||
"client_2_machine" = {
|
||||
tags = [ "backup" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
expr = {
|
||||
# A machine that includes the backup service should have 3 imports
|
||||
# - one for some service agnostic properties of the machine itself
|
||||
# - One for the service itself (default.nix)
|
||||
# - one for the role (roles/client.nix)
|
||||
client_1_machine = builtins.length configs.client_1_machine;
|
||||
client_2_machine = builtins.length configs.client_2_machine;
|
||||
not_used_machine = builtins.length configs.not_used_machine;
|
||||
};
|
||||
expected = {
|
||||
client_1_machine = 3;
|
||||
client_2_machine = 3;
|
||||
not_used_machine = 1;
|
||||
};
|
||||
};
|
||||
|
||||
test_inventory_multiple_roles =
|
||||
let
|
||||
configs = buildInventory {
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.client.machines = [ "machine_1" ];
|
||||
roles.server.machines = [ "machine_1" ];
|
||||
};
|
||||
};
|
||||
machines = {
|
||||
"machine_1" = { };
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
expr = {
|
||||
machine_1_imports = (builtins.head configs."machine_1").imports;
|
||||
};
|
||||
expected = {
|
||||
machine_1_imports = [
|
||||
clan-core.clanModules.borgbackup
|
||||
"${clan-core.clanModules.borgbackup}/roles/client.nix"
|
||||
"${clan-core.clanModules.borgbackup}/roles/server.nix"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
test_inventory_role_doesnt_exist =
|
||||
let
|
||||
configs = buildInventory {
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.roleXYZ.machines = [ "machine_1" ];
|
||||
};
|
||||
};
|
||||
machines = {
|
||||
"machine_1" = { };
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
expr = configs;
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = "Module doesn't have role.*";
|
||||
};
|
||||
};
|
||||
test_inventory_tag_doesnt_exist =
|
||||
let
|
||||
configs = buildInventory {
|
||||
services = {
|
||||
borgbackup.instance_1 = {
|
||||
roles.client.machines = [ "machine_1" ];
|
||||
roles.client.tags = [ "tagXYZ" ];
|
||||
};
|
||||
};
|
||||
machines = {
|
||||
"machine_1" = {
|
||||
tags = [ "tagABC" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
expr = configs;
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = "Tag: '\\w+' not found";
|
||||
};
|
||||
};
|
||||
}
|
|
@ -47,7 +47,7 @@ rec {
|
|||
let
|
||||
evaled = lib.evalModules { modules = [ module ]; };
|
||||
in
|
||||
{ "$schema" = "http://json-schema.org/draft-07/schema#"; } // parseOptions evaled.options;
|
||||
parseOptions evaled.options;
|
||||
|
||||
# parses a set of evaluated nixos options to a jsonschema
|
||||
parseOptions =
|
||||
|
@ -66,7 +66,6 @@ rec {
|
|||
// {
|
||||
type = "object";
|
||||
inherit properties;
|
||||
additionalProperties = false;
|
||||
};
|
||||
|
||||
# parses and evaluated nixos option to a jsonschema property definition
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
|
@ -40,7 +38,6 @@
|
|||
},
|
||||
"services": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"opt": {
|
||||
"type": "string",
|
||||
|
@ -62,8 +59,9 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["repo"],
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"repo"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"default": {},
|
||||
|
|
|
@ -278,7 +278,6 @@ in
|
|||
expr = slib.parseOption (evalType (lib.types.submodule subModule) { });
|
||||
expected = {
|
||||
type = "object";
|
||||
additionalProperties = false;
|
||||
properties = {
|
||||
opt = {
|
||||
type = "boolean";
|
||||
|
@ -302,7 +301,6 @@ in
|
|||
expr = slib.parseOption (evalType (lib.types.submodule subModule) { });
|
||||
expected = {
|
||||
type = "object";
|
||||
additionalProperties = false;
|
||||
properties = {
|
||||
opt = {
|
||||
type = "boolean";
|
||||
|
@ -333,7 +331,6 @@ in
|
|||
type = "object";
|
||||
additionalProperties = {
|
||||
type = "object";
|
||||
additionalProperties = false;
|
||||
properties = {
|
||||
opt = {
|
||||
type = "boolean";
|
||||
|
@ -366,7 +363,6 @@ in
|
|||
type = "array";
|
||||
items = {
|
||||
type = "object";
|
||||
additionalProperties = false;
|
||||
properties = {
|
||||
opt = {
|
||||
type = "boolean";
|
||||
|
|
|
@ -4,9 +4,16 @@
|
|||
lib ? (import <nixpkgs> { }).lib,
|
||||
slib ? import ./. { inherit lib; },
|
||||
}:
|
||||
let
|
||||
evaledOptions =
|
||||
let
|
||||
evaledConfig = lib.evalModules { modules = [ ./example-interface.nix ]; };
|
||||
in
|
||||
evaledConfig.options;
|
||||
in
|
||||
{
|
||||
testParseOptions = {
|
||||
expr = slib.parseModule ./example-interface.nix;
|
||||
expr = slib.parseOptions evaledOptions;
|
||||
expected = builtins.fromJSON (builtins.readFile ./example-schema.json);
|
||||
};
|
||||
|
||||
|
@ -19,10 +26,8 @@
|
|||
{
|
||||
expr = slib.parseOptions evaled.options;
|
||||
expected = {
|
||||
additionalProperties = false;
|
||||
properties = {
|
||||
foo = {
|
||||
additionalProperties = false;
|
||||
properties = {
|
||||
bar = {
|
||||
type = "boolean";
|
||||
|
|
|
@ -14,9 +14,5 @@
|
|||
./vm.nix
|
||||
./wayland-proxy-virtwl.nix
|
||||
./zerotier
|
||||
# Inventory
|
||||
./inventory/interface.nix
|
||||
./meta/interface.nix
|
||||
./vars
|
||||
];
|
||||
}
|
||||
|
|
|
@ -67,19 +67,9 @@
|
|||
publicDirectory = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
The directory where public facts are stored.
|
||||
'';
|
||||
};
|
||||
|
||||
services = lib.mkOption {
|
||||
description = ''
|
||||
Services to generate secrets and facts for.
|
||||
Each service can have a generator script which generates the secrets and facts.
|
||||
The generator script is expected to generate all secrets and facts defined for this service.
|
||||
|
||||
A `service` does not need to ba analogous to a systemd service, it can be any group of facts and secrets that need to be generated together.
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (service: {
|
||||
|
@ -92,9 +82,6 @@
|
|||
'';
|
||||
};
|
||||
generator = lib.mkOption {
|
||||
description = ''
|
||||
The generator to generate the secrets and facts for this service.
|
||||
'';
|
||||
type = lib.types.submodule (
|
||||
{ config, ... }:
|
||||
{
|
||||
|
@ -119,9 +106,8 @@
|
|||
description = ''
|
||||
Shell script snippet to generate the secrets and facts.
|
||||
The script has access to the following environment variables:
|
||||
- prompt_value: prompted value in case a prompt was defined
|
||||
- facts: path to a directory where facts can be stored
|
||||
- secrets: path to a directory where secrets can be stored
|
||||
- facts: path to a directory where facts can be stored
|
||||
- secrets: path to a directory where secrets can be stored
|
||||
The script is expected to generate all secrets and facts defined for this service.
|
||||
'';
|
||||
};
|
||||
|
@ -135,27 +121,26 @@
|
|||
|
||||
export PATH="${lib.makeBinPath config.path}:${pkgs.coreutils}/bin"
|
||||
|
||||
${lib.optionalString (pkgs.stdenv.hostPlatform.isLinux) ''
|
||||
# prepare sandbox user on platforms where this is supported
|
||||
mkdir -p /etc
|
||||
# prepare sandbox user
|
||||
mkdir -p /etc
|
||||
|
||||
cat > /etc/group <<EOF
|
||||
root:x:0:
|
||||
nixbld:!:$(id -g):
|
||||
nogroup:x:65534:
|
||||
EOF
|
||||
cat > /etc/group <<EOF
|
||||
root:x:0:
|
||||
nixbld:!:$(id -g):
|
||||
nogroup:x:65534:
|
||||
EOF
|
||||
|
||||
cat > /etc/passwd <<EOF
|
||||
root:x:0:0:Nix build user:/build:/noshell
|
||||
nixbld:x:$(id -u):$(id -g):Nix build user:/build:/noshell
|
||||
nobody:x:65534:65534:Nobody:/:/noshell
|
||||
EOF
|
||||
cat > /etc/passwd <<EOF
|
||||
root:x:0:0:Nix build user:/build:/noshell
|
||||
nixbld:x:$(id -u):$(id -g):Nix build user:/build:/noshell
|
||||
nobody:x:65534:65534:Nobody:/:/noshell
|
||||
EOF
|
||||
|
||||
cat > /etc/hosts <<EOF
|
||||
127.0.0.1 localhost
|
||||
::1 localhost
|
||||
EOF
|
||||
|
||||
cat > /etc/hosts <<EOF
|
||||
127.0.0.1 localhost
|
||||
::1 localhost
|
||||
EOF
|
||||
''}
|
||||
${config.script}
|
||||
'';
|
||||
};
|
||||
|
@ -164,9 +149,6 @@
|
|||
);
|
||||
};
|
||||
secret = lib.mkOption {
|
||||
description = ''
|
||||
Secret facts to generate for this service.
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (secret: {
|
||||
|
@ -198,11 +180,11 @@
|
|||
};
|
||||
})
|
||||
);
|
||||
description = ''
|
||||
path where the secret is located in the filesystem
|
||||
'';
|
||||
};
|
||||
public = lib.mkOption {
|
||||
description = ''
|
||||
Public facts to generate for this service.
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (fact: {
|
||||
|
@ -224,9 +206,6 @@
|
|||
config.clan.core.clanDir + "/machines/${config.clan.core.machineName}/facts/${fact.config.name}";
|
||||
};
|
||||
value = lib.mkOption {
|
||||
description = ''
|
||||
The value of the public fact.
|
||||
'';
|
||||
defaultText = lib.literalExpression "\${config.clan.core.clanDir}/\${fact.config.path}";
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default =
|
||||
|
@ -250,5 +229,15 @@
|
|||
|
||||
./public/in_repo.nix
|
||||
./public/vm.nix
|
||||
|
||||
# (lib.mkRenamedOptionModule
|
||||
# [
|
||||
# "clanCore"
|
||||
# ]
|
||||
# [
|
||||
# "clan"
|
||||
# "core"
|
||||
# ]
|
||||
# )
|
||||
];
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
{ lib, ... }:
|
||||
let
|
||||
# {
|
||||
# roles = {
|
||||
# client = {
|
||||
# machines = [
|
||||
# "camina_machine"
|
||||
# "vi_machine"
|
||||
# ];
|
||||
# };
|
||||
# server = {
|
||||
# machines = [ "vyr_machine" ];
|
||||
# };
|
||||
# };
|
||||
# }
|
||||
instanceOptions = lib.types.submodule {
|
||||
options.roles = lib.mkOption { type = lib.types.attrsOf machinesList; };
|
||||
};
|
||||
|
||||
# {
|
||||
# machines = [
|
||||
# "camina_machine"
|
||||
# "vi_machine"
|
||||
# "vyr_machine"
|
||||
# ];
|
||||
# }
|
||||
machinesList = lib.types.submodule {
|
||||
options.machines = lib.mkOption { type = lib.types.listOf lib.types.str; };
|
||||
};
|
||||
in
|
||||
{
|
||||
options.clan.inventory.services = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.attrsOf instanceOptions);
|
||||
};
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
{ lib, ... }:
|
||||
let
|
||||
optStr = lib.types.nullOr lib.types.str;
|
||||
in
|
||||
{
|
||||
options.clan.meta.name = lib.mkOption { type = lib.types.str; };
|
||||
options.clan.meta.description = lib.mkOption { type = optStr; };
|
||||
options.clan.meta.icon = lib.mkOption { type = optStr; };
|
||||
options.clan.tags = lib.mkOption { type = lib.types.listOf lib.types.str; };
|
||||
}
|
|
@ -7,9 +7,6 @@
|
|||
{
|
||||
# interface
|
||||
options.clan.core.state = lib.mkOption {
|
||||
description = ''
|
||||
Define state directories to backup and restore
|
||||
'';
|
||||
default = { };
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
{ lib, ... }:
|
||||
{
|
||||
options.clan.core.vars = lib.mkOption {
|
||||
visible = false;
|
||||
description = ''
|
||||
Generated Variables
|
||||
|
||||
Define generators that prompt for or generate variables like facts and secrets to store, deploy, and rotate them easily.
|
||||
For example, generators can be used to:
|
||||
- prompt the user for input, like passwords or host names
|
||||
- generate secrets like private keys automatically when they are needed
|
||||
- output multiple values like private and public keys simultaneously
|
||||
'';
|
||||
type = lib.types.submoduleWith { modules = [ ./interface.nix ]; };
|
||||
};
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
{ lib, pkgs, ... }:
|
||||
let
|
||||
eval =
|
||||
module:
|
||||
(lib.evalModules {
|
||||
modules = [
|
||||
../interface.nix
|
||||
module
|
||||
];
|
||||
specialArgs.pkgs = pkgs;
|
||||
}).config;
|
||||
|
||||
usage_simple = {
|
||||
generators.my_secret = {
|
||||
files.password = { };
|
||||
files.username.secret = false;
|
||||
prompts.prompt1 = { };
|
||||
script = ''
|
||||
cp $prompts/prompt1 $files/password
|
||||
'';
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
single_file_single_prompt =
|
||||
let
|
||||
config = eval usage_simple;
|
||||
in
|
||||
{
|
||||
# files are always secret by default
|
||||
test_file_secret_by_default = {
|
||||
expr = config.generators.my_secret.files.password.secret;
|
||||
expected = true;
|
||||
};
|
||||
# secret files must not provide a value
|
||||
test_secret_value_access_raises_error = {
|
||||
expr = config.generators.my_secret.files.password.value;
|
||||
expectedError.type = "ThrownError";
|
||||
expectedError.msg = "Cannot access value of secret file";
|
||||
};
|
||||
# public values must provide a value at eval time
|
||||
test_public_value_access = {
|
||||
expr = config.generators.my_secret.files.username ? value;
|
||||
expected = true;
|
||||
};
|
||||
# both secret and public values must provide a path
|
||||
test_secret_has_path = {
|
||||
expr = config.generators.my_secret.files.password ? path;
|
||||
expected = true;
|
||||
};
|
||||
test_public_var_has_path = {
|
||||
expr = config.generators.my_secret.files.username ? path;
|
||||
expected = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Ensure that generators.imports works
|
||||
# This allows importing generators from third party projects without providing
|
||||
# them access to other settings.
|
||||
test_generator_modules =
|
||||
let
|
||||
generator_module = {
|
||||
my-generator.files.password = { };
|
||||
};
|
||||
config = eval { generators.imports = [ generator_module ]; };
|
||||
in
|
||||
{
|
||||
expr = config.generators ? my-generator;
|
||||
expected = true;
|
||||
};
|
||||
|
||||
# script can be text
|
||||
test_script_text =
|
||||
let
|
||||
config = eval {
|
||||
# imports = [ usage_simple ];
|
||||
generators.my_secret.script = ''
|
||||
echo "Hello, world!"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
expr = config.generators.my_secret.script;
|
||||
expected = "echo \"Hello, world!\"\n";
|
||||
};
|
||||
|
||||
# script can be a derivation
|
||||
test_script_writer =
|
||||
let
|
||||
config = eval {
|
||||
# imports = [ usage_simple ];
|
||||
generators.my_secret.script = derivation {
|
||||
system = pkgs.system;
|
||||
name = "my-script";
|
||||
builder = "/bin/sh";
|
||||
args = [
|
||||
"-c"
|
||||
''touch $out''
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
expr = lib.hasPrefix builtins.storeDir config.generators.my_secret.script;
|
||||
expected = true;
|
||||
};
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
{
|
||||
self,
|
||||
inputs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inputOverrides = builtins.concatStringsSep " " (
|
||||
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
|
||||
);
|
||||
in
|
||||
{
|
||||
perSystem =
|
||||
{ system, pkgs, ... }:
|
||||
{
|
||||
legacyPackages.evalTests-module-clan-vars = import ./eval-tests {
|
||||
inherit lib;
|
||||
clan-core = self;
|
||||
pkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||
};
|
||||
checks.module-clan-vars-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
|
||||
export HOME="$(realpath .)"
|
||||
|
||||
nix-unit --eval-store "$HOME" \
|
||||
--extra-experimental-features flakes \
|
||||
${inputOverrides} \
|
||||
--flake ${self}#legacyPackages.${system}.evalTests-module-clan-vars
|
||||
|
||||
touch $out
|
||||
'';
|
||||
};
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
finalScript = lib.mkOptionDefault ''
|
||||
set -eu -o pipefail
|
||||
|
||||
export PATH="${lib.makeBinPath config.runtimeInputs}:${pkgs.coreutils}/bin"
|
||||
|
||||
${lib.optionalString (pkgs.stdenv.hostPlatform.isLinux) ''
|
||||
# prepare sandbox user on platforms where this is supported
|
||||
mkdir -p /etc
|
||||
|
||||
cat > /etc/group <<EOF
|
||||
root:x:0:
|
||||
nixbld:!:$(id -g):
|
||||
nogroup:x:65534:
|
||||
EOF
|
||||
|
||||
cat > /etc/passwd <<EOF
|
||||
root:x:0:0:Nix build user:/build:/noshell
|
||||
nixbld:x:$(id -u):$(id -g):Nix build user:/build:/noshell
|
||||
nobody:x:65534:65534:Nobody:/:/noshell
|
||||
EOF
|
||||
|
||||
cat > /etc/hosts <<EOF
|
||||
127.0.0.1 localhost
|
||||
::1 localhost
|
||||
EOF
|
||||
''}
|
||||
${config.script}
|
||||
'';
|
||||
}
|
|
@ -1,147 +0,0 @@
|
|||
{ lib, ... }:
|
||||
let
|
||||
inherit (lib) mkOption;
|
||||
inherit (lib.types)
|
||||
anything
|
||||
attrsOf
|
||||
bool
|
||||
either
|
||||
enum
|
||||
listOf
|
||||
package
|
||||
path
|
||||
str
|
||||
submoduleWith
|
||||
;
|
||||
# the original types.submodule has strange behavior
|
||||
submodule = module: submoduleWith { modules = [ module ]; };
|
||||
options = lib.mapAttrs (_: mkOption);
|
||||
subOptions = opts: submodule { options = options opts; };
|
||||
in
|
||||
{
|
||||
options = options {
|
||||
settings = {
|
||||
description = ''
|
||||
Settings for the generated variables.
|
||||
'';
|
||||
type = submodule {
|
||||
freeformType = anything;
|
||||
imports = [ ./settings.nix ];
|
||||
};
|
||||
};
|
||||
generators = {
|
||||
default = {
|
||||
imports = [
|
||||
# implementation of the generator
|
||||
./generator.nix
|
||||
];
|
||||
};
|
||||
type = submodule {
|
||||
freeformType = attrsOf (subOptions {
|
||||
dependencies = {
|
||||
description = ''
|
||||
A list of other generators that this generator depends on.
|
||||
The output values of these generators will be available to the generator script as files.
|
||||
For example, the file 'file1' of a dependency named 'dep1' will be available via $dependencies/dep1/file1.
|
||||
'';
|
||||
type = listOf str;
|
||||
default = [ ];
|
||||
};
|
||||
files = {
|
||||
description = ''
|
||||
A set of files to generate.
|
||||
The generator 'script' is expected to produce exactly these files under $out.
|
||||
'';
|
||||
type = attrsOf (subOptions {
|
||||
secret = {
|
||||
description = ''
|
||||
Whether the file should be treated as a secret.
|
||||
'';
|
||||
type = bool;
|
||||
default = true;
|
||||
};
|
||||
path = {
|
||||
description = ''
|
||||
The path to the file containing the content of the generated value.
|
||||
This will be set automatically
|
||||
'';
|
||||
type = str;
|
||||
readOnly = true;
|
||||
};
|
||||
value = {
|
||||
description = ''
|
||||
The content of the generated value.
|
||||
Only available if the file is not secret.
|
||||
'';
|
||||
type = str;
|
||||
default = throw "Cannot access value of secret file";
|
||||
defaultText = "Throws error because the value of a secret file is not accessible";
|
||||
};
|
||||
});
|
||||
};
|
||||
prompts = {
|
||||
description = ''
|
||||
A set of prompts to ask the user for values.
|
||||
Prompts are available to the generator script as files.
|
||||
For example, a prompt named 'prompt1' will be available via $prompts/prompt1
|
||||
'';
|
||||
type = attrsOf (subOptions {
|
||||
description = {
|
||||
description = ''
|
||||
The description of the prompted value
|
||||
'';
|
||||
type = str;
|
||||
example = "SSH private key";
|
||||
};
|
||||
type = {
|
||||
description = ''
|
||||
The input type of the prompt.
|
||||
The following types are available:
|
||||
- hidden: A hidden text (e.g. password)
|
||||
- line: A single line of text
|
||||
- multiline: A multiline text
|
||||
'';
|
||||
type = enum [
|
||||
"hidden"
|
||||
"line"
|
||||
"multiline"
|
||||
];
|
||||
default = "line";
|
||||
};
|
||||
});
|
||||
};
|
||||
runtimeInputs = {
|
||||
description = ''
|
||||
A list of packages that the generator script requires.
|
||||
These packages will be available in the PATH when the script is run.
|
||||
'';
|
||||
type = listOf package;
|
||||
default = [ ];
|
||||
};
|
||||
script = {
|
||||
description = ''
|
||||
The script to run to generate the files.
|
||||
The script will be run with the following environment variables:
|
||||
- $dependencies: The directory containing the output values of all declared dependencies
|
||||
- $out: The output directory to put the generated files
|
||||
- $prompts: The directory containing the prompted values as files
|
||||
The script should produce the files specified in the 'files' attribute under $out.
|
||||
'';
|
||||
type = either str path;
|
||||
};
|
||||
finalScript = {
|
||||
description = ''
|
||||
The final generator script, wrapped, so:
|
||||
- all required programs are in PATH
|
||||
- sandbox is set up correctly
|
||||
'';
|
||||
type = lib.types.str;
|
||||
readOnly = true;
|
||||
internal = true;
|
||||
visible = false;
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
{ lib, ... }:
|
||||
{
|
||||
options = {
|
||||
secretStore = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"sops"
|
||||
"password-store"
|
||||
"vm"
|
||||
"custom"
|
||||
];
|
||||
default = "sops";
|
||||
description = ''
|
||||
method to store secret facts
|
||||
custom can be used to define a custom secret fact store.
|
||||
'';
|
||||
};
|
||||
|
||||
secretModule = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
internal = true;
|
||||
description = ''
|
||||
the python import path to the secret module
|
||||
'';
|
||||
};
|
||||
|
||||
secretUploadDirectory = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
The directory where secrets are uploaded into, This is backend specific.
|
||||
'';
|
||||
};
|
||||
|
||||
secretPathFunction = lib.mkOption {
|
||||
type = lib.types.raw;
|
||||
description = ''
|
||||
The function to use to generate the path for a secret.
|
||||
The default function will use the path attribute of the secret.
|
||||
The function will be called with the secret submodule as an argument.
|
||||
'';
|
||||
};
|
||||
|
||||
publicStore = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"in_repo"
|
||||
"vm"
|
||||
"custom"
|
||||
];
|
||||
default = "in_repo";
|
||||
description = ''
|
||||
method to store public facts.
|
||||
custom can be used to define a custom public fact store.
|
||||
'';
|
||||
};
|
||||
|
||||
publicModule = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
internal = true;
|
||||
description = ''
|
||||
the python import path to the public module
|
||||
'';
|
||||
};
|
||||
|
||||
publicDirectory = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
The directory where public facts are stored.
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user