Compare commits

...

107 Commits

Author SHA1 Message Date
pinpox
b37fa18f1b Remove clanModules 2025-08-18 14:37:20 +02:00
Jörg Thalheim
f539d00e9a waypipe: disable gpu for now 2025-08-18 14:35:53 +02:00
Jörg Thalheim
2d22eecd32 waypipe: disable gpu for now 2025-08-18 14:35:53 +02:00
clan-bot
3fb8b6587d Merge pull request 'Update nixpkgs-dev in devFlake' (#4791) from update-devFlake-nixpkgs-dev into main 2025-08-17 00:08:28 +00:00
clan-bot
6aee353b43 Update nixpkgs-dev in devFlake 2025-08-17 00:01:48 +00:00
hsjobeki
e109361e81 Merge pull request 'clanModules: remove unused code' (#4785) from clean-dead-code into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4785
2025-08-16 11:03:16 +00:00
Johannes Kirschbauer
3c34f81a44 inventory/tests: remove unused tests 2025-08-16 12:56:30 +02:00
Johannes Kirschbauer
72e7c2e9b9 clanModules: cleanup some more unused code 2025-08-16 12:56:30 +02:00
Johannes Kirschbauer
03968d8fbc api/inventory: remove leaked schemas 2025-08-16 12:56:30 +02:00
Johannes Kirschbauer
2f27b3941e lib/inventory: limit access to defined keys 2025-08-16 12:56:30 +02:00
clan-bot
e9dc5b9ba6 Merge pull request 'Update nixpkgs-dev in devFlake' (#4787) from update-devFlake-nixpkgs-dev into main 2025-08-16 10:07:46 +00:00
clan-bot
e4ef885cd5 Update nixpkgs-dev in devFlake 2025-08-16 10:01:45 +00:00
Johannes Kirschbauer
9fe457ebd5 lib/clanModules: update nix_models 2025-08-16 11:59:16 +02:00
Johannes Kirschbauer
4a51aa9316 clanModules: remove unused test code 2025-08-16 11:58:55 +02:00
Johannes Kirschbauer
308a10d6e6 clanModules: remove unused code 2025-08-16 11:48:13 +02:00
clan-bot
90f513a08f Merge pull request 'Update nixpkgs' (#4784) from update-nixpkgs into main 2025-08-16 00:21:23 +00:00
clan-bot
4ddc61d132 Update nixpkgs 2025-08-16 00:01:27 +00:00
clan-bot
fc0088e9ea Merge pull request 'Update nix-darwin' (#4783) from update-nix-darwin into main 2025-08-15 20:16:14 +00:00
clan-bot
71094f7fa1 Update nix-darwin 2025-08-15 20:00:52 +00:00
clan-bot
a8516cf9c6 Merge pull request 'Update nixpkgs-dev in devFlake' (#4782) from update-devFlake-nixpkgs-dev into main 2025-08-15 15:08:18 +00:00
clan-bot
a89e2f877a Update nixpkgs-dev in devFlake 2025-08-15 15:01:50 +00:00
Mic92
ed78e49c47 Merge pull request 'vms/inspect: mark test as pure' (#4781) from no-impure into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4781
2025-08-15 11:54:46 +00:00
Jörg Thalheim
3ef0a7919d vms/inspect: mark test as pure 2025-08-15 13:31:27 +02:00
Jörg Thalheim
36812d5f95 test_vars_deployment: simplify test to just start one vm 2025-08-15 13:30:30 +02:00
Mic92
f5bcdb4ba0 Merge pull request 'flakes/inspect: mark test as pure' (#4779) from no-impure into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4779
2025-08-15 11:28:22 +00:00
Jörg Thalheim
b69ad0eca5 backups/list: mark as pure 2025-08-15 13:10:41 +02:00
Jörg Thalheim
b221c29694 flakes/inspect: mark test as pure 2025-08-15 13:08:30 +02:00
Luis Hebendanz
7dc7f09173 Merge pull request 'clanServices: telegraf -> add basic auth' (#4777) from Qubasa/clan-core:basic_auth_telegraf into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4777
2025-08-15 11:07:44 +00:00
Mic92
ec3d224e1d Merge pull request 'tests_secrets_generate: mark as pure' (#4766) from no-impure into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4766
2025-08-15 11:06:47 +00:00
Luis Hebendanz
00c5312080 Merge pull request 'docs: Revamp Getting Started guide for clarity and usability' (#4776) from scriptogre/clan-core:update-getting-started-docs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4776
2025-08-15 11:04:52 +00:00
Qubasa
7811a56d2b clanServices: telegraf -> add basic auth
treefmt
2025-08-15 18:02:31 +07:00
Jörg Thalheim
e9401177b7 installation: make sure target host is actually down 2025-08-15 12:51:20 +02:00
Jörg Thalheim
ef56258e8b impure-checks: reduce to 6 jobs 2025-08-15 12:51:20 +02:00
Jörg Thalheim
c4d9b39a17 tests_secrets_generate: mark as pure 2025-08-15 12:51:20 +02:00
Mic92
1f59b75c20 Merge pull request 'Delete old files when deploying docs' (#4775) from deploy-docs-delete into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4775
2025-08-15 10:24:10 +00:00
scriptogre
6b6da7b897 docs: Revamp and simplify Getting Started guide 2025-08-15 13:19:39 +03:00
pinpox
4391c19ee9 Delete old files when deploying docs 2025-08-15 12:04:46 +02:00
hsjobeki
eb993b7060 Merge pull request 'ui/vars: add more vars to install story' (#4747) from ui-install-3 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4747
2025-08-15 09:14:46 +00:00
Johannes Kirschbauer
08cb6993a8 install/progress: display usb-stick 2025-08-15 11:10:57 +02:00
Johannes Kirschbauer
872f640211 install/assets: init usb-stick png image 2025-08-15 11:04:10 +02:00
Johannes Kirschbauer
c58f7c573d ui/install: clean up design 2025-08-15 11:04:09 +02:00
Johannes Kirschbauer
7b807a0745 ui/vars: add more vars to install story 2025-08-15 11:04:09 +02:00
pinpox
62805c66ff Merge pull request 'Add monitoring service' (#4756) from monitoring-service into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4756
2025-08-15 08:57:36 +00:00
pinpox
30b737ae1f init telegraph service 2025-08-15 10:54:39 +02:00
Luis Hebendanz
cc41185f98 Merge pull request 'flake.py: Error messages are now always ClanSelectErrors. Improved error messages' (#4773) from Qubasa/clan-core:improve_select_error into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4773
2025-08-15 06:50:46 +00:00
Qubasa
606aae7212 flake.py: Error messages are now always ClanSelectErrors. Improved error messages
flake.py: Fix unbound variable

flake.py: Fix test_create.py test
2025-08-15 13:46:12 +07:00
clan-bot
c31d884dc7 Merge pull request 'Update nixpkgs' (#4772) from update-nixpkgs into main 2025-08-15 05:22:06 +00:00
clan-bot
f546ce82f6 Update nixpkgs 2025-08-15 05:01:35 +00:00
clan-bot
b173bc37f5 Merge pull request 'Update nixpkgs' (#4771) from update-nixpkgs into main 2025-08-15 00:18:55 +00:00
clan-bot
0c20cfb34a Update nixpkgs 2025-08-15 00:01:25 +00:00
clan-bot
6c096a276d Merge pull request 'Update nixpkgs-dev in devFlake' (#4770) from update-devFlake-nixpkgs-dev into main 2025-08-14 20:08:03 +00:00
clan-bot
b7436b5b7f Update nixpkgs-dev in devFlake 2025-08-14 20:01:49 +00:00
hsjobeki
a84ab5d4bf Merge pull request 'exports: rename special args' (#4765) from exports-1 into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4765
2025-08-14 16:11:07 +00:00
Johannes Kirschbauer
a82ecbcbff docs: update docs for exports 2025-08-14 18:01:19 +02:00
Mic92
4ae3abe8c2 Merge pull request 'Update nixpkgs-dev in devFlake' (#4669) from update-devFlake-nixpkgs-dev into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4669
2025-08-14 16:01:05 +00:00
Johannes Kirschbauer
90c7951704 exports: expose in special args or 'perInstance' and 'perMachine' 2025-08-14 17:58:23 +02:00
Johannes Kirschbauer
116ff37156 exports: rename special args 2025-08-14 17:58:23 +02:00
clan-bot
f11df276a9 Update nixpkgs-dev in devFlake 2025-08-14 15:57:22 +00:00
Mic92
d44b43a937 Merge pull request 'test_machines_cli: remove more tests from the impure set' (#4764) from no-impure into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4764
2025-08-14 15:22:01 +00:00
Jörg Thalheim
716b74bc02 test_machines_cli: remove more tests from the impure set 2025-08-14 17:14:35 +02:00
Mic92
c85969c2b4 Merge pull request 'chore: remove spurious folder' (#4763) from orga into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4763
2025-08-14 15:11:51 +00:00
Johannes Kirschbauer
edb7dcc154 chore: remove spurious folder 2025-08-14 17:06:02 +02:00
Mic92
3586b4f48c Merge pull request 'clan-cli/machines/list: mark test as pure' (#4761) from no-impure-machines into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4761
2025-08-14 14:59:10 +00:00
Jörg Thalheim
9cdc6a27b6 test_copy_from_nixstore_symlink: drop test.
seems a bit overkill to have a test for two lines of python code...
2025-08-14 16:53:39 +02:00
Mic92
ceecdc0eef Merge pull request 'clan-cli/flash: remove impure tests' (#4757) from no-impure into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4757
2025-08-14 14:41:12 +00:00
Jörg Thalheim
96014c02c5 clan-cli/templates: mark tests as pure 2025-08-14 16:35:25 +02:00
Jörg Thalheim
810a2c67f9 clan-cli/machines/list: mark test as pure 2025-08-14 16:33:21 +02:00
Jörg Thalheim
fbb28afb2f clan-cli/flash: make tests runnable in sandbox 2025-08-14 16:23:22 +02:00
hsjobeki
a6ef38dadd Merge pull request 'vars: move generator class and bound methods into seperate module' (#4734) from generator-class into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4734
2025-08-14 14:17:14 +00:00
Mic92
328e0b20ac Merge pull request 'machines/list: make tests pure' (#4759) from no-impure-list into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4759
2025-08-14 14:07:51 +00:00
brianmcgee
7e77505316 Merge pull request 'fix(ui): host file input max width' (#4760) from fix/host-file-input-max-width into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4760
2025-08-14 14:01:34 +00:00
Brian McGee
245453b461 fix(ui): host file input max width 2025-08-14 14:57:26 +01:00
Jörg Thalheim
21e6a01cf3 machines/list: make tests pure 2025-08-14 15:54:00 +02:00
brianmcgee
302adf6f41 Merge pull request 'chore(ui): general cleanup' (#4758) from chore/cleanup into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4758
2025-08-14 13:32:36 +00:00
Brian McGee
f754b88ae4 chore(ui): general cleanup 2025-08-14 14:28:23 +01:00
Mic92
34d27e6bab Merge pull request 'clan-vm-manager: don't mark test_is_local as impure' (#4755) from no-impure into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4755
2025-08-14 13:12:59 +00:00
Jörg Thalheim
5817713e39 clan-vm-manager: don't mark test_is_local as impure.
should just work without it
2025-08-14 15:09:33 +02:00
clan-bot
cc283e88c9 Merge pull request 'Update disko' (#4700) from update-disko into main 2025-08-14 12:33:03 +00:00
clan-bot
1bb9f4741d Update disko 2025-08-14 12:12:27 +00:00
clan-bot
0d26e991e6 Merge pull request 'Update nuschtos in devFlake' (#4674) from update-devFlake-nuschtos into main 2025-08-14 12:11:19 +00:00
gitea-actions[bot]
961beda3e5 Update nuschtos in devFlake 2025-08-14 14:07:34 +02:00
clan-bot
0a8a1d4354 Merge pull request 'Update nixpkgs' (#4667) from update-nixpkgs into main 2025-08-14 12:05:03 +00:00
clan-bot
daf8d8e80d Update nixpkgs 2025-08-14 14:02:09 +02:00
Mic92
011b2a5872 Merge pull request 'update-flake-inputs: set different author' (#4753) from macos-ci into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4753
2025-08-14 11:35:09 +00:00
Jörg Thalheim
da06babcc2 update-flake-inputs: set different author 2025-08-14 13:30:22 +02:00
Mic92
c43eeb68a5 Merge pull request 'macos: don't build devShell until CI error is fixed' (#4752) from macos-ci into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4752
2025-08-14 11:27:30 +00:00
Jörg Thalheim
5e485a37f5 macos: don't build devShell until CI error is fixed 2025-08-14 13:05:02 +02:00
pinpox
ce902bed0a Merge pull request 'Remove clanModules (again)' (#4387) from remove-modules-new into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4387
2025-08-14 10:39:23 +00:00
pinpox
a5d401b715 Update migration status table 2025-08-14 12:29:27 +02:00
pinpox
2637496059 zt-tcp-relay: drop 2025-08-14 12:03:47 +02:00
pinpox
87c8a4549b re-add table with migration status 2025-08-14 12:02:40 +02:00
pinpox
35e5f4a42a Remove clanModules
- Removes clanModules in favor of the new clanServices
- Adds a warning and link to the migration guide
2025-08-14 12:02:40 +02:00
pinpox
e4949755d7 Merge pull request 'Migrate matrix-synapse to clan services' (#4684) from migrate-matrix-synapse into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4684
2025-08-14 10:01:14 +00:00
pinpox
b239c5bd88 Migrate matrix-synapse to clan services 2025-08-14 11:54:52 +02:00
Johannes Kirschbauer
4312e3fc2f vars: move generator class and bound methods into seperate module 2025-08-14 11:05:42 +02:00
hsjobeki
62ef90e959 Merge pull request 'codeowners: remove @lopter due to inactivity' (#4742) from codeowners into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4742
2025-08-13 17:48:08 +00:00
hsjobeki
7fdbd2e3eb Merge pull request 'dirs: remove unused conditional import' (#4736) from import-cleanup-dirs into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4736
2025-08-13 17:46:19 +00:00
hsjobeki
7daebd5ee0 Merge pull request 'networking: remove unused conditional import' (#4737) from networking-import into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4737
2025-08-13 17:46:04 +00:00
hsjobeki
cc8dd0564b Merge pull request 'askpass: use protocol as interface' (#4739) from interface-askpass into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4739
2025-08-13 17:43:01 +00:00
Johannes Kirschbauer
23e52954c9 codeowners: remove @lopter due to inactivity 2025-08-13 19:41:47 +02:00
hsjobeki
4717d1f149 Merge pull request 'lib/Remote: Unify class method _parse_ssh_uri with class file' (#4733) from imports-cleanup into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/4733
2025-08-13 17:40:36 +00:00
Johannes Kirschbauer
e28f280036 vars: remove unused conditional import 2025-08-13 19:24:53 +02:00
Johannes Kirschbauer
6fa2a977df askpass: use protocol as interface
Avoids a cyclic dependency on the Remote class
Strips down the dependency closure by explizitly declaring what functions it needs
2025-08-13 19:21:18 +02:00
Johannes Kirschbauer
65dba2508f dirs: remove unused conditional import 2025-08-13 19:04:32 +02:00
Johannes Kirschbauer
9884643070 networking: remove unused conditional import 2025-08-13 19:03:35 +02:00
Johannes Kirschbauer
5083992f7b lib: remove unused TYPE_CHECKING 2025-08-13 18:26:57 +02:00
Johannes Kirschbauer
6bd8839128 lib/Remote: Unify class method _parse_ssh_uri with class file 2025-08-13 18:26:28 +02:00
281 changed files with 1411 additions and 9054 deletions

View File

@@ -21,5 +21,9 @@ jobs:
# Exclude private flakes and update-clan-core checks flake
exclude-patterns: "checks/impure/flake.nix"
auto-merge: true
git-author-name: "clan-bot"
git-committer-name: "clan-bot"
git-author-email: "clan-bot@clan.lol"
git-committer-email: "clan-bot@clan.lol"
gitea-token: ${{ secrets.CI_BOT_TOKEN }}
github-token: ${{ secrets.CI_BOT_GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -39,7 +39,6 @@ select
# Generated files
pkgs/clan-app/ui/api/API.json
pkgs/clan-app/ui/api/API.ts
pkgs/clan-app/ui/api/Inventory.ts
pkgs/clan-app/ui/api/modules_schemas.json
pkgs/clan-app/ui/api/schema.json
pkgs/clan-app/ui/.fonts

View File

@@ -1,2 +0,0 @@
nixosModules/clanCore/vars/.* @lopter
pkgs/clan-cli/clan_cli/(secrets|vars)/.* @lopter

View File

@@ -1,51 +0,0 @@
(
{ ... }:
{
name = "borgbackup";
nodes.machine =
{ self, pkgs, ... }:
{
imports = [
self.clanModules.borgbackup
self.nixosModules.clanCore
{
services.openssh.enable = true;
services.borgbackup.repos.testrepo = {
authorizedKeys = [ (builtins.readFile ../assets/ssh/pubkey) ];
};
}
{
clan.core.settings.directory = ./.;
clan.core.state.testState.folders = [ "/etc/state" ];
environment.etc.state.text = "hello world";
systemd.tmpfiles.settings."vmsecrets" = {
"/etc/secrets/borgbackup/borgbackup.ssh" = {
C.argument = "${../assets/ssh/privkey}";
z = {
mode = "0400";
user = "root";
};
};
"/etc/secrets/borgbackup/borgbackup.repokey" = {
C.argument = builtins.toString (pkgs.writeText "repokey" "repokey12345");
z = {
mode = "0400";
user = "root";
};
};
};
# clan.core.facts.secretStore = "vm";
clan.core.vars.settings.secretStore = "vm";
clan.borgbackup.destinations.test.repo = "borg@localhost:.";
}
];
};
testScript = ''
start_all()
machine.systemctl("start --wait borgbackup-job-test.service")
assert "machine-test" in machine.succeed("BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes /run/current-system/sw/bin/borg-job-test list")
'';
}
)

View File

@@ -93,13 +93,10 @@ in
# Base Tests
nixos-test-secrets = self.clanLib.test.baseTest ./secrets nixosTestArgs;
nixos-test-borgbackup-legacy = self.clanLib.test.baseTest ./borgbackup-legacy nixosTestArgs;
nixos-test-wayland-proxy-virtwl = self.clanLib.test.baseTest ./wayland-proxy-virtwl nixosTestArgs;
# Container Tests
nixos-test-container = self.clanLib.test.containerTest ./container nixosTestArgs;
nixos-test-zt-tcp-relay = self.clanLib.test.containerTest ./zt-tcp-relay nixosTestArgs;
nixos-test-matrix-synapse = self.clanLib.test.containerTest ./matrix-synapse nixosTestArgs;
nixos-test-user-firewall-iptables = self.clanLib.test.containerTest ./user-firewall/iptables.nix nixosTestArgs;
nixos-test-user-firewall-nftables = self.clanLib.test.containerTest ./user-firewall/nftables.nix nixosTestArgs;
@@ -114,6 +111,8 @@ in
"dont-depend-on-repo-root"
];
# Temporary workaround: Filter out docs package and devshell for aarch64-darwin due to CI builder hangs
# TODO: Remove this filter once macOS CI builder is updated
flakeOutputs =
lib.mapAttrs' (
name: config: lib.nameValuePair "nixos-${name}" config.config.system.build.toplevel
@@ -121,8 +120,18 @@ in
// lib.mapAttrs' (
name: config: lib.nameValuePair "darwin-${name}" config.config.system.build.toplevel
) (self.darwinConfigurations or { })
// lib.mapAttrs' (n: lib.nameValuePair "package-${n}") packagesToBuild
// lib.mapAttrs' (n: lib.nameValuePair "devShell-${n}") self'.devShells
// lib.mapAttrs' (n: lib.nameValuePair "package-${n}") (
if system == "aarch64-darwin" then
lib.filterAttrs (n: _: n != "docs" && n != "deploy-docs" && n != "docs-options") packagesToBuild
else
packagesToBuild
)
// lib.mapAttrs' (n: lib.nameValuePair "devShell-${n}") (
if system == "aarch64-darwin" then
lib.filterAttrs (n: _: n != "docs") self'.devShells
else
self'.devShells
)
// lib.mapAttrs' (name: config: lib.nameValuePair "home-manager-${name}" config.activation-script) (
self'.legacyPackages.homeConfigurations or { }
);
@@ -130,33 +139,6 @@ in
nixosTests
// flakeOutputs
// {
# TODO: Automatically provide this check to downstream users to check their modules
clan-modules-json-compatible =
let
allSchemas = lib.mapAttrs (
_n: m:
let
schema =
(self.clanLib.evalService {
modules = [ m ];
prefix = [
"checks"
system
];
}).config.result.api.schema;
in
schema
) self.clan.modules;
in
pkgs.runCommand "combined-result"
{
schemaFile = builtins.toFile "schemas.json" (builtins.toJSON allSchemas);
}
''
mkdir -p $out
cat $schemaFile > $out/allSchemas.json
'';
clan-core-for-checks = pkgs.runCommand "clan-core-for-checks" { } ''
cp -r ${privateInputs.clan-core-for-checks} $out
chmod -R +w $out

View File

@@ -40,7 +40,7 @@
jobs=$(nproc)
# Spawning worker in pytest is relatively slow, so we limit the number of jobs to 13
# (current number of impure tests)
jobs="$((jobs > 13 ? 13 : jobs))"
jobs="$((jobs > 6 ? 6 : jobs))"
nix develop "$ROOT#clan-cli" -c bash -c "TMPDIR=/tmp python -m pytest -n $jobs -m impure ./clan_cli $@"

View File

@@ -241,7 +241,7 @@
target.shutdown()
except BrokenPipeError:
# qemu has already exited
pass
target.connected = False
# Create a new machine instance that boots from the installed system
installed_machine = create_test_machine(target, "${pkgs.qemu_test}", name="after_install")

View File

@@ -1,83 +0,0 @@
(
{ pkgs, ... }:
{
name = "matrix-synapse";
nodes.machine =
{
config,
self,
lib,
...
}:
{
imports = [
self.clanModules.matrix-synapse
self.nixosModules.clanCore
{
clan.core.settings.directory = ./.;
services.nginx.virtualHosts."matrix.clan.test" = {
enableACME = lib.mkForce false;
forceSSL = lib.mkForce false;
};
clan.nginx.acme.email = "admins@clan.lol";
clan.matrix-synapse = {
server_tld = "clan.test";
app_domain = "matrix.clan.test";
};
clan.matrix-synapse.users.admin.admin = true;
clan.matrix-synapse.users.someuser = { };
clan.core.facts.secretStore = "vm";
clan.core.vars.settings.secretStore = "vm";
clan.core.vars.settings.publicStore = "in_repo";
# because we use systemd-tmpfiles to copy the secrets, we need to a separate systemd-tmpfiles call to provision them.
boot.postBootCommands = "${config.systemd.package}/bin/systemd-tmpfiles --create /etc/tmpfiles.d/00-vmsecrets.conf";
systemd.tmpfiles.settings."00-vmsecrets" = {
# run before 00-nixos.conf
"/etc/secrets" = {
d.mode = "0700";
z.mode = "0700";
};
"/etc/secrets/matrix-synapse/synapse-registration_shared_secret" = {
f.argument = "supersecret";
z = {
mode = "0400";
user = "root";
};
};
"/etc/secrets/matrix-password-admin/matrix-password-admin" = {
f.argument = "matrix-password1";
z = {
mode = "0400";
user = "root";
};
};
"/etc/secrets/matrix-password-someuser/matrix-password-someuser" = {
f.argument = "matrix-password2";
z = {
mode = "0400";
user = "root";
};
};
};
}
];
};
testScript = ''
start_all()
machine.wait_for_unit("matrix-synapse")
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 8008")
machine.wait_until_succeeds("${pkgs.curl}/bin/curl -Ssf -L http://localhost/_matrix/static/ -H 'Host: matrix.clan.test'")
machine.systemctl("restart matrix-synapse >&2") # check if user creation is idempotent
machine.execute("journalctl -u matrix-synapse --no-pager >&2")
machine.wait_for_unit("matrix-synapse")
machine.succeed("${pkgs.netcat}/bin/nc -z -v ::1 8008")
machine.succeed("${pkgs.curl}/bin/curl -Ssf -L http://localhost/_matrix/static/ -H 'Host: matrix.clan.test'")
'';
}
)

View File

@@ -1 +0,0 @@
registration_shared_secret: supersecret

View File

@@ -16,7 +16,6 @@ nixosLib.runTest (
# This tests the compatibility of the inventory
# With the test framework
# - legacy-modules
# - clan.service modules
name = "service-dummy-test-from-flake";
@@ -37,9 +36,6 @@ nixosLib.runTest (
start_all()
admin1.wait_for_unit("multi-user.target")
peer1.wait_for_unit("multi-user.target")
# Provided by the legacy module
print(admin1.succeed("systemctl status dummy-service"))
print(peer1.succeed("systemctl status dummy-service"))
# peer1 should have the 'hello' file
peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.not-a-secret.path}")

View File

@@ -15,12 +15,6 @@
meta.name = "foo";
machines.peer1 = { };
machines.admin1 = { };
services = {
legacy-module.default = {
roles.peer.machines = [ "peer1" ];
roles.admin.machines = [ "admin1" ];
};
};
instances."test" = {
module.name = "new-service";
@@ -28,9 +22,6 @@
roles.peer.machines.peer1 = { };
};
modules = {
legacy-module = ./legacy-module;
};
};
modules.new-service = {

View File

@@ -1,10 +0,0 @@
---
description = "Set up dummy-module"
categories = ["System"]
features = [ "inventory" ]
[constraints]
roles.admin.min = 1
roles.admin.max = 1
---

View File

@@ -1,5 +0,0 @@
{
imports = [
../shared.nix
];
}

View File

@@ -1,5 +0,0 @@
{
imports = [
../shared.nix
];
}

View File

@@ -1,34 +0,0 @@
{ config, ... }:
{
systemd.services.dummy-service = {
enable = true;
description = "Dummy service";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
generated_password_path="${config.clan.core.vars.generators.dummy-generator.files.generated-password.path}"
if [ ! -f "$generated_password_path" ]; then
echo "Generated password file not found: $generated_password_path"
exit 1
fi
host_id_path="${config.clan.core.vars.generators.dummy-generator.files.host-id.path}"
if [ ! -e "$host_id_path" ]; then
echo "Host ID file not found: $host_id_path"
exit 1
fi
'';
};
# TODO: add and prompt and make it work in the test framework
clan.core.vars.generators.dummy-generator = {
files.host-id.secret = false;
files.generated-password.secret = true;
script = ''
echo $RANDOM > "$out"/host-id
echo $RANDOM > "$out"/generated-password
'';
};
}

View File

@@ -15,7 +15,6 @@ nixosLib.runTest (
# This tests the compatibility of the inventory
# With the test framework
# - legacy-modules
# - clan.service modules
name = "service-dummy-test";
@@ -24,12 +23,6 @@ nixosLib.runTest (
inventory = {
machines.peer1 = { };
machines.admin1 = { };
services = {
legacy-module.default = {
roles.peer.machines = [ "peer1" ];
roles.admin.machines = [ "admin1" ];
};
};
instances."test" = {
module.name = "new-service";
@@ -37,9 +30,6 @@ nixosLib.runTest (
roles.peer.machines.peer1 = { };
};
modules = {
legacy-module = ./legacy-module;
};
};
modules.new-service = {
_class = "clan.service";
@@ -78,9 +68,6 @@ nixosLib.runTest (
start_all()
admin1.wait_for_unit("multi-user.target")
peer1.wait_for_unit("multi-user.target")
# Provided by the legacy module
print(admin1.succeed("systemctl status dummy-service"))
print(peer1.succeed("systemctl status dummy-service"))
# peer1 should have the 'hello' file
peer1.succeed("cat ${nodes.peer1.clan.core.vars.generators.new-service.files.not-a-secret.path}")

View File

@@ -1,24 +0,0 @@
(
{ pkgs, ... }:
{
name = "zt-tcp-relay";
nodes.machine =
{ self, ... }:
{
imports = [
self.nixosModules.clanCore
self.clanModules.zt-tcp-relay
{
clan.core.settings.directory = ./.;
}
];
};
testScript = ''
start_all()
machine.wait_for_unit("zt-tcp-relay.service")
out = machine.succeed("${pkgs.netcat}/bin/nc -z -v localhost 4443")
print(out)
'';
}
)

View File

@@ -1,5 +0,0 @@
---
description = "Convenient Administration for the Clan App"
categories = ["Utility"]
features = [ "inventory", "deprecated" ]
---

View File

@@ -1,3 +0,0 @@
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,30 +0,0 @@
{ lib, config, ... }:
{
options.clan.admin = {
allowedKeys = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.str;
description = "The allowed public keys for ssh access to the admin user";
example = {
"key_1" = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD...";
};
};
};
# Bad practice.
# Should we add 'clanModules' to specialArgs?
imports = [
../../sshd
../../root-password
];
config = {
warnings = [
"The clan.admin module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
users.users.root.openssh.authorizedKeys.keys = builtins.attrValues config.clan.admin.allowedKeys;
};
}

View File

@@ -1,8 +0,0 @@
---
description = "Set up automatic upgrades"
categories = ["System"]
features = [ "inventory", "deprecated" ]
---
Whether to periodically upgrade NixOS to the latest version. If enabled, a
systemd timer will run `nixos-rebuild switch --upgrade` once a day.

View File

@@ -1,32 +0,0 @@
{
config,
lib,
...
}:
let
cfg = config.clan.auto-upgrade;
in
{
options.clan.auto-upgrade = {
flake = lib.mkOption {
type = lib.types.str;
description = "Flake reference";
};
};
config = {
warnings = [
"The clan.auto-upgrade module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
system.autoUpgrade = {
inherit (cfg) flake;
enable = true;
dates = "02:00";
randomizedDelaySec = "45min";
};
};
}

View File

@@ -1,16 +0,0 @@
---
description = "Statically configure borgbackup with sane defaults."
---
!!! Danger "Deprecated"
Use [borgbackup](borgbackup.md) instead.
Don't use borgbackup-static through [inventory](../../concepts/inventory.md).
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
`excludeMachines`, which will add every machine except the excluded machine to the backup.

View File

@@ -1,104 +0,0 @@
{ lib, config, ... }:
let
dir = config.clan.core.settings.directory;
machineDir = dir + "/machines/";
in
{
imports = [ ../borgbackup ];
options.clan.borgbackup-static = {
excludeMachines = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = lib.literalExpression "[ config.clan.core.settings.machine.name ]";
default = [ ];
description = ''
Machines that should not be backuped.
Mutually exclusive with includeMachines.
If this is not empty, every other machine except the targets in the clan will be backuped by this module.
If includeMachines is set, only the included machines will be backuped.
'';
};
includeMachines = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = lib.literalExpression "[ config.clan.core.settings.machine.name ]";
default = [ ];
description = ''
Machines that should be backuped.
Mutually exclusive with excludeMachines.
'';
};
targets = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Machines that should act as target machines for backups.
'';
};
};
config.services.borgbackup.repos =
let
machines = builtins.readDir machineDir;
borgbackupIpMachinePath = machines: machineDir + machines + "/facts/borgbackup.ssh.pub";
filteredMachines =
if ((builtins.length config.clan.borgbackup-static.includeMachines) != 0) then
lib.filterAttrs (name: _: (lib.elem name config.clan.borgbackup-static.includeMachines)) machines
else
lib.filterAttrs (name: _: !(lib.elem name config.clan.borgbackup-static.excludeMachines)) machines;
machinesMaybeKey = lib.mapAttrsToList (
machine: _:
let
fullPath = borgbackupIpMachinePath machine;
in
if builtins.pathExists fullPath then machine else null
) filteredMachines;
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
lib.mkIf
(builtins.any (
target: target == config.clan.core.settings.machine.name
) config.clan.borgbackup-static.targets)
(if (builtins.listToAttrs hosts) != null then builtins.listToAttrs hosts else { });
config.clan.borgbackup.destinations =
let
destinations = builtins.map (d: {
name = d;
value = {
repo = "borg@${d}:/var/lib/borgbackup/${config.clan.core.settings.machine.name}";
};
}) config.clan.borgbackup-static.targets;
in
lib.mkIf (builtins.any (
target: target == config.clan.core.settings.machine.name
) config.clan.borgbackup-static.includeMachines) (builtins.listToAttrs destinations);
config.assertions = [
{
assertion =
!(
((builtins.length config.clan.borgbackup-static.excludeMachines) != 0)
&& ((builtins.length config.clan.borgbackup-static.includeMachines) != 0)
);
message = ''
The options:
config.clan.borgbackup-static.excludeMachines = [${builtins.toString config.clan.borgbackup-static.excludeMachines}]
and
config.clan.borgbackup-static.includeMachines = [${builtins.toString config.clan.borgbackup-static.includeMachines}]
are mutually exclusive.
Use excludeMachines to exclude certain machines and backup the other clan machines.
Use include machines to only backup certain machines.
'';
}
];
config.warnings = lib.optional (
builtins.length config.clan.borgbackup-static.targets > 0
) "The borgbackup-static module is deprecated use the service via the inventory interface instead.";
}

View File

@@ -1,14 +0,0 @@
---
description = "Efficient, deduplicating backup program with optional compression and secure encryption."
categories = ["System"]
features = [ "inventory", "deprecated" ]
---
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.

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/client.nix ];
}

View File

@@ -1,210 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
# Instances might be empty, if the module is not used via the inventory
instances = config.clan.inventory.services.borgbackup or { };
# 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;
machineName = config.clan.core.settings.machine.name;
cfg = config.clan.borgbackup;
preBackupScript = ''
declare -A preCommandErrors
${lib.concatMapStringsSep "\n" (
state:
lib.optionalString (state.preBackupCommand != null) ''
echo "Running pre-backup command for ${state.name}"
if ! /run/current-system/sw/bin/${state.preBackupCommand}; then
preCommandErrors["${state.name}"]=1
fi
''
) (lib.attrValues config.clan.core.state)}
if [[ ''${#preCommandErrors[@]} -gt 0 ]]; then
echo "pre-backup commands failed for the following services:"
for state in "''${!preCommandErrors[@]}"; do
echo " $state"
done
exit 1
fi
'';
in
{
options.clan.borgbackup.destinations = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.strMatching "^[a-zA-Z0-9._-]+$";
default = name;
description = "the name of the backup job";
};
repo = lib.mkOption {
type = lib.types.str;
description = "the borgbackup repository to backup to";
};
rsh = lib.mkOption {
type = lib.types.str;
default = "ssh -i ${
config.clan.core.vars.generators.borgbackup.files."borgbackup.ssh".path
} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=Yes";
defaultText = "ssh -i \${config.clan.core.vars.generators.borgbackup.files.\"borgbackup.ssh\".path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null";
description = "the rsh to use for the backup";
};
};
}
)
);
default = { };
description = ''
destinations where the machine should be backuped to
'';
};
options.clan.borgbackup.exclude = lib.mkOption {
type = lib.types.listOf lib.types.str;
example = [ "*.pyc" ];
default = [ ];
description = ''
Directories/Files to exclude from the backup.
Use * as a wildcard.
'';
};
config = {
warnings = [
"The clan.borgbackup module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
# Destinations
clan.borgbackup.destinations =
let
destinations = builtins.map (serverName: {
name = serverName;
value = {
repo = "borg@${serverName}:/var/lib/borgbackup/${machineName}";
};
}) allServers;
in
(builtins.listToAttrs destinations);
# Derived from the destinations
systemd.services = lib.mapAttrs' (
_: dest:
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}''
];
}
) cfg.destinations;
services.borgbackup.jobs = lib.mapAttrs (_: dest: {
paths = lib.unique (
lib.flatten (map (state: state.folders) (lib.attrValues config.clan.core.state))
);
exclude = cfg.exclude;
repo = dest.repo;
environment.BORG_RSH = dest.rsh;
compression = "auto,zstd";
startAt = "*-*-* 01:00:00";
persistentTimer = true;
encryption = {
mode = "repokey";
passCommand = "cat ${config.clan.core.vars.generators.borgbackup.files."borgbackup.repokey".path}";
};
prune.keep = {
within = "1d"; # Keep all archives from the last day
daily = 7;
weekly = 4;
monthly = 0;
};
}) cfg.destinations;
environment.systemPackages = [
(pkgs.writeShellApplication {
name = "borgbackup-create";
runtimeInputs = [ config.systemd.package ];
text = ''
${lib.concatMapStringsSep "\n" (dest: ''
systemctl start borgbackup-job-${dest.name}
'') (lib.attrValues cfg.destinations)}
'';
})
(pkgs.writeShellApplication {
name = "borgbackup-list";
runtimeInputs = [ pkgs.jq ];
text = ''
(${
lib.concatMapStringsSep "\n" (
dest:
# we need yes here to skip the changed url verification
''echo y | /run/current-system/sw/bin/borg-job-${dest.name} list --json | jq '[.archives[] | {"name": ("${dest.name}::${dest.repo}::" + .name)}]' ''
) (lib.attrValues cfg.destinations)
}) | jq -s 'add // []'
'';
})
(pkgs.writeShellApplication {
name = "borgbackup-restore";
runtimeInputs = [ pkgs.gawk ];
text = ''
cd /
IFS=':' read -ra FOLDER <<< "''${FOLDERS-}"
job_name=$(echo "$NAME" | awk -F'::' '{print $1}')
backup_name=''${NAME#"$job_name"::}
if [[ ! -x /run/current-system/sw/bin/borg-job-"$job_name" ]]; then
echo "borg-job-$job_name not found: Backup name is invalid" >&2
exit 1
fi
echo y | /run/current-system/sw/bin/borg-job-"$job_name" extract "$backup_name" "''${FOLDER[@]}"
'';
})
];
clan.core.vars.generators.borgbackup = {
files."borgbackup.ssh.pub".secret = false;
files."borgbackup.ssh" = { };
files."borgbackup.repokey" = { };
migrateFact = "borgbackup";
runtimeInputs = [
pkgs.coreutils
pkgs.openssh
pkgs.xkcdpass
];
script = ''
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/borgbackup.ssh
xkcdpass -n 4 -d - > "$out"/borgbackup.repokey
'';
};
clan.core.backups.providers.borgbackup = {
list = "borgbackup-list";
create = "borgbackup-create";
restore = "borgbackup-restore";
};
};
}

View File

@@ -1,63 +0,0 @@
{ config, lib, ... }:
let
dir = config.clan.core.settings.directory;
machineDir = dir + "/vars/per-machine/";
machineName = config.clan.core.settings.machine.name;
# Instances might be empty, if the module is not used via the inventory
#
# Type: { ${instanceName} :: { roles :: Roles } }
# Roles :: { ${role_name} :: { machines :: [string] } }
instances = config.clan.inventory.services.borgbackup or { };
allClients = lib.foldlAttrs (
acc: _instanceName: instanceConfig:
acc
++ (
if (builtins.elem machineName instanceConfig.roles.server.machines) then
instanceConfig.roles.client.machines
else
[ ]
)
) [ ] instances;
in
{
options = {
clan.borgbackup.directory = lib.mkOption {
type = lib.types.str;
default = "/var/lib/borgbackup";
description = ''
The directory where the borgbackup repositories are stored.
'';
};
};
config.services.borgbackup.repos =
let
borgbackupIpMachinePath = machine: machineDir + machine + "/borgbackup/borgbackup.ssh.pub/value";
machinesMaybeKey = builtins.map (
machine:
let
fullPath = borgbackupIpMachinePath machine;
in
if builtins.pathExists fullPath then
machine
else
lib.warn ''
Machine ${machine} does not have a borgbackup key at ${fullPath},
run `clan vars generate ${machine}` to generate it.
'' null
) allClients;
machinesWithKey = lib.filter (x: x != null) machinesMaybeKey;
hosts = builtins.map (machine: {
name = machine;
value = {
path = "${config.clan.borgbackup.directory}/${machine}";
authorizedKeys = [ (builtins.readFile (borgbackupIpMachinePath machine)) ];
};
}) machinesWithKey;
in
if (builtins.listToAttrs hosts) != [ ] then builtins.listToAttrs hosts else { };
}

View File

@@ -1,10 +0,0 @@
---
description = "Set up data-mesher"
categories = ["System"]
features = [ "inventory" ]
[constraints]
roles.admin.min = 1
roles.admin.max = 1
---

View File

@@ -1,19 +0,0 @@
lib: {
machines =
config:
let
instanceNames = builtins.attrNames config.clan.inventory.services.data-mesher;
instanceName = builtins.head instanceNames;
dataMesherInstances = config.clan.inventory.services.data-mesher.${instanceName};
uniqueStrings = list: builtins.attrNames (builtins.groupBy lib.id list);
in
rec {
admins = dataMesherInstances.roles.admin.machines or [ ];
signers = dataMesherInstances.roles.signer.machines or [ ];
peers = dataMesherInstances.roles.peer.machines or [ ];
bootstrap = uniqueStrings (admins ++ signers);
};
}

View File

@@ -1,58 +0,0 @@
{ lib, config, ... }:
let
cfg = config.clan.data-mesher;
dmLib = import ../lib.nix lib;
in
{
imports = [
../shared.nix
];
options.clan.data-mesher = {
network = {
tld = lib.mkOption {
type = lib.types.str;
default = (config.networking.domain or "clan");
description = "Top level domain to use for the network";
};
hostTTL = lib.mkOption {
type = lib.types.str;
default = "672h"; # 28 days
example = "24h";
description = "The TTL for hosts in the network, in the form of a Go time.Duration";
};
};
};
config = {
warnings = [
"The clan.admin module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
services.data-mesher.initNetwork =
let
# for a given machine, read it's public key and remove any new lines
readHostKey =
machine:
let
path = "${config.clan.core.settings.directory}/vars/per-machine/${machine}/data-mesher-host-key/public_key/value";
in
builtins.elemAt (lib.splitString "\n" (builtins.readFile path)) 1;
in
{
enable = true;
keyPath = config.clan.core.vars.generators.data-mesher-network-key.files.private_key.path;
tld = cfg.network.tld;
hostTTL = cfg.network.hostTTL;
# admin and signer host public keys
signingKeys = builtins.map readHostKey (dmLib.machines config).bootstrap;
};
};
}

View File

@@ -1,5 +0,0 @@
{
imports = [
../shared.nix
];
}

View File

@@ -1,5 +0,0 @@
{
imports = [
../shared.nix
];
}

View File

@@ -1,152 +0,0 @@
{
config,
lib,
...
}:
let
cfg = config.clan.data-mesher;
dmLib = import ./lib.nix lib;
# the default bootstrap nodes are any machines with the admin or signers role
# we iterate through those machines, determining an IP address for them based on their VPN
# currently only supports zerotier
defaultBootstrapNodes = builtins.foldl' (
urls: name:
let
ipPath = "${config.clan.core.settings.directory}/vars/per-machine/${name}/zerotier/zerotier-ip/value";
in
if builtins.pathExists ipPath then
let
ip = builtins.readFile ipPath;
in
urls ++ [ "[${ip}]:${builtins.toString cfg.network.port}" ]
else
urls
) [ ] (dmLib.machines config).bootstrap;
in
{
options.clan.data-mesher = {
bootstrapNodes = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
description = ''
A list of bootstrap nodes that act as an initial gateway when joining
the cluster.
'';
example = [
"192.168.1.1:7946"
"192.168.1.2:7946"
];
};
network = {
interface = lib.mkOption {
type = lib.types.str;
description = ''
The interface over which cluster communication should be performed.
All the ip addresses associate with this interface will be part of
our host claim, including both ipv4 and ipv6.
This should be set to an internal/VPN interface.
'';
example = "tailscale0";
};
port = lib.mkOption {
type = lib.types.port;
default = 7946;
description = ''
Port to listen on for cluster communication.
'';
};
};
};
config = {
services.data-mesher = {
enable = true;
openFirewall = true;
settings = {
log_level = "warn";
state_dir = "/var/lib/data-mesher";
# read network id from vars
network.id = config.clan.core.vars.generators.data-mesher-network-key.files.public_key.value;
host = {
names = [ config.networking.hostName ];
key_path = config.clan.core.vars.generators.data-mesher-host-key.files.private_key.path;
};
cluster = {
port = cfg.network.port;
join_interval = "30s";
push_pull_interval = "30s";
interface = cfg.network.interface;
bootstrap_nodes = if cfg.bootstrapNodes == null then defaultBootstrapNodes else cfg.bootstrapNodes;
};
http.port = 7331;
http.interface = "lo";
};
};
# Generate host key.
clan.core.vars.generators.data-mesher-host-key = {
files =
let
owner = config.users.users.data-mesher.name;
in
{
private_key = {
inherit owner;
};
public_key.secret = false;
};
runtimeInputs = [
config.services.data-mesher.package
];
script = ''
data-mesher generate keypair \
--public-key-path "$out"/public_key \
--private-key-path "$out"/private_key
'';
};
clan.core.vars.generators.data-mesher-network-key = {
# generated once per clan
share = true;
files =
let
owner = config.users.users.data-mesher.name;
in
{
private_key = {
inherit owner;
};
public_key.secret = false;
};
runtimeInputs = [
config.services.data-mesher.package
];
script = ''
data-mesher generate keypair \
--public-key-path "$out"/public_key \
--private-key-path "$out"/private_key
'';
};
};
}

View File

@@ -1,17 +0,0 @@
---
description = "Email-based instant messaging for Desktop."
categories = ["Social"]
features = [ "inventory", "deprecated" ]
---
!!! info
This module will automatically configure an email server on the machine for handling the e-mail messaging seamlessly.
## Features
- [x] **Email-based**: Uses any email account as its backend.
- [x] **End-to-End Encryption**: Supports Autocrypt to automatically encrypt messages.
- [x] **No Phone Number Required**: Uses your email address instead of a phone number.
- [x] **Cross-Platform**: Available on desktop and mobile platforms.
- [x] **Automatic Server Setup**: Includes your own DeltaChat server for enhanced control and privacy.
- [ ] **Bake a cake**: This module cannot cake a bake.

View File

@@ -1,3 +0,0 @@
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,153 +0,0 @@
{
config,
pkgs,
...
}:
{
warnings = [
"The clan.deltachat module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
networking.firewall.interfaces."zt+".allowedTCPPorts = [ 25 ]; # smtp with other hosts
environment.systemPackages = [ pkgs.deltachat-desktop ];
services.maddy =
let
domain = "${config.clan.core.settings.machine.name}.local";
in
{
enable = true;
primaryDomain = domain;
config = ''
# Minimal configuration with TLS disabled, adapted from upstream example
# configuration here https://github.com/foxcpp/maddy/blob/master/maddy.conf
# Do not use this in unencrypted networks!
auth.pass_table local_authdb {
table sql_table {
driver sqlite3
dsn credentials.db
table_name passwords
}
}
storage.imapsql local_mailboxes {
driver sqlite3
dsn imapsql.db
}
table.chain local_rewrites {
optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
optional_step static {
entry postmaster postmaster@$(primary_domain)
}
optional_step file /etc/maddy/aliases
}
msgpipeline local_routing {
destination postmaster $(local_domains) {
modify {
replace_rcpt &local_rewrites
}
deliver_to &local_mailboxes
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
smtp tcp://[::]:25 {
limits {
all rate 20 1s
all concurrency 10
}
dmarc yes
check {
require_mx_record
dkim
spf
}
source $(local_domains) {
reject 501 5.1.8 "Use Submission for outgoing SMTP"
}
default_source {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.1.1 "User doesn't exist"
}
}
}
submission tcp://[::1]:587 {
limits {
all rate 50 1s
}
auth &local_authdb
source $(local_domains) {
check {
authorize_sender {
prepare_email &local_rewrites
user_to_email identity
}
}
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
modify {
dkim $(primary_domain) $(local_domains) default
}
deliver_to &remote_queue
}
}
default_source {
reject 501 5.1.8 "Non-local sender domain"
}
}
target.remote outbound_delivery {
limits {
destination rate 20 1s
destination concurrency 10
}
mx_auth {
dane
mtasts {
cache fs
fs_dir mtasts_cache/
}
local_policy {
min_tls_level encrypted
min_mx_level none
}
}
}
target.queue remote_queue {
target &outbound_delivery
autogenerated_msg_domain $(primary_domain)
bounce {
destination postmaster $(local_domains) {
deliver_to &local_routing
}
default_destination {
reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
}
}
}
imap tcp://[::1]:143 {
auth &local_authdb
storage &local_mailboxes
}
'';
ensureAccounts = [ "user@${domain}" ];
ensureCredentials = {
"user@${domain}".passwordFile = pkgs.writeText "dummy" "foobar";
};
};
}

View File

@@ -1,5 +0,0 @@
---
description = "Generates a uuid for use in disk device naming"
features = [ "inventory" ]
categories = [ "System" ]
---

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,36 +0,0 @@
{
pkgs,
...
}:
{
config = {
warnings = [
''
The clan.disk-id module is deprecated and will be removed on 2025-07-15.
For migration see: https://docs.clan.lol/guides/migrations/disk-id/
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!! Please migrate. Otherwise you may not be able to boot your system after that date. !!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
''
];
clan.core.vars.generators.disk-id = {
files.diskId.secret = false;
runtimeInputs = [
pkgs.coreutils
pkgs.bash
];
script = ''
uuid=$(bash ${./uuid4.sh})
# Remove the hyphens from the UUID
uuid_no_hyphens=$(echo -n "$uuid" | tr -d '-')
echo -n "$uuid_no_hyphens" > "$out/diskId"
'';
};
};
}

View File

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
# Read 16 bytes from /dev/urandom
uuid=$(dd if=/dev/urandom bs=1 count=16 2>/dev/null | od -An -tx1 | tr -d ' \n')
# Break the UUID into pieces and apply the required modifications
byte6=${uuid:12:2}
byte8=${uuid:16:2}
# Construct the correct version and variant
hex_byte6=$(printf "%x" $((0x$byte6 & 0x0F | 0x40)))
hex_byte8=$(printf "%x" $((0x$byte8 & 0x3F | 0x80)))
# Rebuild the UUID with the correct fields
uuid_v4="${uuid:0:12}${hex_byte6}${uuid:14:2}${hex_byte8}${uuid:18:14}"
# Format the UUID correctly 8-4-4-4-12
uuid_formatted="${uuid_v4:0:8}-${uuid_v4:8:4}-${uuid_v4:12:4}-${uuid_v4:16:4}-${uuid_v4:20:12}"
echo -n "$uuid_formatted"

View File

@@ -1,6 +0,0 @@
---
description = "A dynamic DNS service to update domain IPs"
---
To understand the possible options that can be set visit the documentation of [ddns-updater](https://github.com/qdm12/ddns-updater?tab=readme-ov-file#versioned-documentation)

View File

@@ -1,257 +0,0 @@
{
config,
pkgs,
lib,
...
}:
let
name = "dyndns";
cfg = config.clan.${name};
# We dedup secrets if they have the same provider + base domain
secret_id = opt: "${name}-${opt.provider}-${opt.domain}";
secret_path =
opt: config.clan.core.vars.generators."${secret_id opt}".files."${secret_id opt}".path;
# We check that a secret has not been set in extraSettings.
extraSettingsSafe =
opt:
if (builtins.hasAttr opt.secret_field_name opt.extraSettings) then
throw "Please do not set ${opt.secret_field_name} in extraSettings, it is automatically set by the dyndns module."
else
opt.extraSettings;
/*
We go from:
{home.example.com:{value:{domain:example.com,host:home, provider:namecheap}}}
To:
{settings: [{domain: example.com, host: home, provider: namecheap, password: dyndns-namecheap-example.com}]}
*/
service_config = {
settings = builtins.catAttrs "value" (
builtins.attrValues (
lib.mapAttrs (_: opt: {
value =
(extraSettingsSafe opt)
// {
domain = opt.domain;
provider = opt.provider;
}
// {
"${opt.secret_field_name}" = secret_id opt;
};
}) cfg.settings
)
);
};
secret_generator = _: opt: {
name = secret_id opt;
value = {
share = true;
migrateFact = "${secret_id opt}";
prompts.${secret_id opt} = {
type = "hidden";
persist = true;
};
};
};
in
{
options.clan.${name} = {
server = {
enable = lib.mkEnableOption "dyndns webserver";
domain = lib.mkOption {
type = lib.types.str;
description = "Domain to serve the webservice on";
};
port = lib.mkOption {
type = lib.types.int;
default = 54805;
description = "Port to listen on";
};
};
period = lib.mkOption {
type = lib.types.int;
default = 5;
description = "Domain update period in minutes";
};
settings = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ ... }:
{
options = {
provider = lib.mkOption {
example = "namecheap";
type = lib.types.str;
description = "The dyndns provider to use";
};
domain = lib.mkOption {
type = lib.types.str;
example = "example.com";
description = "The top level domain to update.";
};
secret_field_name = lib.mkOption {
example = [
"password"
"api_key"
];
type = lib.types.enum [
"password"
"token"
"api_key"
"secret_api_key"
];
default = "password";
description = "The field name for the secret";
};
# TODO: Ideally we would create a gigantic list of all possible settings / types
# optimally we would have a way to generate the options from the source code
extraSettings = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = ''
Extra settings for the provider.
Provider specific settings: https://github.com/qdm12/ddns-updater#configuration
'';
};
};
}
)
);
default = [ ];
description = "Configuration for which domains to update";
};
};
imports = [
../nginx
];
config = lib.mkMerge [
(lib.mkIf (cfg.settings != { }) {
clan.core.vars.generators = lib.mapAttrs' secret_generator cfg.settings;
users.groups.${name} = { };
users.users.${name} = {
group = name;
isSystemUser = true;
description = "User for ${name} service";
home = "/var/lib/${name}";
createHome = true;
};
services.nginx = lib.mkIf cfg.server.enable {
enable = true;
virtualHosts = {
"${cfg.server.domain}" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:${toString cfg.server.port}";
};
};
};
};
systemd.services.${name} = {
path = [ ];
description = "Dynamic DNS updater";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = {
MYCONFIG = "${builtins.toJSON service_config}";
SERVER_ENABLED = if cfg.server.enable then "yes" else "no";
PERIOD = "${toString cfg.period}m";
LISTENING_ADDRESS = ":${toString cfg.server.port}";
};
serviceConfig =
let
pyscript =
pkgs.writers.writePython3Bin "generate_secret_config.py"
{
libraries = [ ];
doCheck = false;
}
''
import json
from pathlib import Path
import os
cred_dir = Path(os.getenv("CREDENTIALS_DIRECTORY"))
config_str = os.getenv("MYCONFIG")
def get_credential(name):
secret_p = cred_dir / name
with open(secret_p, 'r') as f:
return f.read().strip()
config = json.loads(config_str)
print(f"Config: {config}")
for attrset in config["settings"]:
if "password" in attrset:
attrset['password'] = get_credential(attrset['password'])
elif "token" in attrset:
attrset['token'] = get_credential(attrset['token'])
elif "secret_api_key" in attrset:
attrset['secret_api_key'] = get_credential(attrset['secret_api_key'])
elif "api_key" in attrset:
attrset['api_key'] = get_credential(attrset['api_key'])
else:
raise ValueError(f"Missing secret field in {attrset}")
# create directory data if it does not exist
data_dir = Path('data')
data_dir.mkdir(mode=0o770, exist_ok=True)
# Create a temporary config file
# with appropriate permissions
tmp_config_path = data_dir / '.config.json'
tmp_config_path.touch(mode=0o660, exist_ok=False)
# Write the config with secrets back
with open(tmp_config_path, 'w') as f:
f.write(json.dumps(config, indent=4))
# Move config into place
config_path = data_dir / 'config.json'
tmp_config_path.rename(config_path)
# Set file permissions to read
# and write only by the user and group
for file in data_dir.iterdir():
file.chmod(0o660)
'';
in
{
ExecStartPre = lib.getExe pyscript;
ExecStart = lib.getExe pkgs.ddns-updater;
LoadCredential = lib.mapAttrsToList (_: opt: "${secret_id opt}:${secret_path opt}") cfg.settings;
User = name;
Group = name;
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ReadOnlyPaths = "/";
PrivateDevices = "yes";
ProtectKernelModules = "yes";
ProtectKernelTunables = "yes";
WorkingDirectory = "/var/lib/${name}";
ReadWritePaths = [
"/proc/self"
"/var/lib/${name}"
];
Restart = "always";
RestartSec = 60;
};
};
})
];
}

View File

@@ -1,5 +0,0 @@
---
description = "A modern IRC server"
categories = ["Social"]
features = [ "inventory", "deprecated" ]
---

View File

@@ -1,3 +0,0 @@
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,21 +0,0 @@
_: {
warnings = [
"The clan.ergochat module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
services.ergochat = {
enable = true;
settings = {
datastore = {
autoupgrade = true;
path = "/var/lib/ergo/ircd.db";
};
};
};
clan.core.state.ergochat.folders = [ "/var/lib/ergo" ];
}

View File

@@ -1,51 +0,0 @@
{ lib, ... }:
let
inherit (lib)
filterAttrs
pathExists
;
in
{
# only import available files, as this allows to filter the files for tests.
flake.clanModules = filterAttrs (_name: pathExists) {
auto-upgrade = ./auto-upgrade;
admin = ./admin;
borgbackup = ./borgbackup;
borgbackup-static = ./borgbackup-static;
deltachat = ./deltachat;
data-mesher = ./data-mesher;
disk-id = ./disk-id;
dyndns = ./dyndns;
ergochat = ./ergochat;
garage = ./garage;
heisenbridge = ./heisenbridge;
importer = ./importer;
iwd = ./iwd;
localbackup = ./localbackup;
localsend = ./localsend;
matrix-synapse = ./matrix-synapse;
moonlight = ./moonlight;
mumble = ./mumble;
mycelium = ./mycelium;
nginx = ./nginx;
packages = ./packages;
postgresql = ./postgresql;
root-password = ./root-password;
single-disk = ./single-disk;
sshd = ./sshd;
state-version = ./state-version;
static-hosts = ./static-hosts;
sunshine = ./sunshine;
syncthing = ./syncthing;
syncthing-static-peers = ./syncthing-static-peers;
thelounge = ./thelounge;
trusted-nix-caches = ./trusted-nix-caches;
user-password = ./user-password;
vaultwarden = ./vaultwarden;
wifi = ./wifi;
xfce = ./xfce;
zerotier = ./zerotier;
zerotier-static-peers = ./zerotier-static-peers;
zt-tcp-relay = ./zt-tcp-relay;
};
}

View File

@@ -1,11 +0,0 @@
---
description = "S3-compatible object store for small self-hosted geo-distributed deployments"
categories = ["System"]
features = [ "inventory", "deprecated" ]
---
This module generates garage specific keys automatically.
Also shares the `rpc_secret` between instances.
Options: [NixosModuleOptions](https://search.nixos.org/options?channel=unstable&size=50&sort=relevance&type=packages&query=garage)
Documentation: https://garagehq.deuxfleurs.fr/

View File

@@ -1,3 +0,0 @@
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,50 +0,0 @@
{ config, pkgs, ... }:
{
warnings = [
"The clan.ergochat module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
systemd.services.garage.serviceConfig = {
LoadCredential = [
"rpc_secret_path:${config.clan.core.vars.generators.garage-shared.files.rpc_secret.path}"
"admin_token_path:${config.clan.core.vars.generators.garage.files.admin_token.path}"
"metrics_token_path:${config.clan.core.vars.generators.garage.files.metrics_token.path}"
];
Environment = [
"GARAGE_ALLOW_WORLD_READABLE_SECRETS=true"
"GARAGE_RPC_SECRET_FILE=%d/rpc_secret_path"
"GARAGE_ADMIN_TOKEN_FILE=%d/admin_token_path"
"GARAGE_METRICS_TOKEN_FILE=%d/metrics_token_path"
];
};
clan.core.vars.generators.garage = {
files.admin_token = { };
files.metrics_token = { };
runtimeInputs = [
pkgs.coreutils
pkgs.openssl
];
script = ''
openssl rand -base64 -out "$out"/admin_token 32
openssl rand -base64 -out "$out"/metrics_token 32
'';
};
clan.core.vars.generators.garage-shared = {
share = true;
files.rpc_secret = { };
runtimeInputs = [
pkgs.coreutils
pkgs.openssl
];
script = ''
openssl rand -hex -out "$out"/rpc_secret 32
'';
};
clan.core.state.garage.folders = [ config.services.garage.settings.metadata_dir ];
}

View File

@@ -1,5 +0,0 @@
---
description = "A matrix bridge to communicate with IRC"
categories = ["Social"]
features = [ "inventory", "deprecated" ]
---

View File

@@ -1,3 +0,0 @@
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,27 +0,0 @@
{
lib,
...
}:
{
imports = [
(lib.mkRemovedOptionModule [
"clan"
"heisenbridge"
"enable"
] "Importing the module will already enable the service.")
];
config = {
warnings = [
"The clan.heisenbridge module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
services.heisenbridge = {
enable = true;
homeserver = "http://localhost:8008"; # TODO: Sync with matrix-synapse
};
services.matrix-synapse.settings.app_service_config_files = [
"/var/lib/heisenbridge/registration.yml"
];
};
}

View File

@@ -1,27 +0,0 @@
---
description = "Convenient, structured module imports for hosts."
categories = ["Utility"]
features = [ "inventory" ]
---
The importer module allows users to configure importing modules in a flexible and structured way.
It exposes the `extraModules` functionality of the inventory, without any added configuration.
## Usage
```nix
inventory.services = {
importer.base = {
roles.default.tags = [ "all" ];
roles.default.extraModules = [ "modules/base.nix" ];
};
importer.zone1 = {
roles.default.tags = [ "zone1" ];
roles.default.extraModules = [ "modules/zone1.nix" ];
};
};
```
This will import the module `modules/base.nix` to all machines that have the `all` tag,
which by default is every machine managed by the clan.
And also import for all machines tagged with `zone1` the module at `modules/zone1.nix`.

View File

@@ -1 +0,0 @@
{ }

View File

@@ -1,9 +0,0 @@
---
description = "Automatically provisions wifi credentials"
features = [ "inventory", "deprecated" ]
categories = [ "Network" ]
---
!!! Warning
If you've been using network manager + wpa_supplicant and now are switching to IWD read this migration guide:
https://archive.kernel.org/oldwiki/iwd.wiki.kernel.org/networkmanager.html#converting_network_profiles

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,106 +0,0 @@
{
lib,
config,
pkgs,
...
}:
let
cfg = config.clan.iwd;
secret_path = ssid: config.clan.core.vars.generators."iwd.${ssid}".files."iwd.${ssid}".path;
secret_generator = name: value: {
name = "iwd.${value.ssid}";
value =
let
secret_name = "iwd.${value.ssid}";
in
{
prompts.${secret_name} = {
description = "Wifi password for '${value.ssid}'";
persist = true;
};
migrateFact = secret_name;
# ref. man iwd.network
script = ''
config="
[Settings]
AutoConnect=${if value.AutoConnect then "true" else "false"}
[Security]
Passphrase=$(echo -e "$prompt_value/${secret_name}" | ${lib.getExe pkgs.gnused} "s=\\\=\\\\\\\=g;s=\t=\\\t=g;s=\r=\\\r=g;s=^ =\\\s=")
"
echo "$config" > "$out/${secret_name}"
'';
};
};
in
{
options.clan.iwd = {
networks = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
ssid = lib.mkOption {
type = lib.types.str;
default = name;
description = "The name of the wifi network";
};
AutoConnect = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Automatically try to join this wifi network";
};
};
}
)
);
default = { };
description = "Wifi networks to predefine";
};
};
imports = [
(lib.mkRemovedOptionModule [
"clan"
"iwd"
"enable"
] "Just define clan.iwd.networks to enable it")
];
config = lib.mkMerge [
(lib.mkIf (cfg.networks != { }) {
# Systemd tmpfiles rule to create /var/lib/iwd/example.psk file
systemd.tmpfiles.rules = lib.mapAttrsToList (
_: value: "C /var/lib/iwd/${value.ssid}.psk 0600 root root - ${secret_path value.ssid}"
) cfg.networks;
clan.core.vars.generators = lib.mapAttrs' secret_generator cfg.networks;
# TODO: restart the iwd.service if something changes
})
{
warnings = [
"The clan.iwd module is deprecated and will be removed on 2025-07-15. Please migrate to a user-maintained configuration or use the wifi service."
];
# disable wpa supplicant
networking.wireless.enable = false;
# Set the network manager backend to iwd
networking.networkmanager.wifi.backend = "iwd";
# Use iwd instead of wpa_supplicant. It has a user friendly CLI
networking.wireless.iwd = {
enable = true;
settings = {
Network = {
EnableIPv6 = true;
RoutePriorityOffset = 300;
};
Settings.AutoConnect = true;
};
};
}
];
}

View File

@@ -1,3 +0,0 @@
---
description = "Automatically backups current machine to local directory."
---

View File

@@ -1,241 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.clan.localbackup;
uniqueFolders = lib.unique (
lib.flatten (lib.mapAttrsToList (_name: state: state.folders) config.clan.core.state)
);
rsnapshotConfig = target: ''
config_version 1.2
snapshot_root ${target.directory}
sync_first 1
cmd_cp ${pkgs.coreutils}/bin/cp
cmd_rm ${pkgs.coreutils}/bin/rm
cmd_rsync ${pkgs.rsync}/bin/rsync
cmd_ssh ${pkgs.openssh}/bin/ssh
cmd_logger ${pkgs.inetutils}/bin/logger
cmd_du ${pkgs.coreutils}/bin/du
cmd_rsnapshot_diff ${pkgs.rsnapshot}/bin/rsnapshot-diff
${lib.optionalString (target.postBackupHook != null) ''
cmd_postexec ${pkgs.writeShellScript "postexec.sh" ''
set -efu -o pipefail
${target.postBackupHook}
''}
''}
retain snapshot ${builtins.toString config.clan.localbackup.snapshots}
${lib.concatMapStringsSep "\n" (folder: ''
backup ${folder} ${config.networking.hostName}/
'') uniqueFolders}
'';
in
{
options.clan.localbackup = {
targets = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.strMatching "^[a-zA-Z0-9._-]+$";
default = name;
description = "the name of the backup job";
};
directory = lib.mkOption {
type = lib.types.str;
description = "the directory to backup";
};
mountpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "mountpoint of the directory to backup. If set, the directory will be mounted before the backup and unmounted afterwards";
};
preMountHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run before the directory is mounted";
};
postMountHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run after the directory is mounted";
};
preUnmountHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run before the directory is unmounted";
};
postUnmountHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run after the directory is unmounted";
};
preBackupHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run before the backup";
};
postBackupHook = lib.mkOption {
type = lib.types.nullOr lib.types.lines;
default = null;
description = "Shell commands to run after the backup";
};
};
}
)
);
default = { };
description = "List of directories where backups are stored";
};
snapshots = lib.mkOption {
type = lib.types.int;
default = 20;
description = "Number of snapshots to keep";
};
};
config =
let
mountHook = target: ''
if [[ -x /run/current-system/sw/bin/localbackup-mount-${target.name} ]]; then
/run/current-system/sw/bin/localbackup-mount-${target.name}
fi
if [[ -x /run/current-system/sw/bin/localbackup-unmount-${target.name} ]]; then
trap "/run/current-system/sw/bin/localbackup-unmount-${target.name}" EXIT
fi
'';
in
lib.mkIf (cfg.targets != { }) {
environment.systemPackages = [
(pkgs.writeShellScriptBin "localbackup-create" ''
set -efu -o pipefail
export PATH=${
lib.makeBinPath [
pkgs.rsnapshot
pkgs.coreutils
pkgs.util-linux
]
}
${lib.concatMapStringsSep "\n" (target: ''
${mountHook target}
echo "Creating backup '${target.name}'"
${lib.optionalString (target.preBackupHook != null) ''
(
${target.preBackupHook}
)
''}
declare -A preCommandErrors
${lib.concatMapStringsSep "\n" (
state:
lib.optionalString (state.preBackupCommand != null) ''
echo "Running pre-backup command for ${state.name}"
if ! /run/current-system/sw/bin/${state.preBackupCommand}; then
preCommandErrors["${state.name}"]=1
fi
''
) (builtins.attrValues config.clan.core.state)}
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" sync
rsnapshot -c "${pkgs.writeText "rsnapshot.conf" (rsnapshotConfig target)}" snapshot
'') (builtins.attrValues cfg.targets)}'')
(pkgs.writeShellScriptBin "localbackup-list" ''
set -efu -o pipefail
export PATH=${
lib.makeBinPath [
pkgs.jq
pkgs.findutils
pkgs.coreutils
pkgs.util-linux
]
}
(${
lib.concatMapStringsSep "\n" (target: ''
(
${mountHook target}
find ${lib.escapeShellArg target.directory} -mindepth 1 -maxdepth 1 -name "snapshot.*" -print0 -type d \
| jq -Rs 'split("\u0000") | .[] | select(. != "") | { "name": ("${target.name}::" + .)}'
)
'') (builtins.attrValues cfg.targets)
}) | jq -s .
'')
(pkgs.writeShellScriptBin "localbackup-restore" ''
set -efu -o pipefail
export PATH=${
lib.makeBinPath [
pkgs.rsync
pkgs.coreutils
pkgs.util-linux
pkgs.gawk
]
}
if [[ "''${NAME:-}" == "" ]]; then
echo "No backup name given via NAME environment variable"
exit 1
fi
if [[ "''${FOLDERS:-}" == "" ]]; then
echo "No folders given via FOLDERS environment variable"
exit 1
fi
name=$(awk -F'::' '{print $1}' <<< $NAME)
backupname=''${NAME#$name::}
if command -v localbackup-mount-$name; then
localbackup-mount-$name
fi
if command -v localbackup-unmount-$name; then
trap "localbackup-unmount-$name" EXIT
fi
if [[ ! -d $backupname ]]; then
echo "No backup found $backupname"
exit 1
fi
IFS=':' read -ra FOLDER <<< "''$FOLDERS"
for folder in "''${FOLDER[@]}"; do
mkdir -p "$folder"
rsync -a "$backupname/${config.networking.hostName}$folder/" "$folder"
done
'')
]
++ (lib.mapAttrsToList (
name: target:
pkgs.writeShellScriptBin ("localbackup-mount-" + name) ''
set -efu -o pipefail
${lib.optionalString (target.preMountHook != null) target.preMountHook}
${lib.optionalString (target.mountpoint != null) ''
if ! ${pkgs.util-linux}/bin/mountpoint -q ${lib.escapeShellArg target.mountpoint}; then
${pkgs.util-linux}/bin/mount -o X-mount.mkdir ${lib.escapeShellArg target.mountpoint}
fi
''}
${lib.optionalString (target.postMountHook != null) target.postMountHook}
''
) cfg.targets)
++ lib.mapAttrsToList (
name: target:
pkgs.writeShellScriptBin ("localbackup-unmount-" + name) ''
set -efu -o pipefail
${lib.optionalString (target.preUnmountHook != null) target.preUnmountHook}
${lib.optionalString (
target.mountpoint != null
) "${pkgs.util-linux}/bin/umount ${lib.escapeShellArg target.mountpoint}"}
${lib.optionalString (target.postUnmountHook != null) target.postUnmountHook}
''
) cfg.targets;
clan.core.backups.providers.localbackup = {
# TODO list needs to run locally or on the remote machine
list = "localbackup-list";
create = "localbackup-create";
restore = "localbackup-restore";
};
};
}

View File

@@ -1,5 +0,0 @@
---
description = "Securely sharing files and messages over a local network without internet connectivity."
categories = ["Utility"]
features = [ "inventory", "deprecated" ]
---

View File

@@ -1,3 +0,0 @@
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,22 +0,0 @@
{
lib,
writers,
writeShellScriptBin,
localsend,
alias ? null,
}:
let
localsend-ensure-config = writers.writePython3 "localsend-ensure-config" {
flakeIgnore = [
# We don't live in the dark ages anymore.
# Languages like Python that are whitespace heavy will overrun
# 79 characters..
"E501"
];
} (builtins.readFile ./localsend-ensure-config.py);
in
writeShellScriptBin "localsend" ''
set -xeu
${localsend-ensure-config} ${lib.optionalString (alias != null) alias}
${lib.getExe localsend}
''

View File

@@ -1,64 +0,0 @@
import json
import sys
from pathlib import Path
def load_json(file_path: Path) -> dict[str, any]:
try:
with file_path.open("r") as file:
return json.load(file)
except FileNotFoundError:
return {}
def save_json(file_path: Path, data: dict[str, any]) -> None:
with file_path.open("w") as file:
json.dump(data, file, indent=4)
def update_json(file_path: Path, updates: dict[str, any]) -> None:
data = load_json(file_path)
data.update(updates)
save_json(file_path, data)
def config_location() -> str:
config_file = "shared_preferences.json"
config_directory = ".local/share/org.localsend.localsend_app"
config_path = Path.home() / Path(config_directory) / Path(config_file)
return config_path
def ensure_config_directory() -> None:
config_directory = Path(config_location()).parent
config_directory.mkdir(parents=True, exist_ok=True)
def load_config() -> dict[str, any]:
return load_json(config_location())
def save_config(data: dict[str, any]) -> None:
save_json(config_location(), data)
def update_username(username: str, data: dict[str, any]) -> dict[str, any]:
data["flutter.ls_alias"] = username
return data
def main(argv: list[str]) -> None:
try:
display_name = argv[1]
except IndexError:
# This is not an error, just don't update the name
print("No display name provided.")
sys.exit(0)
ensure_config_directory()
updated_data = update_username(display_name, load_config())
save_config(updated_data)
if __name__ == "__main__":
main(sys.argv[:2])

View File

@@ -1,69 +0,0 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.clan.localsend;
in
{
# Integration can be improved, if the following issues get implemented:
# - cli frontend: https://github.com/localsend/localsend/issues/11
# - ipv6 support: https://github.com/localsend/localsend/issues/549
options.clan.localsend = {
displayName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "The name that localsend will use to display your instance.";
};
package = lib.mkPackageOption pkgs "localsend" { };
ipv4Addr = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "192.168.56.2/24";
description = "Optional IPv4 address for ZeroTier network.";
};
};
imports = [
(lib.mkRemovedOptionModule [
"clan"
"localsend"
"enable"
] "Importing the module will already enable the service.")
];
config = {
warnings = [
"The clan.localsend module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
clan.core.state.localsend.folders = [
"/var/localsend"
];
environment.systemPackages = [
(pkgs.callPackage ./localsend-ensure-config {
localsend = config.clan.localsend.package;
alias = config.clan.localsend.displayName;
})
];
networking.firewall.interfaces."zt+".allowedTCPPorts = [ 53317 ];
networking.firewall.interfaces."zt+".allowedUDPPorts = [ 53317 ];
#TODO: This is currently needed because there is no ipv6 multicasting support yet
systemd.network.networks = lib.mkIf (cfg.ipv4Addr != null) {
"09-zerotier" = {
networkConfig = {
Address = cfg.ipv4Addr;
};
};
};
};
}

View File

@@ -1,3 +0,0 @@
---
description = "A federated messaging server with end-to-end encryption."
---

View File

@@ -1,207 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.clan.matrix-synapse;
element-web =
pkgs.runCommand "element-web-with-config" { nativeBuildInputs = [ pkgs.buildPackages.jq ]; }
''
cp -r ${pkgs.element-web} $out
chmod -R u+w $out
jq '."default_server_config"."m.homeserver" = { "base_url": "https://${cfg.app_domain}:443", "server_name": "${cfg.server_tld}" }' \
> $out/config.json < ${pkgs.element-web}/config.json
ln -s $out/config.json $out/config.${cfg.app_domain}.json
'';
in
# FIXME: This was taken from upstream. Drop this when our patch is upstream
{
options.services.matrix-synapse.package = lib.mkOption { readOnly = false; };
options.clan.matrix-synapse = {
server_tld = lib.mkOption {
type = lib.types.str;
description = "The address that is suffixed after your username i.e @alice:example.com";
example = "example.com";
};
app_domain = lib.mkOption {
type = lib.types.str;
description = "The matrix server hostname also serves the element client";
example = "matrix.example.com";
};
users = lib.mkOption {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
description = "The name of the user";
};
admin = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether the user should be an admin";
};
};
}
)
);
description = "A list of users. Not that only new users will be created and existing ones are not modified.";
example.alice = {
admin = true;
};
};
};
imports = [
(lib.mkRemovedOptionModule [
"clan"
"matrix-synapse"
"enable"
] "Importing the module will already enable the service.")
../nginx
];
config = {
services.matrix-synapse = {
enable = true;
settings = {
server_name = cfg.server_tld;
database = {
args.user = "matrix-synapse";
args.database = "matrix-synapse";
name = "psycopg2";
};
turn_uris = [
"turn:turn.matrix.org?transport=udp"
"turn:turn.matrix.org?transport=tcp"
];
registration_shared_secret_path = "/run/synapse-registration-shared-secret";
listeners = [
{
port = 8008;
bind_addresses = [ "::1" ];
type = "http";
tls = false;
x_forwarded = true;
resources = [
{
names = [ "client" ];
compress = true;
}
{
names = [ "federation" ];
compress = false;
}
];
}
];
};
};
clan.core.postgresql.enable = true;
clan.core.postgresql.users.matrix-synapse = { };
clan.core.postgresql.databases.matrix-synapse.create.options = {
TEMPLATE = "template0";
LC_COLLATE = "C";
LC_CTYPE = "C";
ENCODING = "UTF8";
OWNER = "matrix-synapse";
};
clan.core.postgresql.databases.matrix-synapse.restore.stopOnRestore = [ "matrix-synapse" ];
clan.core.vars.generators = {
"matrix-synapse" = {
files."synapse-registration_shared_secret" = { };
runtimeInputs = with pkgs; [
coreutils
pwgen
];
migrateFact = "matrix-synapse";
script = ''
echo -n "$(pwgen -s 32 1)" > "$out"/synapse-registration_shared_secret
'';
};
}
// lib.mapAttrs' (
name: user:
lib.nameValuePair "matrix-password-${user.name}" {
files."matrix-password-${user.name}" = { };
migrateFact = "matrix-password-${user.name}";
runtimeInputs = with pkgs; [ xkcdpass ];
script = ''
xkcdpass -n 4 -d - > "$out"/${lib.escapeShellArg "matrix-password-${user.name}"}
'';
}
) cfg.users;
systemd.services.matrix-synapse =
let
usersScript = ''
while ! ${pkgs.netcat}/bin/nc -z -v ::1 8008; do
if ! kill -0 "$MAINPID"; then exit 1; fi
sleep 1;
done
''
+ lib.concatMapStringsSep "\n" (user: ''
# only create user if it doesn't exist
/run/current-system/sw/bin/matrix-synapse-register_new_matrix_user --exists-ok --password-file ${
config.clan.core.vars.generators."matrix-password-${user.name}".files."matrix-password-${user.name}".path
} --user "${user.name}" ${if user.admin then "--admin" else "--no-admin"}
'') (lib.attrValues cfg.users);
in
{
path = [ pkgs.curl ];
serviceConfig.ExecStartPre = lib.mkBefore [
"+${pkgs.coreutils}/bin/install -o matrix-synapse -g matrix-synapse ${
lib.escapeShellArg
config.clan.core.vars.generators.matrix-synapse.files."synapse-registration_shared_secret".path
} /run/synapse-registration-shared-secret"
];
serviceConfig.ExecStartPost = [
''+${pkgs.writeShellScript "matrix-synapse-create-users" usersScript}''
];
};
services.nginx = {
enable = true;
virtualHosts = {
"${cfg.server_tld}" = {
locations."= /.well-known/matrix/server".extraConfig = ''
add_header Content-Type application/json;
return 200 '${builtins.toJSON { "m.server" = "${cfg.app_domain}:443"; }}';
'';
locations."= /.well-known/matrix/client".extraConfig = ''
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${
builtins.toJSON {
"m.homeserver" = {
"base_url" = "https://${cfg.app_domain}";
};
"m.identity_server" = {
"base_url" = "https://vector.im";
};
}
}';
'';
forceSSL = true;
enableACME = true;
};
"${cfg.app_domain}" = {
forceSSL = true;
enableACME = true;
locations."/".root = element-web;
locations."/_matrix".proxyPass = "http://localhost:8008"; # TODO: We should make the port configurable
locations."/_synapse".proxyPass = "http://localhost:8008";
};
};
};
};
}

View File

@@ -1,5 +0,0 @@
---
description = "A desktop streaming client optimized for remote gaming and synchronized movie viewing."
---
**Warning**: This module was written with our VM integration in mind likely won't work outside of this context. They will be generalized in future.

View File

@@ -1,91 +0,0 @@
{ pkgs, config, ... }:
let
ms-accept = pkgs.callPackage ../../pkgs/moonlight-sunshine-accept { };
defaultPort = 48011;
in
{
warnings = [
"The clan.moonlight module is deprecated and will be removed on 2025-07-15. Please migrate to user-maintained configuration."
];
hardware.opengl.enable = true;
environment.systemPackages = [
pkgs.moonlight-qt
ms-accept
];
systemd.tmpfiles.rules = [
"d '/var/lib/moonlight' 0770 'user' 'users' - -"
"C '/var/lib/moonlight/moonlight.cert' 0644 'user' 'users' - ${
config.clan.core.vars.generators.moonlight.files."moonlight.cert".path or ""
}"
"C '/var/lib/moonlight/moonlight.key' 0644 'user' 'users' - ${
config.clan.core.vars.generators.moonlight.files."moonlight.key".path or ""
}"
];
systemd.user.services.init-moonlight = {
enable = false;
description = "Initializes moonlight";
wantedBy = [ "graphical-session.target" ];
script = ''
${ms-accept}/bin/moonlight-sunshine-accept moonlight init-config --key /var/lib/moonlight/moonlight.key --cert /var/lib/moonlight/moonlight.cert
'';
serviceConfig = {
user = "user";
Type = "oneshot";
WorkingDirectory = "/home/user/";
RunTimeDirectory = "moonlight";
TimeoutSec = "infinity";
Restart = "on-failure";
RemainAfterExit = true;
ReadOnlyPaths = [
"/var/lib/moonlight/moonlight.key"
"/var/lib/moonlight/moonlight.cert"
];
};
};
systemd.user.services.moonlight-join = {
description = "Join sunshine hosts";
script = ''${ms-accept}/bin/moonlight-sunshine-accept moonlight join --port ${builtins.toString defaultPort} --cert '${
config.clan.core.vars.generators.moonlight.files."moonlight.cert".value or ""
}' --host fd2e:25da:6035:c98f:cd99:93e0:b9b8:9ca1'';
serviceConfig = {
Type = "oneshot";
TimeoutSec = "infinity";
Restart = "on-failure";
ReadOnlyPaths = [
"/var/lib/moonlight/moonlight.key"
"/var/lib/moonlight/moonlight.cert"
];
};
};
systemd.user.timers.moonlight-join = {
description = "Join sunshine hosts";
wantedBy = [ "timers.target" ];
timerConfig = {
OnUnitActiveSec = "5min";
OnBootSec = "0min";
Persistent = true;
Unit = "moonlight-join.service";
};
};
clan.core.vars.generators.moonlight = {
migrateFact = "moonlight";
files."moonlight.key" = { };
files."moonlight.cert" = { };
files."moonlight.cert".secret = false;
runtimeInputs = [
pkgs.coreutils
ms-accept
];
script = ''
moonlight-sunshine-accept moonlight init
mv credentials/cakey.pem "$out"/moonlight.key
cp credentials/cacert.pem "$out"/moonlight.cert
mv credentials/cacert.pem "$out"/moonlight.cert
'';
};
}

View File

@@ -1,19 +0,0 @@
---
description = "Open Source, Low Latency, High Quality Voice Chat."
categories = ["Audio", "Social"]
features = [ "inventory" ]
[constraints]
roles.server.min = 1
---
The mumble clan module gives you:
- True low latency voice communication.
- Secure, authenticated encryption.
- Free software.
- Backed by a large and active open-source community.
This all set up in a way that allows peer-to-peer hosting.
Every machine inside the clan can be a host for mumble,
and thus it doesn't matter who in the network is online - as long as two people are online they are able to chat with each other.

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/server.nix ];
}

View File

@@ -1,247 +0,0 @@
import argparse
import json
import sqlite3
from pathlib import Path
def ensure_config(path: Path, db_path: Path) -> None:
# Default JSON structure if the file doesn't exist
default_json = {
"misc": {
"audio_wizard_has_been_shown": True,
"database_location": str(db_path),
"viewed_server_ping_consent_message": True,
},
"settings_version": 1,
}
# Check if the file exists
if path.exists():
data = json.loads(path.read_text())
else:
data = default_json
# Create the file with default JSON structure
with path.open("w") as file:
json.dump(data, file, indent=4)
# TODO: make sure to only update the diff
updated_data = {**default_json, **data}
# Write the modified JSON object back to the file
with path.open("w") as file:
json.dump(updated_data, file, indent=4)
def initialize_database(db_location: str) -> None:
"""
Initializes the database. If the database or the servers table does not exist, it creates them.
:param db_location: The path to the SQLite database
"""
conn = sqlite3.connect(db_location)
try:
cursor = conn.cursor()
# Create the servers table if it doesn't exist
cursor.execute("""
CREATE TABLE IF NOT EXISTS servers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
hostname TEXT NOT NULL,
port INTEGER NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
url TEXT
)
""")
# Commit the changes
conn.commit()
except sqlite3.Error as e:
print(f"An error occurred while initializing the database: {e}")
finally:
conn.close()
def initialize_certificates(
db_location: str, hostname: str, port: str, digest: str
) -> None:
# Connect to the SQLite database
conn = sqlite3.connect(db_location)
try:
# Create a cursor object
cursor = conn.cursor()
# TODO: check if cert already there
# if server_check(cursor, name, hostname):
# print(
# f"Server with name '{name}' and hostname '{hostname}' already exists."
# )
# return
# SQL command to insert data into the servers table
insert_query = """
INSERT INTO cert (hostname, port, digest)
VALUES (?, ?, ?)
"""
# Data to be inserted
data = (hostname, port, digest)
# Execute the insert command with the provided data
cursor.execute(insert_query, data)
# Commit the changes
conn.commit()
print("Data has been successfully inserted.")
except sqlite3.Error as e:
print(f"An error occurred: {e}")
finally:
# Close the connection
conn.close()
def calculate_digest(cert: str) -> str:
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
cert = cert.strip()
cert = cert.encode("utf-8")
cert = x509.load_pem_x509_certificate(cert, default_backend())
digest = cert.fingerprint(hashes.SHA1()).hex()
return digest
def server_check(cursor: str, name: str, hostname: str) -> bool:
"""
Check if a server with the given name and hostname already exists.
:param cursor: The database cursor
:param name: The name of the server
:param hostname: The hostname of the server
:return: True if the server exists, False otherwise
"""
check_query = """
SELECT 1 FROM servers WHERE name = ? AND hostname = ?
"""
cursor.execute(check_query, (name, hostname))
return cursor.fetchone() is not None
def insert_server(
name: str,
hostname: str,
port: str,
username: str,
password: str,
url: str,
db_location: str,
) -> None:
"""
Inserts a new server record into the servers table.
:param name: The name of the server
:param hostname: The hostname of the server
:param port: The port number
:param username: The username
:param password: The password
:param url: The URL
"""
# Connect to the SQLite database
conn = sqlite3.connect(db_location)
try:
# Create a cursor object
cursor = conn.cursor()
if server_check(cursor, name, hostname):
print(
f"Server with name '{name}' and hostname '{hostname}' already exists."
)
return
# SQL command to insert data into the servers table
insert_query = """
INSERT INTO servers (name, hostname, port, username, password, url)
VALUES (?, ?, ?, ?, ?, ?)
"""
# Data to be inserted
data = (name, hostname, port, username, password, url)
# Execute the insert command with the provided data
cursor.execute(insert_query, data)
# Commit the changes
conn.commit()
print("Data has been successfully inserted.")
except sqlite3.Error as e:
print(f"An error occurred: {e}")
finally:
# Close the connection
conn.close()
if __name__ == "__main__":
port = 64738
password = ""
url = None
parser = argparse.ArgumentParser(
prog="initialize_mumble",
)
subparser = parser.add_subparsers(dest="certificates")
# cert_parser = subparser.add_parser("certificates")
parser.add_argument("--cert")
parser.add_argument("--digest")
parser.add_argument("--machines")
parser.add_argument("--servers")
parser.add_argument("--username")
parser.add_argument("--db-location")
parser.add_argument("--ensure-config", type=Path)
args = parser.parse_args()
print(args)
if args.ensure_config:
ensure_config(args.ensure_config, args.db_location)
print("Initialized config")
exit(0)
if args.servers:
print(args.servers)
servers = json.loads(f"{args.servers}")
db_location = args.db_location
for server in servers:
digest = calculate_digest(server.get("value"))
name = server.get("name")
initialize_certificates(db_location, name, port, digest)
print("Initialized certificates")
exit(0)
initialize_database(args.db_location)
# Insert the server into the database
print(args.machines)
machines = json.loads(f"{args.machines}")
print(machines)
print(list(machines))
for machine in list(machines):
print(f"Inserting {machine}.")
insert_server(
machine,
machine,
port,
args.username,
password,
url,
args.db_location,
)

View File

@@ -1,150 +0,0 @@
{
lib,
config,
pkgs,
...
}:
let
dir = config.clan.core.settings.directory;
# TODO: this should actually use the inventory to figure out which machines to use.
machineDir = dir + "/vars/per-machine";
machinesFileSet = builtins.readDir machineDir;
machines = lib.mapAttrsToList (name: _: name) machinesFileSet;
machineJson = builtins.toJSON machines;
certificateMachinePath = machines: machineDir + "/${machines}" + "/mumble/mumble-cert/value";
certificatesUnchecked = builtins.map (
machine:
let
fullPath = certificateMachinePath machine;
in
if builtins.pathExists fullPath then machine else null
) machines;
certificate = lib.filter (machine: machine != null) certificatesUnchecked;
machineCert = builtins.map (
machine: (lib.nameValuePair machine (builtins.readFile (certificateMachinePath machine)))
) certificate;
machineCertJson = builtins.toJSON machineCert;
in
{
options.clan.services.mumble = {
user = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "alice";
description = "The user mumble should be set up for.";
};
};
config = {
warnings = [
"The clan.mumble module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
services.murmur = {
enable = true;
logDays = -1;
registerName = config.clan.core.settings.machine.name;
openFirewall = true;
bonjour = true;
sslKey = "/var/lib/murmur/sslKey";
sslCert = "/var/lib/murmur/sslCert";
};
clan.core.state.mumble.folders = [
"/var/lib/mumble"
"/var/lib/murmur"
];
systemd.tmpfiles.rules = [
"d '/var/lib/mumble' 0770 '${config.clan.services.mumble.user}' 'users' - -"
];
systemd.tmpfiles.settings."murmur" = {
"/var/lib/murmur/sslKey" = {
C.argument = config.clan.core.vars.generators.mumble.files.mumble-key.path;
Z = {
mode = "0400";
user = "murmur";
};
};
"/var/lib/murmur/sslCert" = {
C.argument = config.clan.core.vars.generators.mumble.files.mumble-cert.path;
Z = {
mode = "0400";
user = "murmur";
};
};
};
environment.systemPackages =
let
mumbleCfgDir = "/var/lib/mumble";
mumbleDatabasePath = "${mumbleCfgDir}/mumble.sqlite";
mumbleCfgPath = "/var/lib/mumble/mumble_settings.json";
populate-channels = pkgs.writers.writePython3 "mumble-populate-channels" {
libraries = [
pkgs.python3Packages.cryptography
pkgs.python3Packages.pyopenssl
];
flakeIgnore = [
# We don't live in the dark ages anymore.
# Languages like Python that are whitespace heavy will overrun
# 79 characters..
"E501"
];
} (builtins.readFile ./mumble-populate-channels.py);
mumble = pkgs.writeShellScriptBin "mumble" ''
set -xeu
mkdir -p ${mumbleCfgDir}
pushd "${mumbleCfgDir}"
XDG_DATA_HOME=${mumbleCfgDir}
XDG_DATA_DIR=${mumbleCfgDir}
${populate-channels} --ensure-config '${mumbleCfgPath}' --db-location ${mumbleDatabasePath}
${populate-channels} --machines '${machineJson}' --username ${config.clan.core.settings.machine.name} --db-location ${mumbleDatabasePath}
${populate-channels} --servers '${machineCertJson}' --username ${config.clan.core.settings.machine.name} --db-location ${mumbleDatabasePath} --cert True
${pkgs.mumble}/bin/mumble --config ${mumbleCfgPath} "$@"
popd
'';
in
[ mumble ];
clan.core.vars.generators.mumble = {
migrateFact = "mumble";
files.mumble-key = { };
files.mumble-cert.secret = false;
runtimeInputs = [
pkgs.coreutils
pkgs.openssl
];
script = ''
openssl genrsa -out "$out/mumble-key" 2048
cat > mumble-cert.conf <<EOF
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
prompt = no
[ req_distinguished_name ]
C = "US"
ST = "California"
L = "San Francisco"
O = "Clan"
OU = "Clan"
CN = "${config.clan.core.settings.machine.name}"
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
DNS.1 = "${config.clan.core.settings.machine.name}"
EOF
openssl req -new -x509 -config mumble-cert.conf -key "$out/mumble-key" -out "$out/mumble-cert" < /dev/null
'';
};
};
}

View File

@@ -1,30 +0,0 @@
---
description = "End-2-end encrypted IPv6 overlay network"
categories = ["System", "Network"]
features = [ "inventory", "deprecated" ]
---
Mycelium is an IPv6 overlay network written in Rust. Each node that joins the overlay network will receive an overlay network IP in the 400::/7 range.
Features:
- Mycelium, is locality aware, it will look for the shortest path between nodes
- All traffic between the nodes is end-2-end encrypted
- Traffic can be routed over nodes of friends, location aware
- If a physical link goes down Mycelium will automatically reroute your traffic
- The IP address is IPV6 and linked to private key
- A simple reliable messagebus is implemented on top of Mycelium
- Mycelium has multiple ways how to communicate quic, tcp, ... and we are working on holepunching for Quick which means P2P traffic without middlemen for NATted networks e.g. most homes
- Scalability is very important for us, we tried many overlay networks before and got stuck on all of them, we are trying to design a network which scales to a planetary level
- You can run mycelium without TUN and only use it as reliable message bus.
An example configuration might look like this in the inventory:
```nix
mycelium.default = {
roles.peer.machines = [
"berlin"
"munich"
];
};
```
This will add the machines named `berlin` and `munich` to the `mycelium` vpn.

View File

@@ -1,51 +0,0 @@
{
pkgs,
config,
lib,
...
}:
{
options = {
clan.mycelium.openFirewall = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Open the firewall for mycelium";
};
clan.mycelium.addHostedPublicNodes = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Add hosted Public nodes";
};
};
config.warnings = [
"The clan.mycelium module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
config.services.mycelium = {
enable = true;
addHostedPublicNodes = lib.mkDefault config.clan.mycelium.addHostedPublicNodes;
openFirewall = lib.mkDefault config.clan.mycelium.openFirewall;
keyFile = config.clan.core.vars.generators.mycelium.files.key.path;
};
config.clan.core.vars.generators.mycelium = {
files."key" = { };
files."ip".secret = false;
files."pubkey".secret = false;
runtimeInputs = [
pkgs.mycelium
pkgs.coreutils
pkgs.jq
];
script = ''
timeout 5 mycelium --key-file "$out"/key || :
mycelium inspect --key-file "$out"/key --json | jq -r .publicKey > "$out"/pubkey
mycelium inspect --key-file "$out"/key --json | jq -r .address > "$out"/ip
'';
};
}

View File

@@ -1,3 +0,0 @@
---
description = "Good defaults for the nginx webserver"
---

View File

@@ -1,68 +0,0 @@
{ config, lib, ... }:
{
imports = [
(lib.mkRemovedOptionModule [
"clan"
"nginx"
"enable"
] "Importing the module will already enable the service.")
];
options = {
clan.nginx.acme.email = lib.mkOption {
type = lib.types.str;
description = ''
Email address for account creation and correspondence from the CA.
It is recommended to use the same email for all certs to avoid account
creation limits.
'';
};
};
config = {
security.acme.acceptTerms = true;
security.acme.defaults.email = config.clan.nginx.acme.email;
networking.firewall.allowedTCPPorts = [
443
80
];
services.nginx = {
enable = true;
statusPage = lib.mkDefault true;
recommendedBrotliSettings = lib.mkDefault true;
recommendedGzipSettings = lib.mkDefault true;
recommendedOptimisation = lib.mkDefault true;
recommendedProxySettings = lib.mkDefault true;
recommendedTlsSettings = lib.mkDefault true;
# Nginx sends all the access logs to /var/log/nginx/access.log by default.
# instead of going to the journal!
commonHttpConfig = "access_log syslog:server=unix:/dev/log;";
resolver.addresses =
let
isIPv6 = addr: builtins.match ".*:.*:.*" addr != null;
escapeIPv6 = addr: if isIPv6 addr then "[${addr}]" else addr;
cloudflare = [
"1.1.1.1"
"2606:4700:4700::1111"
];
resolvers =
if config.networking.nameservers == [ ] then cloudflare else config.networking.nameservers;
in
map escapeIPv6 resolvers;
sslDhparam = config.security.dhparams.params.nginx.path;
};
security.dhparams = {
enable = true;
params.nginx = { };
};
};
}

View File

@@ -1,5 +0,0 @@
---
description = "Define package sets from nixpkgs and install them on one or more machines"
categories = ["System"]
features = [ "inventory", "deprecated" ]
---

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,25 +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 = {
warnings = [
"The clan.packages module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
environment.systemPackages = map (
pName: lib.getAttrFromPath (lib.splitString "." pName) pkgs
) config.clan.packages.packages;
};
}

View File

@@ -1,3 +0,0 @@
---
description = "A free and open-source relational database management system (RDBMS) emphasizing extensibility and SQL compliance."
---

View File

@@ -1,9 +0,0 @@
{ lib, ... }:
{
imports = [
(lib.mkRemovedOptionModule [
"clan"
"postgresql"
] "The postgresql module has been migrated to a clan core option. Use clan.core.postgresql instead")
];
}

View File

@@ -1,20 +0,0 @@
---
description = "Automatically generates and configures a password for the root user."
categories = ["System"]
features = ["inventory", "deprecated"]
---
This module is deprecated and will be removed in a future release. It's functionality has been replaced by the user-password service.
After the system was installed/deployed the following command can be used to display the root-password:
```bash
clan vars get [machine_name] root-password/root-password
```
See also: [Vars](../../concepts/generators.md)
To regenerate the password run:
```
clan vars generate --regenerate [machine_name] --generator root-password
```

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,50 +0,0 @@
{
_class,
pkgs,
config,
lib,
...
}:
{
warnings = [
"The clan.root-password module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
users.mutableUsers = false;
users.users.root.hashedPasswordFile =
config.clan.core.vars.generators.root-password.files.password-hash.path;
clan.core.vars.generators.root-password = {
files.password-hash = {
neededFor = "users";
}
// (lib.optionalAttrs (_class == "nixos") {
restartUnits = lib.optional (config.services.userborn.enable) "userborn.service";
});
files.password = {
deploy = false;
};
migrateFact = "root-password";
runtimeInputs = [
pkgs.coreutils
pkgs.mkpasswd
pkgs.xkcdpass
];
prompts.password.type = "hidden";
prompts.password.persist = true;
prompts.password.description = "You can autogenerate a password, if you leave this prompt blank.";
script = ''
prompt_value="$(cat "$prompts"/password)"
if [[ -n "''${prompt_value-}" ]]; then
echo "$prompt_value" | tr -d "\n" > "$out"/password
else
xkcdpass --numwords 4 --delimiter - --count 1 | tr -d "\n" > "$out"/password
fi
mkpasswd -s -m sha-512 < "$out"/password | tr -d "\n" > "$out"/password-hash
'';
};
}

View File

@@ -1,43 +0,0 @@
---
description = "Configures partitioning of the main disk"
categories = ["System"]
features = [ "inventory" ]
---
# Primary Disk Layout
A module for the "disk-layout" category MUST be chosen.
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"
}
}
}
}
}
}
```

View File

@@ -1,3 +0,0 @@
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,56 +0,0 @@
{ lib, config, ... }:
{
options.clan.single-disk = {
device = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = "The primary disk device to install the system on";
};
};
config = {
warnings = [
"clanModules.single-disk is deprecated. Please copy the disko config from the module into your machine 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";
mountOptions = [ "umask=0077" ];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
};
};
}

View File

@@ -1,11 +0,0 @@
---
description = "Enables secure remote access to the machine over ssh."
categories = ["System", "Network"]
features = [ "inventory", "deprecated" ]
---
This module will setup the opensshd service.
It will generate a host key for each machine
## Roles

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/server.nix ];
}

View File

@@ -1,6 +0,0 @@
{ ... }:
{
imports = [
../shared.nix
];
}

View File

@@ -1,106 +0,0 @@
{
config,
pkgs,
lib,
...
}:
let
stringSet = list: builtins.attrNames (builtins.groupBy lib.id list);
domains = stringSet config.clan.sshd.certificate.searchDomains;
cfg = config.clan.sshd;
in
{
imports = [ ../shared.nix ];
options = {
clan.sshd.hostKeys.rsa.enable = lib.mkEnableOption "Generate RSA host key";
};
config = {
warnings = [
"The clan.sshd module is deprecated and will be removed on 2025-07-15.
Please migrate to user-maintained configuration or the new equivalent clan services
(https://docs.clan.lol/reference/clanServices)."
];
services.openssh = {
enable = true;
settings.PasswordAuthentication = false;
settings.HostCertificate = lib.mkIf (
cfg.certificate.searchDomains != [ ]
) config.clan.core.vars.generators.openssh-cert.files."ssh.id_ed25519-cert.pub".path;
hostKeys = [
{
path = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519".path;
type = "ed25519";
}
]
++ lib.optional cfg.hostKeys.rsa.enable {
path = config.clan.core.vars.generators.openssh-rsa.files."ssh.id_rsa".path;
type = "rsa";
};
};
clan.core.vars.generators.openssh = {
files."ssh.id_ed25519" = { };
files."ssh.id_ed25519.pub".secret = false;
migrateFact = "openssh";
runtimeInputs = [
pkgs.coreutils
pkgs.openssh
];
script = ''
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/ssh.id_ed25519
'';
};
programs.ssh.knownHosts.clan-sshd-self-ed25519 = {
hostNames = [
"localhost"
config.networking.hostName
]
++ (lib.optional (config.networking.domain != null) config.networking.fqdn);
publicKey = config.clan.core.vars.generators.openssh.files."ssh.id_ed25519.pub".value;
};
clan.core.vars.generators.openssh-rsa = lib.mkIf config.clan.sshd.hostKeys.rsa.enable {
files."ssh.id_rsa" = { };
files."ssh.id_rsa.pub".secret = false;
runtimeInputs = [
pkgs.coreutils
pkgs.openssh
];
script = ''
ssh-keygen -t rsa -b 4096 -N "" -C "" -f "$out"/ssh.id_rsa
'';
};
clan.core.vars.generators.openssh-cert = lib.mkIf (cfg.certificate.searchDomains != [ ]) {
files."ssh.id_ed25519-cert.pub".secret = false;
dependencies = [
"openssh"
"openssh-ca"
];
validation = {
name = config.clan.core.settings.machine.name;
domains = lib.genAttrs config.clan.sshd.certificate.searchDomains lib.id;
};
runtimeInputs = [
pkgs.openssh
pkgs.jq
];
script = ''
ssh-keygen \
-s $in/openssh-ca/id_ed25519 \
-I ${config.clan.core.settings.machine.name} \
-h \
-n ${lib.concatMapStringsSep "," (d: "${config.clan.core.settings.machine.name}.${d}") domains} \
$in/openssh/ssh.id_ed25519.pub
mv $in/openssh/ssh.id_ed25519-cert.pub "$out"/ssh.id_ed25519-cert.pub
'';
};
};
}

View File

@@ -1,49 +0,0 @@
{
config,
lib,
pkgs,
...
}:
{
options = {
clan.sshd.certificate = {
# TODO: allow per-server domains that we than collect in the inventory
#domains = lib.mkOption {
# type = lib.types.listOf lib.types.str;
# default = [ ];
# example = [ "git.mydomain.com" ];
# description = "List of domains to include in the certificate. This option will not prepend the machine name in front of each domain.";
#};
searchDomains = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "mydomain.com" ];
description = "List of domains to include in the certificate. This option will prepend the machine name in front of each domain before adding it to the certificate.";
};
};
};
config = {
clan.core.vars.generators.openssh-ca =
lib.mkIf (config.clan.sshd.certificate.searchDomains != [ ])
{
share = true;
files.id_ed25519.deploy = false;
files."id_ed25519.pub" = {
deploy = false;
secret = false;
};
runtimeInputs = [
pkgs.openssh
];
script = ''
ssh-keygen -t ed25519 -N "" -C "" -f "$out"/id_ed25519
'';
};
programs.ssh.knownHosts.ssh-ca = lib.mkIf (config.clan.sshd.certificate.searchDomains != [ ]) {
certAuthority = true;
extraHostNames = builtins.map (domain: "*.${domain}") config.clan.sshd.certificate.searchDomains;
publicKey = config.clan.core.vars.generators.openssh-ca.files."id_ed25519.pub".value;
};
};
}

View File

@@ -1,18 +0,0 @@
---
description = "Automatically generate the state version of the nixos installation."
features = [ "inventory", "deprecated" ]
---
This module generates the `system.stateVersion` of the nixos installation automatically.
Options: [system.stateVersion](https://search.nixos.org/options?channel=unstable&show=system.stateVersion&from=0&size=50&sort=relevance&type=packages&query=stateVersion)
Migration:
If you are already setting `system.stateVersion`, then import the module and then either let the automatic generation happen, or trigger the generation manually for the machine. The module will take the specified version, if one is already supplied through the config.
To manually generate the version for a specified machine run:
```
clan vars generate [MACHINE]
```
If the setting was already set you can then remove `system.stateVersion` from your machine configuration. For new machines, just import the module.

View File

@@ -1,6 +0,0 @@
# Dont import this file
# It is only here for backwards compatibility.
# Dont author new modules with this file.
{
imports = [ ./roles/default.nix ];
}

View File

@@ -1,28 +0,0 @@
{ config, lib, ... }:
let
var = config.clan.core.vars.generators.state-version.files.version or { };
in
{
warnings = [
''
The clan.state-version service is deprecated and will be
removed on 2025-07-15 in favor of a nix option.
Please migrate your configuration to use `clan.core.settings.state-version.enable = true` instead.
''
];
system.stateVersion = lib.mkDefault (lib.removeSuffix "\n" var.value);
clan.core.vars.generators.state-version = {
files.version = {
secret = false;
value = lib.mkDefault config.system.nixos.release;
};
runtimeInputs = [ ];
script = ''
echo -n ${config.system.stateVersion} > "$out"/version
'';
};
}

View File

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

View File

@@ -1,63 +0,0 @@
{ lib, config, ... }:
{
options.clan.static-hosts = {
excludeHosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default =
if config.clan.static-hosts.topLevelDomain != "" then
[ ]
else
[ config.clan.core.settings.machine.name ];
defaultText = lib.literalExpression ''
if config.clan.static-hosts.topLevelDomain != "" then
[ ]
else
[ config.clan.core.settings.machine.name ];
'';
description = "Hosts that should be excluded";
};
topLevelDomain = lib.mkOption {
type = lib.types.str;
default = "";
description = "Top level domain to reach hosts";
};
};
config.networking.hosts =
let
dir = config.clan.core.settings.directory;
machineDir = "${dir}/vars/per-machine";
zerotierIpMachinePath = machine: "${machineDir}/${machine}/zerotier/zerotier-ip/value";
machinesFileSet = builtins.readDir machineDir;
machines = lib.mapAttrsToList (name: _: name) machinesFileSet;
networkIpsUnchecked = builtins.map (
machine:
let
fullPath = zerotierIpMachinePath machine;
in
if builtins.pathExists fullPath then machine else null
) machines;
networkIps = lib.filter (machine: machine != null) networkIpsUnchecked;
machinesWithIp = lib.filterAttrs (name: _: (lib.elem name networkIps)) machinesFileSet;
filteredMachines = lib.filterAttrs (
name: _: !(lib.elem name config.clan.static-hosts.excludeHosts)
) machinesWithIp;
in
lib.filterAttrs (_: value: value != null) (
lib.mapAttrs' (
machine: _:
let
path = zerotierIpMachinePath machine;
in
if builtins.pathExists path then
lib.nameValuePair (builtins.readFile path) (
if (config.clan.static-hosts.topLevelDomain == "") then
[ machine ]
else
[ "${machine}.${config.clan.static-hosts.topLevelDomain}" ]
)
else
{ }
) filteredMachines
);
}

View File

@@ -1,5 +0,0 @@
---
description = "A desktop streaming server optimized for remote gaming and synchronized movie viewing."
---
**Warning**: This module was written with our VM integration in mind likely won't work outside of this context. They will be generalized in future.

View File

@@ -1,203 +0,0 @@
{
pkgs,
config,
lib,
...
}:
let
ms-accept = pkgs.callPackage ../../pkgs/moonlight-sunshine-accept { };
sunshineConfiguration = pkgs.writeText "sunshine.conf" ''
address_family = both
channels = 5
pkey = /var/lib/sunshine/sunshine.key
cert = /var/lib/sunshine/sunshine.cert
file_state = /var/lib/sunshine/state.json
credentials_file = /var/lib/sunshine/credentials.json
'';
listenPort = 48011;
in
{
warnings = [
"The clan.sunshine module is deprecated and will be removed on 2025-07-15. Please migrate to user-maintained configuration."
];
networking.firewall = {
allowedTCPPorts = [
47984
47989
47990
48010
48011
];
allowedUDPPorts = [
47998
47999
48000
48002
48010
];
};
networking.firewall.allowedTCPPortRanges = [
{
from = 47984;
to = 48010;
}
];
networking.firewall.allowedUDPPortRanges = [
{
from = 47998;
to = 48010;
}
];
environment.systemPackages = [
ms-accept
pkgs.sunshine
pkgs.avahi
# Convenience script, until we find a better UX
(pkgs.writers.writeDashBin "sun" ''
${pkgs.sunshine}/bin/sunshine -0 ${sunshineConfiguration} "$@"
'')
# Create a dummy account, for easier setup,
# don't use this account in actual production yet.
(pkgs.writers.writeDashBin "init-sun" ''
${pkgs.sunshine}/bin/sunshine \
--creds "sunshine" "sunshine"
'')
];
# Required to simulate input
boot.kernelModules = [ "uinput" ];
services.udev.extraRules = ''
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", TAG+="uaccess"
'';
security = {
rtkit.enable = true;
wrappers.sunshine = {
owner = "root";
group = "root";
capabilities = "cap_sys_admin+p";
source = "${pkgs.sunshine}/bin/sunshine";
};
};
systemd.tmpfiles.rules = [
"d '/var/lib/sunshine' 0770 'user' 'users' - -"
"C '/var/lib/sunshine/sunshine.cert' 0644 'user' 'users' - ${
config.clan.core.vars.generators.sunshine.files."sunshine.cert".path or ""
}"
"C '/var/lib/sunshine/sunshine.key' 0644 'user' 'users' - ${
config.clan.core.vars.generators.sunshine.files."sunshine.key".path or ""
}"
];
hardware.graphics.enable = true;
systemd.user.services.sunshine = {
enable = true;
description = "Sunshine self-hosted game stream host for Moonlight";
startLimitBurst = 5;
startLimitIntervalSec = 500;
script = "/run/current-system/sw/bin/env /run/wrappers/bin/sunshine ${sunshineConfiguration}";
serviceConfig = {
Restart = "on-failure";
RestartSec = "5s";
ReadWritePaths = [ "/var/lib/sunshine" ];
ReadOnlyPaths = [
(config.clan.core.vars.services.sunshine.files."sunshine.key".path or "")
(config.clan.core.vars.services.sunshine.files."sunshine.cert".path or "")
];
};
wantedBy = [ "graphical-session.target" ];
partOf = [ "graphical-session.target" ];
wants = [ "graphical-session.target" ];
after = [
"sunshine-init-state.service"
"sunshine-init-credentials.service"
];
};
systemd.user.services.sunshine-init-state = {
enable = true;
description = "Sunshine self-hosted game stream host for Moonlight";
startLimitBurst = 5;
startLimitIntervalSec = 500;
script = ''
${ms-accept}/bin/moonlight-sunshine-accept sunshine init-state \
--uuid ${config.clan.core.vars.generators.sunshine.files.sunshine-uuid.value} \
--state-file /var/lib/sunshine/state.json
'';
serviceConfig = {
Restart = "on-failure";
RestartSec = "5s";
Type = "oneshot";
ReadWritePaths = [ "/var/lib/sunshine" ];
};
wantedBy = [ "graphical-session.target" ];
};
systemd.user.services.sunshine-init-credentials = {
enable = true;
description = "Sunshine self-hosted game stream host for Moonlight";
startLimitBurst = 5;
startLimitIntervalSec = 500;
script = ''
${lib.getExe pkgs.sunshine} ${sunshineConfiguration} --creds sunshine sunshine
'';
serviceConfig = {
Restart = "on-failure";
RestartSec = "5s";
Type = "oneshot";
ReadWritePaths = [ "/var/lib/sunshine" ];
};
wantedBy = [ "graphical-session.target" ];
};
systemd.user.services.sunshine-listener = {
enable = true;
description = "Sunshine self-hosted game stream host for Moonlight";
startLimitBurst = 5;
startLimitIntervalSec = 500;
script = ''
${ms-accept}/bin/moonlight-sunshine-accept sunshine listen --port ${builtins.toString listenPort} \
--uuid ${config.clan.core.vars.generators.sunshine.files.sunshine-uuid.value} \
--state /var/lib/sunshine/state.json --cert '${
config.clan.core.vars.generators.sunshine.files."sunshine.cert".value
}'
'';
serviceConfig = {
# );
Restart = "on-failure";
RestartSec = 5;
ReadWritePaths = [ "/var/lib/sunshine" ];
};
wantedBy = [ "graphical-session.target" ];
};
clan.core.vars.generators.sunshine = {
# generator was named incorrectly in the past
migrateFact = "ergochat";
files."sunshine.key" = { };
files."sunshine.cert" = { };
files."sunshine-uuid".secret = false;
files."sunshine.cert".secret = false;
runtimeInputs = [
pkgs.coreutils
ms-accept
];
script = ''
moonlight-sunshine-accept sunshine init
mv credentials/cakey.pem "$out"/sunshine.key
cp credentials/cacert.pem "$out"/sunshine.cert
mv credentials/cacert.pem "$out"/sunshine.cert
mv uuid "$out"/sunshine-uuid
'';
};
}

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