Compare commits

...

383 Commits

Author SHA1 Message Date
yukkop e38c0c499f fix: gitea: now works 2026-05-29 21:27:53 +00:00
snuff 7d254cd8c4 fix(gitea)?: syntax 2026-05-29 20:38:16 +03:00
snuff 2e66319cfc fix(gitea)?: postgres socket peer auth for gitea 2026-05-29 20:17:08 +03:00
snuff e41f0aff7e fix?: +gitea v3 2026-05-29 20:07:27 +03:00
snuff ab4fa3140e fix?: +gitea v2 2026-05-29 18:43:31 +03:00
snuff 3130ae3dd7 fix?: +gitea 2026-05-29 18:37:02 +03:00
snuff fe8ff3d895 feat?: +gitea 2026-05-29 18:29:47 +03:00
yukkop f4ec839241 feat: matrix-cluster: hz 2026-05-27 19:04:56 +00:00
yukkop 5dc83564af Merge branch 'master' of github.com:hectic-lab/util.nix 2026-05-27 18:51:33 +00:00
yukkop a68f3a329a fix!: matrix-cluster: +jitsy 2026-05-27 18:51:18 +00:00
yukkop 74ed204e6b fix: mechabellum: api 2026-05-27 18:48:29 +00:00
yukkop 8625000952 feat: matrix-cluster: shared users 2026-05-27 12:54:15 +00:00
yukkop a8dd82d05f feat: matrix-cluster: shared users 2026-05-27 12:41:51 +00:00
yukkop 92f55320b5 fix: matrix 2026-05-25 23:12:05 +00:00
yukkop 4ce1945abe feat: matrix prepare move to poland 2026-05-25 07:48:16 +00:00
yukkop e29f09a694 feat: matrix: diferent servers one home server 2026-05-25 07:11:43 +00:00
yukkop ecabc0b6c2 fix: some recomendations 2026-05-24 15:08:40 +00:00
yukkop bcf5ae5da7 fix: element 2026-05-24 14:54:01 +00:00
yukkop 0eecea6324 fix: element 2026-05-23 23:11:58 +00:00
yukkop 4c0a178646 some 2026-05-23 22:57:09 +00:00
yukkop 9c6de9d067 fix: hectic-lab: mechabellum 2026-05-23 20:53:30 +00:00
yukkop 934c02609a feat: hectic-lab: mechabellum for lismy 2026-05-23 19:40:36 +00:00
yukkop fb6ee3cb54 chore: devide xrays systems 2026-05-23 18:27:02 +00:00
yukkop bce720fcff chore: hectic-lab: +vismajor in matrix 2026-05-23 18:17:27 +00:00
yukkop 7f39fc2dc3 fix 2026-05-23 17:13:49 +00:00
yukkop 819f1691b4 chore: hecitc-lab: +lvgkcfjl email to hectic-lab 2026-05-23 14:11:27 +00:00
yukkop 0a6e97f456 fix: media-browser: view button 2026-05-23 12:18:05 +00:00
yukkop d220a4588f fix: media-browser: synced files 2026-05-23 12:03:56 +00:00
yukkop 7dc66abc21 feat: media-browser: preview for local files 2026-05-23 11:56:04 +00:00
yukkop e38bf7582d feat: media-browser for matrix 2026-05-23 11:46:58 +00:00
yukkop afd4970cf4 fix: matrix: s3 sync 2026-05-23 11:22:49 +00:00
yukkop ef59f14372 feat: matrix: s3 object storage 2026-05-23 09:59:34 +00:00
yukkop d86eeae4a5 fix?: support bot 2026-05-23 08:50:10 +00:00
yukkop 4d7908de4b chore: hecitc-lab: add lvgkcfjl to matrix 2026-05-23 08:49:51 +00:00
yukkop 20b12d42a8 feat: hectic-lab: matrix 100mb max file 2026-05-23 06:47:34 +00:00
yukkop 51d40841ce feat: hectic-lab: users for matrix 2026-05-22 21:19:52 +00:00
yukkop 09ed045da9 feat: module: ~matrix 2026-05-22 20:13:47 +00:00
yukkop 56881b766a feat: +package merge-archive 2026-05-22 07:45:47 +00:00
yukkop 19b95e707d feat: deploy: 1 no tty option 2026-05-05 20:19:52 +00:00
yukkop e71886c34e feat: lab: +models 2026-05-04 10:19:38 +00:00
yukkop da999fc37b feat: lab: configure olama models 2026-05-04 05:41:25 +00:00
yukkop f9c292643d Merge branch 'master' of github.com:hectic-lab/util.nix 2026-05-03 22:07:13 +00:00
yukkop bcdc8f0836 feat: lab: configure olama 2026-05-03 22:06:57 +00:00
yukkop 66722bd8a4 fix: db-tool: NO_TTY 2026-05-03 15:35:22 +00:00
yukkop 1aaf21e9c0 fix: posix-helpers 2026-05-03 05:23:23 +00:00
yukkop 4204d68eed fix(db-tool): prevent fd leak from logger to long-running daemons
The hectic logger opens fd 3 as a dup of stderr. Child processes inherit
this fd, and daemonized PostgreSQL/PostgREST keeping it open prevents the
terminal from returning to the prompt after the spawning script exits.

- Add with_closed_fds helper that runs commands in a subshell with fds
  3-9 redirected to /dev/null
- Inline the helper into both database and postgres-init builds
- Wrap pg_ctl start and postgrest with the helper
2026-05-03 03:14:15 +00:00
yukkop 4378b13877 feat: windows-devshell 2026-05-02 15:31:26 +00:00
yukkop c041f2e88d fix: linux-devshell for ubuntu 2026-05-01 23:11:23 +00:00
yukkop 61861ff170 fix: linux-devshell from root 2026-05-01 22:56:03 +00:00
yukkop d64314d68b chore: fix nixpkgs 25.05 deprecation warnings 2026-05-01 22:26:29 +00:00
yukkop 35e35980b4 test: fix: . 2026-05-01 22:19:43 +00:00
yukkop 0a54a41670 fix: wsl system 2026-05-01 21:57:41 +00:00
yukkop d5d4d5a5c3 test: arch 2026-05-01 21:40:54 +00:00
yukkop ff0766519c test: linux-devshell 2026-05-01 21:13:15 +00:00
yukkop 87b57d5011 feat: linux-devshell: init 2026-05-01 20:39:38 +00:00
yukkop e46a4bf8a1 fix: db-tool: envcontent 2026-05-01 12:31:44 +00:00
yukkop 8b7a9b5141 fix(db-tool): typo hydate.stdout.log -> hydrate.stdout.log 2026-04-30 23:10:46 +00:00
yukkop d3cdbdf3e2 docs(postgres-hooks): document hectic bundle + responsibility split
Add lib/hook/sql/README.md describing bundle layout, apply order, Nix API
(self.lib.hectic.*), shell helper contract, and the steps for adding a new
SQL file. Rewrite db-tool README's hectic section: drop stale
PG_HECTIC_INHERITANCE / HECTIC_INHERITANCE_SQL env vars, add
HECTIC_DOTENV_FILE, document the postgres-init / migrator init / database
hydrate responsibility split.
2026-04-30 22:12:18 +00:00
yukkop 1a209f6960 test(postgres-hooks): retarget hectic bundle tests to migrator init + db-tool hydrate
Move postgres-init-hectic-inheritance test (13 cases) to
migrator/init-hectic-bundle since the bundle is now applied by `migrator init`
instead of `postgres-init`. Drop init-migrator-with-inherits since
`--inherits` is now a deprecation warning, not an error. Add db-tool
hydrate-hook test (5 cases) covering --no-hook skip, default apply,
idempotency, and HECTIC_DOTENV_FILE. Augment init-migrator with
hectic.version and hectic.secret table assertions.
2026-04-30 22:12:09 +00:00
yukkop 3d5e3fdb36 feat: postgres hooks 2026-04-30 21:59:53 +00:00
yukkop bf7ee34716 feat: start impliment postgres hooks 2026-04-30 21:36:22 +00:00
yukkop 70c44f1fa7 feat: include hectic-landing to hectic 2026-04-30 19:33:21 +00:00
yukkop 2eaa568f5b feat(db-tool): hectic-inheritance: add hectic.immutable + diff coverage
Add a hectic.immutable parent table. Tables inheriting it get auto-attached
BEFORE INSERT/UPDATE/DELETE/TRUNCATE row+statement triggers that block DML
unless the session sets hectic.migration_mode='on' (intended use: SET LOCAL
inside a migration transaction). Same exemptions as the rest of the bundle
apply (hectic schema, partitions, temp tables, GUC-excluded schemas).

database diff now appends an --- IMMUTABLE TABLE DATA --- section to its
output, with per-table unified row diffs of every table inheriting
hectic.immutable, surfacing drift in 'frozen' reference data alongside schema
drift. Subcommand exits non-zero when either schema or data differs.

Test postgres-init-hectic-inheritance extended to 10 cases covering
immutable triggers, DML blocked outside migration_mode, SET LOCAL allowing
DML inside a transaction, GUC not leaking past COMMIT, and TRUNCATE under
migration_mode.
2026-04-30 16:10:38 +00:00
yukkop 31d2994997 feat(db-tool): postgres-init: apply hectic-inheritance by default
Flip PG_HECTIC_INHERITANCE default 0 -> 1. Set PG_HECTIC_INHERITANCE=0 to opt out.
2026-04-30 15:48:33 +00:00
yukkop 58e9aa8fad feat(db-tool): hectic-inheritance: bootstrap hectic.created_at / hectic.updated_at inheritance hook
Adds a SQL bundle plus event triggers that enforce `INHERITS (hectic.created_at)`
on every user CREATE TABLE and auto-attach a BEFORE UPDATE row trigger when a
table inherits `hectic.updated_at`. Always-exempt: `hectic`, `information_schema`,
`pg_*`, declarative partitions, temp tables. Per-DB opt-out via the GUC
`hectic.inheritance_extra_excluded_schemas`.

Exposed three ways:
  * `pkgs.hectic.hectic-inheritance` — derivation with the SQL at
    $out/share/hectic/hectic-inheritance.sql
  * `self.lib.hecticInheritance.{sql,path}` — pkgs-free Nix surface
  * `postgres-init` opt-in via `PG_HECTIC_INHERITANCE=1` (HECTIC_INHERITANCE_SQL
    overrides the default)

Test postgres-init-hectic-inheritance covers all six branches: bootstrap,
non-inheriting reject, accepting inheritance, auto updated_at trigger fires,
GUC exclusion, declarative partition exemption.
2026-04-30 15:16:57 +00:00
yukkop 7d5300853b fix(db-tool): postgres-init: createdb on reuse when target DB missing
Previously when PG_REUSE=1 and PG_VERSION existed but the target database had
never been successfully created (e.g. devshell exited mid-init in a prior run),
postgres-init skipped createdb and the subsequent psql connection failed with
'database "<db>" does not exist'.

Now on reuse path we probe pg_database and create the target DB if missing,
making postgres-init fully idempotent across stale-state recovery.

Adds postgres-init-reuse-missing-db test.
2026-04-30 12:16:09 +00:00
yukkop e732ecb878 feat(db-tool): expose overridable postgresql arg and PG_CONF_FILE env
Wrap db-tool, postgres-init, postgres-cleanup with lib.makeOverridable so
consumers can inject extension-enabled PostgreSQL via .override { postgresql = ...; }.
Add PG_CONF_FILE: when set, replaces script-generated postgresql.conf entirely
(runtime port and unix_socket_directories still appended/overridden).
2026-04-30 12:00:08 +00:00
yukkop 705f5bbd01 fix(db-tool): postgres-init: always invoke main so sourcing exports vars
Previous basename guard only ran the function when invoked as the
postgres-init binary, but consumer shellHooks source the script to
inherit POSTGRESQL_HOST/PGURL/etc. Sourcing left the function defined
but never called, leaving WIPE_PGURL/PGURL unset and breaking devshell
entry in proxydoe and ruststats.
2026-04-30 11:37:19 +00:00
yukkop 06a5d16ee3 fix(package): db-tool: call postgres-init/cleanup binaries instead of local devshell scripts
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-30 10:10:11 +00:00
yukkop 5713eaad14 fix(package): db-tool: default PGURL to empty in dispatch to fix pull_staging exit=3 contract 2026-04-30 09:24:58 +00:00
yukkop b5dcbf08a1 feat(\db-tool\): introduce unified db-tool package with postgres harness and tests (T0-T8) 2026-04-30 09:06:44 +00:00
yukkop 395bddee94 Merge branch 'master' of github.com:hectic-lab/util.nix 2026-04-29 09:57:40 +00:00
snuff d8312b69b8 feat?(neuro): create aeronautics minecraft server 2026-04-28 14:04:14 +03:00
yukkop 62757df384 fix(package): sentinèlla: use dig(1) instead of host(1) which is not in bind.dnsutils 2026-04-27 17:30:01 +00:00
yukkop f2ea372742 debug(package): sentinèlla: surface host(1) and SRV-parse failures in watcher 2026-04-27 17:28:01 +00:00
yukkop f158a80492 fix(package): sentinèlla: use ip(8) for local-IP detection (inetutils hostname lacks -I); harden numeric parsing 2026-04-27 17:21:00 +00:00
yukkop 0c81c0454d feat(package): sentinèlla: switch peer discovery from A records to SRV records 2026-04-27 14:08:23 +00:00
yukkop 7268ec3704 fix(package): sentinèlla: probe — use socat EXEC pipes to avoid pty CR/LF mangling 2026-04-27 13:34:52 +00:00
yukkop 0dfbee6472 feat: sentinella: some 2026-04-27 12:58:36 +00:00
yukkop 50d682d6f7 fix: hectic-lab: email name 2026-04-26 22:22:31 +00:00
yukkop 3ca4d4fb86 feat: sentinella: update 2026-04-26 22:22:15 +00:00
yukkop 4fae68764c feat(hectic-lab): enable sentinèlla watcher service
Enable the p2p watcher alongside the existing probe. Peers are discovered
via DNS name peers.hectic-lab.com. TG credentials are loaded from a SOPS
secret sentinèlla/watcher/environment (to be populated with TG_TOKEN and
TG_CHAT_ID).
2026-04-26 22:04:35 +00:00
yukkop 6035397e9b feat(sentinèlla): p2p topology with DNS peer discovery
- Replace central sentinel with watcher: each node polls peers discovered
  via a single DNS name with multiple A records (e.g. peers.sentinella.com)
- Auto-detect own IPs via hostname -I; SELF env var available as optional
  override for NAT/floating-IP setups
- Fix Basic Auth bug in router.sh: compare tok against AUTH_TOKENS instead
  of unset $USER/$PASS
- Rename sentinel binary to watcher; drop unused shellplot dep
- Add inetutils to watcher runtime deps for hostname -I
- Update NixOS module: replace sentinel options with watcher p2p options
  (peersDns, self, peersPort, peersScheme, pollingIntervalSec)
- Add sentinèlla test suite: probe-status-empty, probe-disk, watcher-state-file
2026-04-26 21:54:07 +00:00
yukkop 249d027f6b docs: +plan 2026-04-26 14:11:37 +00:00
yukkop 3343a8be6e Merge branch 'merge-hectic-lab' 2026-04-26 13:52:17 +00:00
yukkop b8fc0971c6 fix: hectic-lab: something 2026-04-26 13:48:36 +00:00
yukkop 406acf662e feat: hectic-lab: mail server 2026-04-26 10:33:28 +00:00
yukkop 970c1214af chore: config 2026-04-25 15:18:01 +00:00
yukkop 0316d0b535 zalupa 2026-04-18 20:53:44 +00:00
yukkop 305efdbbae feat: work on profinity 2026-04-14 08:43:56 +00:00
yukkop 6e72841337 docs: neuro: router forwarding 2026-03-31 20:09:27 +00:00
yukkop 29acdc9ad6 feat: bfs: add xray package 2026-03-31 19:59:40 +00:00
yukkop 04bc83e004 chore: bfs: add 3 cert 2026-03-23 19:47:54 +00:00
yukkop 2513b76658 some jitsi shit 2026-03-23 19:22:40 +00:00
yukkop 62b7742ef0 some 2026-03-22 06:55:10 +00:00
yukkop f23720e151 feat: wsl: some things 2026-03-15 10:59:19 +00:00
yukkop b9730784ba fix: deploy: logs 2026-03-01 09:36:22 +00:00
yukkop 577c167d5a feat: migrator: +multifiles migrations 2026-02-28 21:19:29 +00:00
yukkop 525c6a220b chore: 2026-02-28 20:25:13 +00:00
yukkop bf41ee6e0a feat: merge own wsl config 2026-02-28 01:57:42 +00:00
yukkop cfcd6647ae feat: init hectic-lab merge 2026-02-27 19:57:44 +00:00
yukkop 4e2264dc03 feat: neuro: some 2026-02-27 12:53:51 +00:00
yukkop 3414edf2a8 fix: some warnings 2026-02-23 18:27:36 +00:00
yukkop 0d7b84e3bc refactor: derustification 2026-02-20 17:36:20 +00:00
yukkop bea9ca602e fix: +fixed nixpkgs 2026-02-20 17:30:36 +00:00
yukkop d8a51cd389 refactor: update to 25.11 2026-02-20 17:18:38 +00:00
yukkop f93e12accd feat: nixvim + zsh, fix: warnings 2026-02-20 16:41:18 +00:00
yukkop 63a7fd716f apply patch from neuro 2026-02-20 12:51:32 +00:00
yukkop 799ad9a15d feat: +neuro devshell 2026-02-20 12:06:55 +00:00
yukkop 9010b2678e refactor: deploy: update logs 2026-02-20 12:04:15 +00:00
yukkop 26bdecb8e0 fix: migrator: more accurate check for hectic.migrator isntalled 2026-02-17 16:50:08 +00:00
yukkop ff87e8c411 chore: something about hemar tree sitter 2026-02-17 16:17:47 +00:00
yukkop d7ada535d4 feat: +pol bfs server 2026-02-17 16:13:58 +00:00
yukkop 7490ef2fe9 fix: nvim-pager close all pages on q 2026-02-17 08:55:41 +00:00
yukkop a7cb54d04c hz 2026-02-14 18:35:04 +00:00
yukkop 0f4f6a6339 hz 2026-02-14 15:59:13 +00:00
yukkop 043b333654 feat: migrator: colorize help in migrator.sh 2026-02-06 08:38:36 +00:00
yukkop 683370a71c feat: stong &2 in log.sh 2026-02-05 06:50:54 +00:00
yukkop 2724295bcf feat(nixos): postgresql: +settings script 2026-01-29 18:56:22 +00:00
yukkop 78a3329271 Merge branch 'master' of github.com:hectic-lab/util.nix 2026-01-16 16:16:43 +00:00
yukkop a61f2cfe5e feat(nixos): +zombro hardware 2026-01-16 16:16:13 +00:00
ISNUFFI ed5c1cc1af chore: removed excess variables 2026-01-13 18:13:52 +03:00
yukkop 8ffe9ad244 iso 2026-01-13 15:04:33 +00:00
yukkop bf72ac7e97 feat(template): +nodejs 2026-01-04 20:40:43 +00:00
yukkop 4a9bb42015 feat(nixos): some moves to nixos server 2025-12-28 19:18:15 +00:00
yukkop 96782b7b51 feat(nixos): +neuro system 2025-12-26 20:04:42 +00:00
yukkop 3d84864382 fix(package): migrator: sqlite transactions 2025-12-23 19:31:44 +00:00
yukkop cb96de6125 fix(package): migrator: handle sqlite posible settings 2025-12-23 18:30:26 +00:00
yukkop 686a794a69 fix(pakcage): migrator: -z -> +x 2025-12-23 18:18:51 +00:00
yukkop 2ffe187b12 fix(package): migrator: awk dependency 2025-12-21 13:19:46 +00:00
yukkop 89ab7d56af feat(pakcage): migrator: add dependency 2025-12-20 01:19:40 +00:00
yukkop 01f13723a8 feat(package): migrator: up to latest 2025-12-18 00:45:09 +00:00
yukkop 9c25e82c79 fix(package): migrator: uncomplite RAISE on sqlite 2025-12-17 03:28:03 +00:00
yukkop 956239ab79 feat(package): migrator: ! sqlite support 2025-12-17 03:24:59 +00:00
yukkop bb2ae34758 feat(package): migrator: mvp 2025-12-16 17:28:36 +00:00
yukkop 13fdfac2ef test(hemar): update parser test to match better paths 2025-12-09 14:21:03 +00:00
yukkop 93a7636c61 fix(package): hemar: paths better handling 2025-12-09 14:13:34 +00:00
yukkop 6fb3ee1af8 test(hemar): many tests but not pass 2025-12-09 13:49:43 +00:00
yukkop 73dc9aee9e feat(package): hemar: json_escape() 2025-12-09 03:47:27 +00:00
yukkop 71082b6414 feat(package): hemar: interpolation 2025-12-08 23:47:33 +00:00
yukkop 65a96c860b feat(package): accord: some cli things 2025-12-08 01:55:33 +00:00
yukkop 9b9254bb93 feat(nixos): bgs: vless 2025-12-07 03:40:07 +00:00
yukkop 1beb980b95 refactor(nixos): bfs: some secrets 2025-12-07 02:25:29 +00:00
yukkop 082b80f252 feate(package): accord: init 2025-12-06 23:49:14 +00:00
yukkop f4a65afb01 Merge branch 'master' of github.com:hectic-lab/util.nix 2025-12-06 04:47:46 +00:00
yukkop 373a3fe3f0 fix(package): migrator: index_of subshell issue 2025-12-06 04:47:31 +00:00
snuff 8bbc0eda8f fix: matrix works with self-hosted element 2025-12-06 00:36:01 +03:00
yukkop 3b506a7888 fix: bfs nixosConfiguration check 2025-12-05 21:01:53 +00:00
yukkop 9a5bfa3f80 feate(nixos) bfs: matrix, element, bla bla bla 2025-12-05 19:36:53 +00:00
yukkop 5177f9a1f6 feat(nixos): bfs: fix: element-rtc 2025-12-05 18:17:41 +00:00
yukkop 49bf4c6d91 feat(nixos): bfs: +matrix, very unsave 2025-12-05 16:30:34 +00:00
yukkop 9d914ed863 feat(nixos): netherlands xray 2025-12-05 00:56:08 +00:00
yukkop 3e6bf6ce7f feat(nixos): xray 2025-12-04 21:03:01 +00:00
yukkop b786098867 feat(package): migrator: something 2025-12-04 21:02:38 +00:00
yukkop dfa76b2a19 feat(package): hemar: more grammar parsing 2025-11-30 13:31:40 +00:00
yukkop 6dad0d95fb fix(nixos): generic postgresql 2025-11-29 13:48:57 +00:00
yukkop 78f4777141 feat(package): hemar: some parser work 2025-11-29 00:30:27 +00:00
yukkop 48077aaccc feat(package): hemar: add indexes to tree-sitter 2025-11-28 10:29:59 +00:00
yukkop 4b4ea7a1d2 feat(package): hemar: add tree sitter hightlights 2025-11-27 19:45:12 +00:00
yukkop d1353d3a36 fix(package): hemar: treesitter language name 2025-11-27 19:15:35 +00:00
yukkop 8048c4adff fix(package): hemar: treesitter language name 2025-11-27 19:13:07 +00:00
yukkop 345f94742e feat(package): hemar: tree sitter grammar 2025-11-27 17:53:49 +00:00
yukkop 777d48bf3d feat(package): hemar: antlr grammar, but still does not work 2025-11-26 14:48:51 +00:00
yukkop 8a08272f68 feat(package): hemar: I think I was wrong 2025-11-25 15:45:02 +00:00
yukkop 4729770b59 feat(package): hemar: some stages of parsing 2025-11-25 13:19:45 +00:00
yukkop f5d412997e feat(package): hemar: conected to nix config 2025-11-24 14:33:45 +00:00
yukkop 783cbe20e8 fix(package): migrator: init migration 2025-11-22 11:50:42 +00:00
yukkop 9a7e7d677a feat(package): migrator: some migrate up works and init 2025-11-17 16:26:25 +00:00
yukkop 5b1f05589d test: fix: migrator: multifile test 2025-11-17 01:57:32 +00:00
yukkop 00f05cc3db feat(legacy): helpers: allow HECTIC_NAMESPACE be empty 2025-11-16 16:56:26 +00:00
yukkop bb99ef9a8a feat(legacy): helpers: update log, unefficient but works 2025-11-16 16:38:38 +00:00
yukkop 33cdebb64a refactor: remove nix warnings 2025-11-14 21:13:32 +00:00
yukkop 88573ea4aa feat(package): nbt2json: pack into nix 2025-11-14 21:12:41 +00:00
yukkop 4ae460f6e3 fix(template): correct hectic utils input 2025-11-14 15:53:12 +00:00
yukkop 5a8fd0a275 feat(package): voronoi: simple voronoi algoritm realization 2025-11-14 15:51:28 +00:00
yukkop 6204ba5abb feat(package): migrator: ! migration up 2025-11-14 15:50:55 +00:00
yukkop 00bbd42cab feat(package): migrator: init migration table 2025-11-04 13:21:15 +00:00
yukkop 111db5a1ca feat(package): migrator: create new migration 2025-11-04 12:15:29 +00:00
yukkop 9d97fd8d87 refactor(package): pull out all realization from default.nix 2025-11-03 20:08:20 +00:00
yukkop 94fbbdf0d9 fix(package): in default.nix migratior -> migrator 2025-11-03 19:15:57 +00:00
yukkop 43dc59a04f feat: +checks flake output 2025-11-03 19:02:50 +00:00
yukkop e3167ce6e5 feat(package): +migrator 2025-11-03 18:55:35 +00:00
yukkop 532f4b0901 feat(legacy): +writeDash +helpers 2025-11-03 18:51:38 +00:00
yukkop ad00891e6b fix(nixos): hardwares 2025-10-30 09:38:58 +00:00
yukkop 60bd185fa4 feat(nixos): cloudzy hardware 2025-10-28 16:16:01 +00:00
yukkop f08bd1e702 feat(template): +avarage 2025-10-28 11:41:26 +00:00
yukkop d01674e6a4 feat(package): sentinella: sqlschema 2025-10-28 11:37:22 +00:00
yukkop 39abe553f5 refactor(package): sentinella: +logging 2025-10-25 09:54:57 +00:00
yukkop af5d46b3f0 fix(nixos): module: wrong separator 2025-10-25 07:21:07 +00:00
yukkop 450ba9229b feat(package): sentinel: some changes 2025-10-25 06:48:48 +00:00
yukkop 9903e83f68 feat(package): deploy: rollback 2025-10-23 14:48:10 +00:00
yukkop 3ee41979bd refactor(nixos): modules: hetzner network config 2025-10-23 11:49:34 +00:00
yukkop b400acd18e some fixes 2025-10-23 11:28:56 +00:00
yukkop dd0d823e36 feat(package): +deploy 2025-10-23 10:22:48 +00:00
yukkop fafc1c4664 refactor(nixos): modules: hetzner hardware 2025-10-22 09:32:41 +00:00
yukkop 3bfc5799c7 feat(nixos): modules: +network configuration to hetzner hardware 2025-10-22 08:20:27 +00:00
yukkop e7423780e8 ruined every thing again 2025-10-21 17:27:09 +00:00
yukkop b3041edbdc ruined every thing 2025-10-20 17:17:08 +00:00
yukkop dbe4dfc2bc feat(package): sentinèlla: sentinel: loging 2025-10-19 15:16:38 +00:00
yukkop abdc808693 feat(nixos): lenovo hardware 2025-10-16 12:22:19 +00:00
yukkop 3139fd1d9f fix: lib systems 2025-10-16 09:52:49 +00:00
yukkop dc6ca7a0a9 fix(nixos): senttinèlla-probe: module args 2025-10-12 08:59:03 +00:00
yukkop 6dd7d112c5 fix: writers & overlays 2025-10-11 14:56:25 +00:00
yukkop 65fa944df8 refactor: legacyPackages 2025-10-10 13:13:03 +00:00
yukkop 7d55064a44 feat(package): onlinepubs: init 2025-10-08 15:58:35 +00:00
yukkop a1b31b1684 fix(package): `sentinèlla one more unset error 2025-10-07 10:55:37 +00:00
yukkop d446b24c47 fix(package): `sentinèlla unset error 2025-10-07 04:02:49 +00:00
yukkop 76e09ce5c3 fix(package): sentinèlla: base64 logic 2025-10-03 19:02:33 +00:00
yukkop 7fd3ba46c9 feat(package): sentinèlla: add auth 2025-10-03 02:30:24 +00:00
yukkop 0eb95197bc feat(package): hemar: continios work 2025-10-01 11:09:09 +00:00
yukkop 38bf9151eb refactor: hemar reboot 2025-09-28 04:44:46 +00:00
yukkop 5dd03a947f feat: appropriate sops dream wrapper 2025-09-12 17:17:29 +00:00
yukkop adbd946f50 fix 2025-09-02 10:33:04 +00:00
yukkop 78dcb215cc fix: stupid error 2025-08-31 16:01:00 +00:00
yukkop 744a8c9cf4 refactor(nixos): module: hectic services config path 2025-08-28 18:17:18 +00:00
yukkop 00e02f328a feat(nixos): module: +support-bot 2025-08-28 15:02:58 +00:00
yukkop 7dadd2290f feat(package): shellplot: created 2025-08-23 16:37:08 +00:00
yukkop 25575a69f3 feat(package): server-health: created 2025-08-23 15:16:15 +00:00
yukkop bb16555d4f feat: add templates 2025-08-05 04:41:27 +00:00
yukkop 492597b7ab fix(package): nix-derivation-hash: purify 2025-07-24 19:33:51 +00:00
yukkop 52cb6b2410 feat(package): nix-derivation-hash 2025-07-23 20:15:53 +00:00
yukkop 6e77853453 feat(lib): shell: local-dir 2025-07-22 21:15:42 +00:00
yukkop 8f4ff74546 feat: service.postgresql module extension 2025-07-22 02:11:32 +00:00
yukkop e74ef30dc6 fix(lib): shell: remove unneccessary ''$ 2025-07-18 03:23:25 +00:00
yukkop 3516d24868 fix(nixos): module: user 2025-07-17 16:53:48 +00:00
yukkop 92def64fea feat(lib): shell: +check-tool 2025-07-17 16:22:31 +00:00
yukkop be3aab9b48 fix(lib): logs: mkdir error work around 2025-07-17 16:17:43 +00:00
yukkop 5cac466f90 feat(nixos): module: +user.yukkop 2025-07-17 16:10:04 +00:00
yukkop 8cbfdcc0ea refactor: consistence 2025-07-17 16:00:59 +00:00
yukkop 3d0fd7a030 refactor: pull out flake devshells 2025-07-17 15:48:20 +00:00
yukkop 33422320a5 refactor: pull out flake packages 2025-07-17 15:28:29 +00:00
yukkop 23d9db27e6 refactor: pull out systems 2025-07-17 15:21:06 +00:00
yukkop 20dcbdfdc4 feat: modules 2025-07-17 13:37:33 +00:00
yukkop 8fb996501c feat: shell modules 2025-07-16 06:36:27 +00:00
yukkop b433fd7ea0 feat: +some python packages 2025-07-06 22:12:14 +00:00
yukkop f0596784a1 feat: remove vmw_pvscsi on aarch64-linux 2025-07-05 23:24:58 +00:00
yukkop 5670f11510 feat(package): slpt: jq dependency 2025-07-03 01:21:44 +00:00
yukkop 0db6ce3dd4 feat(package): update slpt 2025-07-02 16:08:30 +00:00
yukkop 2c16cebf9a feat(package) add slpt 2025-07-01 02:26:34 +00:00
yukkop 497213a2ea fix(package): bolt-unpack update nodejs_22 2025-06-30 23:48:14 +00:00
yukkop c73c887a9c Merge branch 'master' of github.com:hectic-lab/util.nix 2025-06-30 21:00:04 +00:00
yukkop f7fda629f5 refactor: move to nixpkgs 25.05 2025-06-30 20:43:38 +00:00
yukkop 5e112f5df8 fix: plsh 2025-06-30 16:31:04 +00:00
yukkop 510c350b7a fix: remove broken haskell 2025-06-30 16:24:56 +00:00
yukkop 8a13b8c8fd feat: plsh 2025-06-30 16:06:43 +00:00
yukkop 0ab2061776 feat: aliases to system preset 2025-06-11 22:38:54 +00:00
yukkop 8631eac0d4 chore: pager AnsiEsc 2025-06-11 22:25:34 +00:00
yukkop 9c8a3790c5 feat: find a way separate pg extension from overlay 2025-05-22 11:57:09 +00:00
yukkop 70253c9a3a done 2025-05-21 20:00:02 +00:00
yukkop 3b7c4d71e7 checkpoint 2025-05-21 19:50:47 +00:00
yukkop d1b0d7154d test: hemar: fix parse tests according code cahnges 2025-05-19 14:36:37 +00:00
yukkop 7ce48d75f8 fix: hemar: EBUCHIE WHITESPACЫ 2025-05-19 13:59:26 +00:00
yukkop 6c953563d0 fix: hemar: section whitespaces checkpoint 2025-05-19 11:07:10 +00:00
yukkop ef314495ba fix: hemar: include object does not exists 2025-05-18 20:18:54 +00:00
yukkop 1b1deaf721 test: hemar: test all at once 2025-05-18 19:58:04 +00:00
yukkop 746779e952 feat: hemar: included tag 2025-05-18 18:51:21 +00:00
yukkop 5cabf6c04d fix: hemar: section implimentation 2025-05-18 13:20:57 +00:00
yukkop 3493c14686 feat: hemar: section checkpoint 2025-05-18 10:27:38 +00:00
yukkop 19ea8992a0 fix: hemar: jsonb_value_by_path string copy 2025-05-17 17:59:10 +00:00
yukkop a17a20b7d6 chore: hemar: more logs for god of logs 2025-05-17 15:56:46 +00:00
yukkop eabfbbdc95 feat: hemar: separate jsonb_get_by_path form postgresql 2025-05-17 14:30:18 +00:00
yukkop 86d30ea494 chore: forgoten changes 2025-05-17 14:13:11 +00:00
yukkop bcc755d325 test: exec 2025-05-17 13:39:40 +00:00
yukkop 6a2f7cd1fa test: hemar: more tests for god of tests 2025-05-16 01:18:01 +00:00
yukkop 39495115a2 feat: hemar: exec handle the {{ }} inside 2025-05-16 00:37:30 +00:00
yukkop f0d7ca60fe fix: hemar: parser 2025-05-16 00:30:11 +00:00
yukkop 725f6eea30 test: hemar: puh 2025-05-15 23:57:18 +00:00
yukkop 421cc6e870 test: hemar: +20 tests for parsing 2025-05-15 18:54:56 +00:00
yukkop 653330dd48 test: hemar: +10 complex tests 2025-05-15 17:53:45 +00:00
yukkop 54684c0f78 feat: find element in jsonb by path is works!!! 2025-05-15 17:48:28 +00:00
yukkop c28e7fcfd5 feat!: vpizdu 2025-05-15 15:33:53 +00:00
yukkop a770215f0c feat: hemar: handling [] in path 2025-05-14 21:52:01 +00:00
yukkop b3172edbd7 feat: something 2025-05-14 21:30:16 +00:00
yukkop 83029b52ea feat: array with objects 2025-05-14 18:56:42 +00:00
yukkop 56fa799d53 fix: more parsing errors handling, section body fix 2025-05-14 14:09:48 +00:00
yukkop e9ba38f4eb feat!: to the moon 2025-05-14 12:15:45 +00:00
yukkop bcb8afbbf4 feat!: one more checkpoint 2025-05-13 21:56:27 +00:00
yukkop 598eae7e71 feat!: better? 2025-05-13 21:41:37 +00:00
yukkop e260e8f7cc feat!: some section parse issue 2025-05-13 15:44:55 +00:00
yukkop 7f58985c41 refactor: gemar grand rework 2025-05-13 12:12:57 +00:00
yukkop 88327be038 fix: hemar & hectic: nothing 2025-05-13 02:30:15 +00:00
yukkop 609bae352f fix: hemar: unbeliveble, without memmory error? not true 2025-05-13 01:15:43 +00:00
yukkop 5f52fc42af feat!: many work useles with hemar 2025-05-10 11:00:36 +00:00
yukkop 4db5cc171b fix: hectic C: template parse sections 2025-05-04 19:33:52 +00:00
yukkop 9b045b71f9 fix: hectic C: template parse 2025-05-04 13:49:29 +00:00
yukkop e800f11103 refactor: hemar: logs 2025-04-24 00:37:03 +00:00
yukkop e55437b856 feat: hel -> hemar--amend 2025-04-23 12:47:28 +00:00
yukkop cda46a7e13 feat: static page for fun 2025-04-23 12:37:32 +00:00
yukkop cc7de6c0dd feat hectic C: logger log in file 2025-04-23 01:04:49 +00:00
yukkop e25190422b feat hectic C: no asan option 2025-04-22 15:09:47 +00:00
zerosummed 016db3d06a feat(package): +support-bot 2025-04-22 10:00:33 +03:00
yukkop 286555e026 feat: wrapper for pg_dump 2025-04-22 01:13:23 +00:00
yukkop dc93bada89 feat: wrapper for pg_dumpall 2025-04-22 00:44:52 +00:00
yukkop 8857835409 refactor: hectic C: static-asan 2025-04-20 13:38:16 +00:00
yukkop 30778b1955 refactor: hectic C: perfavor 2025-04-20 13:32:22 +00:00
yukkop 791ff77e14 refactor: hectic C: libhectic.so -> hectic.so 2025-04-20 09:57:32 +00:00
yukkop 17acafd50a fix: hel: anus linking 2025-04-20 00:33:34 +00:00
yukkop 7ddceab11c build: hectic C: shared library in build 2025-04-19 23:44:39 +00:00
yukkop 0abd4fc70e fix: hectic: lib building 2025-04-19 23:23:09 +00:00
yukkop f97e451af1 fix: hel: Makefile LIBS 2025-04-19 23:10:13 +00:00
yukkop a9d64fe059 feat: hel: c89... 2025-04-19 23:04:29 +00:00
yukkop 62abe7b846 feat: hel: "" -> <> 2025-04-19 22:38:58 +00:00
yukkop 0c58befc43 feat: hecitc C: add hectic-config 2025-04-19 22:05:56 +00:00
yukkop 2229cf072b feat: hel: render without execute 2025-04-19 20:30:00 +00:00
yukkop 10a0a08dcd fix: overlay postgres extensions 2025-04-19 02:39:10 +00:00
zerosummed 60d977110c fix?(package): postgreact: ~stir the stupid shit till it works [3] 2025-04-18 21:37:23 +03:00
zerosummed aec4876832 fix?(package): postgreact: ~stir the stupid shit till it works [2] 2025-04-17 07:06:34 +03:00
zerosummed 90f588843a fix?(package): postgreact: ~stir the stupid shit till it works [1] 2025-04-17 07:06:31 +03:00
zerosummed fb8fa34ff2 fix?(package): postgreact 2025-04-17 06:22:21 +03:00
zerosummed 29ff6989e5 style: ~apply alejandra 2025-04-17 03:57:54 +03:00
zerosummed 3dfe3d61f6 refactor: postgreact: ~file modes, control file 2025-04-17 03:46:53 +03:00
yukkop e607bf09fa feat: postgreact: hello world 2025-04-15 20:05:53 +00:00
yukkop 1612799fb7 Merge branch 'master' of github.com:hectic-lab/util.nix 2025-04-15 17:04:12 +00:00
yukkop 5055983881 test: hectic C!: templater sectin 2025-04-15 17:03:15 +00:00
yukkop 1da02a7ad6 test: hectic C: templater interpolation 2025-04-15 16:33:49 +00:00
yukkop 6b8e825a4b feat: hectic C: prettify debug strings 2025-04-15 15:53:39 +00:00
yukkop ba6ec7a113 feat: +plhaskell package 2025-04-15 05:51:37 +00:00
yukkop 8697b00cfc feat: hectic C: some sigfault 2025-04-15 04:54:31 +00:00
yukkop d45307336a feat: hectic C: debug to json parser 2025-04-14 19:43:57 +00:00
yukkop 1dcb165abf fix: hectic C: union debug string constructor 2025-04-14 17:27:32 +00:00
yukkop abf1e706b7 feat: hectic C: union in debug string constructor 2025-04-14 16:28:16 +00:00
yukkop 807ea83892 fix: hectic C: impruve cycle detection for debug strig constructor 2025-04-13 23:13:28 +00:00
yukkop 267d0ec464 refactor: hectic C: logger rules and results 2025-04-13 19:38:11 +00:00
yukkop 2f6806185d feat: hectic C: template node debug, but union issue in debug string constructor 2025-04-12 18:25:25 +00:00
yukkop 8981af315c test: hectic C: fix logger test 2025-04-12 15:38:06 +00:00
yukkop 1f25c4fcad feat: hectic C: generic result 2025-04-12 15:21:33 +00:00
yukkop db5b88388e test: hectic C: debug constructor 2025-04-12 01:38:40 +00:00
yukkop 2cb814f054 feat: hectic C: debug constructor impruve 2025-04-12 01:07:20 +00:00
yukkop d7bdc0722f feat: hectic C: debug constructor wroks!!! 2025-04-11 22:34:43 +00:00
yukkop 80eac39b90 feat: hectic C: debug string constructor init 2025-04-11 10:33:20 +00:00
yukkop de08900297 fix: hectic C: template parser config 2025-04-10 14:26:21 +00:00
yukkop b3d040d6fd feat: hectic C: template parser, template to json 2025-04-09 19:52:43 +00:00
yukkop 7c82a5a866 docs: hectic C: macro for reduse boilerplane with code position 2025-04-07 22:55:37 +00:00
yukkop cda65a56eb docs: hectic C: types for template parser 2025-04-07 22:44:05 +00:00
yukkop d04a78de23 feat: hectic C: logging settings 2025-04-07 16:54:26 +00:00
yukkop 98a26f1f61 fix: hectic C: impruve logging guidelines 2025-04-07 05:26:30 +00:00
yukkop 11150151f9 fix: hectic C: impruve logging 2025-04-06 19:18:14 +00:00
yukkop bfb69b06ea fix: hmpl: section blocks 2025-04-06 15:51:21 +00:00
yukkop f151438969 feat: hectic C: debug print 2025-04-06 15:17:26 +00:00
yukkop 14c246de3e feat: hmpl: section test passeeed 2025-04-05 21:31:35 +00:00
yukkop 42f8e839d1 refactor: change packages names 2025-04-05 19:09:13 +00:00
yukkop 686344e356 feat: some python packages 2025-04-05 17:04:29 +00:00
yukkop 34c798b222 fix: c: log colorized with terminal mode 2025-04-04 14:13:54 +00:00
yukkop d5ef2bf551 feat: c: log colorized 2025-04-04 14:07:17 +00:00
yukkop 7258adbcc2 fix: hectic C: func definitions 2025-04-04 02:26:55 +00:00
yukkop c5e9d9c8cb feat: watch: try pager mode.. 2025-04-04 02:09:43 +00:00
yukkop d800266fe0 feat: some nix shit 2025-04-04 01:59:57 +00:00
yukkop 5ab0833d78 feat: watch: paterns 2025-04-03 22:57:55 +00:00
yukkop d680e080ad feat: watch: init 2025-04-03 22:34:50 +00:00
yukkop 35db5e4bf6 test(hectic C): slice 2025-03-27 21:07:00 +00:00
yukkop 3bb2b23be5 refactor(hmpl): fix warnings 2025-03-24 19:27:11 +00:00
yukkop 4280f0df95 refactor(hectic C): fix warnings 2025-03-24 14:21:00 +00:00
yukkop d39ed7f9fe refactor(hmpl,hectic): warnings 2025-03-24 13:36:31 +00:00
yukkop 972e2d2968 refactor(hmpl): eval 2025-03-22 19:45:17 +00:00
yukkop f64636fe9e test(hmpl): render interpolation tags with prefix 2025-03-22 15:08:48 +00:00
yukkop 546a35f1d2 test(hmpl): render interpolation tags 2025-03-22 14:56:03 +00:00
yukkop aeea41b0e1 fix: json eval 2025-03-22 04:15:58 +00:00
yukkop 27e42e9010 fix: segfalt in json_get_object_item 2025-03-22 03:18:34 +00:00
yukkop bcaca5038d feat(hectic C): json api 2025-03-22 01:22:36 +00:00
yukkop aff923d470 refactor(hmpl): rewrite via arenas 2025-03-21 15:29:00 +00:00
yukkop 06e5fdf1ab fix(hmpl): memory leak 2025-03-21 13:33:01 +00:00
yukkop c4d14e5aa2 feat(hmpl): render simple placeholder with memory leak of course 2025-03-21 04:00:23 +00:00
yukkop 9438280750 fix(libhectic): arena allocators 2025-03-21 02:54:47 +00:00
yukkop 4f2391e9b5 feat(libhectic): arenas 2025-03-21 01:53:02 +00:00
yukkop 647155f859 test: fix: test raise generic 2025-03-20 18:47:44 +00:00
yukkop c91b52b448 Merge branch 'master' of github.com:hectic-lab/util.nix 2025-03-20 18:16:04 +00:00
yukkop 18b36e43b7 test: libhectic 2025-03-20 18:15:52 +00:00
zerosummed c22a769e50 feat!: makeEnvironment: +strip prefix, ~rename to readEnvironment 2025-03-20 14:00:10 +03:00
yukkop a7e754f48d feat: min C writer 2025-03-19 01:44:19 +00:00
yukkop 1f1f3c7211 Merge branch 'feat/pg-neo-migration' of github.com:hectic-lab/util.nix into feat/pg-neo-migration 2025-03-19 01:43:46 +00:00
yukkop 3f9d8902d5 feat: C writer 2025-03-19 01:09:47 +00:00
yukkop 58fc204f46 feat(c): extrude lib c 2025-03-12 23:19:45 +01:00
yukkop cbba9016cd refactor: stir some shit around again 2025-03-07 00:40:00 +00:00
yukkop 0bd5bb65f6 refector(pg-neo-migration): stir some shit around trust me it's fine 2025-03-06 16:07:53 +01:00
yukkop cc7bd6b1a2 feat(pg-neo-migration): init 2025-03-05 19:13:00 +00:00
yukkop e8e2890171 feat(pg-schema): init 2025-03-02 10:48:52 +00:00
yukkop 4d1c210986 fix(pg-migration): db_url fetch 2025-02-16 17:21:02 +00:00
yukkop 3e790cd18a fix(pg-migration): postgres build inputs 2025-02-16 17:03:59 +00:00
yukkop d02f559635 feat(pg-migration): variable providing for migrations 2025-02-16 15:58:31 +00:00
yukkop f3bbbeb12d feat: inheritance for migration table 2025-02-16 14:21:20 +00:00
yukkop c3cdf9858a feat: postgres extension pg_smtp_client 2025-02-16 10:32:37 +00:00
zerosummed 7cc4c6292b fix: nix check errors 2025-02-14 21:27:48 +03:00
zerosummed e1c484b1c0 style: format (alejandra) 2025-02-14 21:06:51 +03:00
473 changed files with 46649 additions and 1372 deletions
+1
View File
@@ -0,0 +1 @@
use flake .#postgres-c
+2
View File
@@ -1,4 +1,6 @@
.env
result
result-*
rust-toolchain.toml
target/
docs/plans
+78
View File
@@ -0,0 +1,78 @@
keys:
- &snuff age1w4hw2ntxrtfqhht63s9lf7nhjxjmdcc927hndn5ygcqqj532qssq4m2m6p
- &yukkop age1r25zdeqq8nac6dgca9en28r57ffyz9u9d8z5yc25gc8xqz747vaqmdtk0h
- &yukkop-alt age1vv46vn4hsn2lg6jy834cpu40c3mvqklldcm3hjtynrhwtpmlpc8szruz4v
- &nrv age1x04u7ftjgx8de2gq596e7frauze764cmn7jjwqnx8szthvfft5qq0tezx6
- &bfs-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
- &bfs-pol-server age1fpytf05sg9n6ywpwkmn09lhpfvgtud9h75h76jhxha475zpnasqq952rpu
- &bfs-new-server age17yx98qk9gzgcf2q6zhhp05p6mmtrkgz66dvyk9gqclypvlr8rersxjy5v7
- &neuro-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
- &games-server age15yzgmsvl3ku2w863h6gw2vpmw37m9aruv6xrj4fue6n2jpm7pyuqk9xjmj
- &hectic-lab-server age13h8twnwvgxn04l5ywtru89a6psw5d0uckr2eghxsjp88a5augvsstq5ard
- &umbriel-bfs age1jxntjca8q2vxvf2jaal4xyvm2ae6sh62fhv897694kuzawfrk5asj00zdt
creation_rules:
- path_regex: sus/home.xray.yaml$
key_groups:
- age:
- *yukkop
- path_regex: sus/bfs.xray.yaml$
key_groups:
- age:
- *snuff
- *yukkop
- *bfs-server
- *bfs-pol-server
- *bfs-new-server
- path_regex: sus/neuro.yaml$
key_groups:
- age:
- *yukkop
- *neuro-server
- path_regex: sus/games.yaml$
key_groups:
- age:
- *yukkop
- *games-server
- path_regex: sus/hectic-lab.yaml$
key_groups:
- age:
- *nrv
- *yukkop
- *yukkop-alt
- *hectic-lab-server
- *umbriel-bfs
- path_regex: sus/matrix-cluster.yaml$
key_groups:
- age:
- *nrv
- *yukkop
- *snuff
- *yukkop-alt
- *hectic-lab-server
- *bfs-pol-server
- *umbriel-bfs
- path_regex: sus/sentinella-default.yaml$
key_groups:
- age:
- *yukkop
- *yukkop-alt
- *nrv
- *bfs-server
- *bfs-pol-server
- *bfs-new-server
- *neuro-server
- *games-server
- *hectic-lab-server
- *umbriel-bfs
- path_regex: docs/.*\.md$
key_groups:
- age:
- *yukkop
+17
View File
@@ -0,0 +1,17 @@
{
system,
pkgs,
self
}: pkgs.mkShell {
buildInputs = (with pkgs; [
inotify-tools
gdb
gcc
]) ++ (with self.packages.${system}; [
c-hectic
nvim-pager
watch
]);
PAGER = "${self.packages.${system}.nvim-pager}/bin/pager";
}
+30
View File
@@ -0,0 +1,30 @@
{ system, pkgs, self, ... }:
(import ./dev { inherit self system pkgs; })
// {
c = import ./c.nix { inherit self system pkgs; };
postgres-c = import ./postgres-c.nix { inherit self system pkgs; };
pure-c = import ./pure-c.nix { inherit self system pkgs; };
rust = import ./rust.nix { inherit self system pkgs; };
haskell = import ./haskell.nix { inherit self system pkgs; };
neuro = import ./neuro.nix { inherit self system pkgs; };
xmpp = import ./xmpp.nix { inherit self system pkgs; };
default = pkgs.mkShell {
buildInputs =
(with self.packages.${system}; [
nvim-alias
nvim-pager
])
++ (with pkgs; [
git
jq
yq-go
curl
#(writeScriptBin "hemar-check" ''
# ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null vm-postgres 'zsh -c check'
#'')
]);
# environment
PAGER = "${self.packages.${system}.nvim-pager}/bin/pager";
};
}
+7
View File
@@ -0,0 +1,7 @@
{
system,
self,
pkgs
}: {
hemar = import ./hemar { inherit self system pkgs; };
}
+8
View File
@@ -0,0 +1,8 @@
{ pkgs, ... }: pkgs.mkShell {
buildInputs = (with pkgs; [
dash
(pkgs.writeShellScriptBin "letest" ''
${pkgs.dash}/bin/dash ${./test.sh} "$@"
'')
]);
}
+7
View File
@@ -0,0 +1,7 @@
#!/bin/dash
ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
if [ "${1:?}" = 'test' ]; then
dash "${ROOT_DIR}/package/hemar/test.sh"
fi
+8
View File
@@ -0,0 +1,8 @@
{
system,
pkgs,
self
}: self.devShells.${system}.default
// (pkgs.mkShell {
buildInputs = [pkgs.stack];
})
+7
View File
@@ -0,0 +1,7 @@
{
system,
pkgs,
self,
}: pkgs.mkShell {
buildInputs = [];
}
+22
View File
@@ -0,0 +1,22 @@
{
system,
pkgs,
self
}: pkgs.mkShell {
buildInputs = (with pkgs; [
inotify-tools
postgresql_15
]) ++ (with self.packages.${system}; [
nvim-pager
]) ++ (with pkgs; [
gdb
gcc
]);
PAGER = "${self.packages.${system}.nvim-pager}/bin/pager";
shellHook = ''
export PATH=${pkgs.gcc}/bin:$PATH
export PAGER="${self.packages.${system}.nvim-pager}/bin/pager"
'';
}
+14
View File
@@ -0,0 +1,14 @@
{
system,
pkgs,
self
}: pkgs.mkShell {
buildInputs = (with pkgs; [ inotify-tools ]) ++ (with self.packages.${system}; [ nvim-pager ]) ++ (with pkgs; [ gdb gcc binutils ]);
PAGER = "${self.packages.${system}.nvim-pager}/bin/pager";
shellHook = ''
export PATH=${pkgs.gcc}/bin:$PATH
export PAGER="${self.packages.${system}.nvim-pager}/bin/pager"
'';
}
+17
View File
@@ -0,0 +1,17 @@
{
self,
pkgs,
system
}: let
rustToolchain =
if builtins.pathExists ./rust-toolchain.toml
then pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml
else pkgs.pkgsBuildHost.rust-bin.stable."1.81.0".default;
in
self.devShells.${system}.default
// (pkgs.mkShell {
nativeBuildInputs = [
rustToolchain
pkgs.pkg-config
];
})
+40
View File
@@ -0,0 +1,40 @@
{
system,
pkgs,
self,
}: let
proxychainsConf = pkgs.writeText "proxychains.conf" ''
strict_chain
proxy_dns
tcp_read_time_out 15000
tcp_connect_time_out 8000
[ProxyList]
socks5 127.0.0.1 1080
'';
# Wrapper script for profanity with proxy
profanity-proxy = pkgs.writeShellScriptBin "profanity-proxy" ''
exec ${pkgs.proxychains-ng}/bin/proxychains4 -f ${proxychainsConf} ${pkgs.profanity}/bin/profanity "$@"
'';
in pkgs.mkShell {
buildInputs = [
pkgs.profanity
pkgs.proxychains-ng
profanity-proxy
];
shellHook = ''
echo ""
echo "=== XMPP DevShell ==="
echo ""
echo "1. Start SSH SOCKS proxy (in another terminal):"
echo " ssh -D 1080 -N neuro"
echo ""
echo "2. Run profanity with proxy:"
echo " profanity-proxy"
echo ""
echo "3. In profanity:"
echo " /connect yukkop@accord.tube"
echo ""
'';
}
File diff suppressed because one or more lines are too long
+101
View File
@@ -0,0 +1,101 @@
# Spec: sentinella-p2p-design
Scope: feature
# sentinèlla P2P Design Spec
## Goal
Replace the hub-and-spoke sentinel topology with a fully peer-to-peer model where every node is equal.
## Topology
- Every node runs both `probe` and `watcher`
- No privileged coordinator; any node can go down without breaking monitoring of the others
- Duplicate Telegram alerts from multiple nodes detecting the same failure are **accepted** (reliability over deduplication)
## Peer Discovery — DNS multi-A record
- One DNS name (e.g. `peers.sentinella.com`) has multiple A records, one per node IP
- Configured externally via any DNS registrar (Cloudflare, Namecheap, etc.)
- Recommended TTL: **60 seconds** so new nodes propagate quickly
- Each watcher resolves the name via `getent hosts $PEERS_DNS` on every poll cycle
- Own IP (`$SELF`) is stripped from the result so a node never polls itself
- No per-node DNS names needed; IP addresses are used directly in peer URLs
```
peers.sentinella.com A 1.2.3.4 TTL 60
peers.sentinella.com A 5.6.7.8 TTL 60
peers.sentinella.com A 9.10.11.12 TTL 60
```
## Environment Variables
### watcher (new, replaces sentinel)
| Variable | Default | Required | Description |
|---|---|---|---|
| `PEERS_DNS` | — | yes | DNS name resolving to all peer IPs |
| `SELF` | — | yes | This node's own IP; excluded from peer list |
| `PEERS_PORT` | `5988` | no | Port all peers listen on |
| `PEERS_SCHEME` | `http` | no | URL scheme for peer connections |
| `PEERS_TOKEN` | — | no | Single Basic Auth token sent to all peers (replaces per-server TOKENS) |
| `TG_TOKEN` | — | yes | Telegram bot token |
| `TG_CHAT_ID` | — | yes | Telegram chat ID |
| `TIMEOUT` | `5` | no | curl timeout seconds |
| `POLLING_INTERVAL_SEC` | `3` | no | Seconds between poll rounds |
| `STATE_DIR` | `/var/lib/sentinel` | no | Directory for state files |
| `SPAM` | `0` | no | If 1, notify on every poll |
### probe / router (unchanged)
| Variable | Default | Description |
|---|---|---|
| `PORT` | `5988` | TCP port to listen on |
| `URLS` | — | Space-separated URLs to health-check |
| `VOLUMES` | all from df -P | Mount points to report |
| `TIMEOUT` | `5` | curl timeout |
| `AUTH_FILE` | — | Path to user:pass auth file |
## Key Implementation Details
### resolve_peers() in watcher.sh
```sh
resolve_peers() {
getent hosts "$PEERS_DNS" \
| awk '{print $1}' \
| grep -v "^${SELF}$" \
| awk -v s="$PEERS_SCHEME" -v p="$PEERS_PORT" '{print s"://"$1":"p}'
}
```
Called at the top of every outer poll loop iteration — no restart needed when DNS changes.
### Auth simplification
- Old: per-server CSV `TOKENS` aligned with `SERVERS`
- New: single optional `PEERS_TOKEN`; either all peers require auth or none do
### State files
- Unchanged: `$STATE_DIR/$(cksum url).state` contains last known state string
- Format: `up:N/M:200` or `down:0/0:000`
## Binaries
| Old name | New name | Role |
|---|---|---|
| `sentinel` | `watcher` | Polls peers, sends alerts |
| `probe` | `probe` | socat TCP listener (unchanged) |
| `router` | `router` | HTTP handler (unchanged + auth bug fixed) |
| `base64` | `base64` | awk base64 util (unchanged) |
## NixOS Module Options
```
hectic.sentinella.enable bool
hectic.sentinella.peersDns string # e.g. "peers.sentinella.com"
hectic.sentinella.self string # this node's own IP
hectic.sentinella.port int # default 5988
hectic.sentinella.urls [string] # URLs for probe to health-check
hectic.sentinella.volumes [string] # mount points for probe
hectic.sentinella.tgToken string
hectic.sentinella.tgChatId string
hectic.sentinella.pollingIntervalSec int # default 3
```
Generates two systemd services: `sentinella-probe` and `sentinella-watcher`.
## Known Bug to Fix (router.sh)
The Basic Auth check references `$USER` and `$PASS` which are never populated.
Fix: move `auth_ok=false` before the header loop and compare `$tok` against
each entry in `$AUTH_TOKENS` (which is correctly populated from `AUTH_FILE`).
Generated
+1156 -8
View File
File diff suppressed because it is too large Load Diff
+100 -240
View File
@@ -1,256 +1,116 @@
{
description = "yukkop's nix utilities";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs-fixed.url = "github:NixOS/nixpkgs/nixos-25.11";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs = {
nixpkgs.follows = "nixpkgs";
};
};
deploy-rs = {
url = "github:serokell/deploy-rs";
inputs.nixpkgs.follows = "nixpkgs";
};
hyprland = {
url = "github:hyprwm/Hyprland";
inputs.nixpkgs.follows = "nixpkgs";
};
nixvim = {
url = "github:nix-community/nixvim/nixos-25.11";
inputs.nixpkgs.follows = "nixpkgs-fixed";
};
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
impermanence = {
url = "github:nix-community/impermanence";
};
home-manager = {
url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs-fixed";
};
nixos-wsl = {
url = "github:nix-community/NixOS-WSL";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-hardware = {
url = "github:NixOS/nixos-hardware";
};
nixos-anywhere = {
url = "github:nix-community/nixos-anywhere";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-mailserver = {
url = "gitlab:simple-nixos-mailserver/nixos-mailserver/snm-25.11";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-minecraft.url = "github:Infinidoge/nix-minecraft";
hectic-landing = {
# NOTE(yukkop): private repo - SSH access required.
# Only evaluated when nixosConfigurations."hectic-lab|x86_64-linux" is built.
url = "git+ssh://git@github.com/liquizz/hectic-landing.git";
inputs.nixpkgs.follows = "nixpkgs-fixed";
};
mechabellum-replay-analysis = {
# NOTE(yukkop): private repo - SSH access required.
# Only evaluated when nixosConfigurations."hectic-lab|x86_64-linux" is built.
url = "git+ssh://git@github.com/LysmiMx/mechabellum-replay-analysis.git";
inputs.nixpkgs.follows = "nixpkgs-fixed";
};
};
outputs = { self, nixpkgs, rust-overlay }:
let
lib = nixpkgs.lib;
recursiveUpdate = lib.recursiveUpdate;
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" ];
forSpecSystemsWithPkgs = supportedSystems: pkgOverlays: f:
builtins.foldl' (acc: system:
let
pkgs = import nixpkgs {
inherit system;
overlays = pkgOverlays;
};
systemOutputs = f { system = system; pkgs = pkgs; };
in
recursiveUpdate acc systemOutputs
) {} supportedSystems;
forAllSystemsWithPkgs = pkgOverlays: f: forSpecSystemsWithPkgs supportedSystems pkgOverlays f;
envErrorMessage = varName: "Error: The ${varName} environment variable is not set.";
parseEnv = import ./parse-env.nix;
dotEnv = builtins.getEnv "DOTENV";
minorEnvironment =
if dotEnv != "" then
if builtins.pathExists dotEnv then
parseEnv dotEnv
else
throw "${dotEnv} file not exist"
else
if builtins.pathExists ./.env then
parseEnv ./.env
else
{};
in
forAllSystemsWithPkgs [ (import rust-overlay) ] ({ system, pkgs }:
{
packages.${system} =
let
rust = {
nativeBuildInputs = [
pkgs.pkgsBuildHost.rust-bin.stable."1.81.0".default
pkgs.pkg-config
];
commonArgs = {
inherit (self.lib) cargoToml;
inherit (rust) nativeBuildInputs;
};
};
in
{
nvim-alias = pkgs.callPackage ./package/nvim-alias.nix {};
bolt-unpack = pkgs.callPackage ./package/bolt-unpack.nix {};
nvim-pager = pkgs.callPackage ./package/nvim-pager.nix {};
printobstacle = pkgs.callPackage ./package/printobstacle.nix {};
printprogress = pkgs.callPackage ./package/printprogress.nix {};
colorize = pkgs.callPackage ./package/colorize.nix {};
github.gh-tl = pkgs.callPackage ./package/github/gh-tl.nix {};
supabase-with-env-collection = pkgs.callPackage ./package/supabase-with-env-collection.nix {};
migration-name = pkgs.callPackage ./package/migration-name.nix {};
prettify-log = pkgs.callPackage ./package/prettify-log/default.nix rust.commonArgs;
pg = {
pg-from = pkgs.callPackage ./package/postgres/pg-from/default.nix rust.commonArgs;
pg-migration = pkgs.callPackage ./package/postgres/pg-migration/default.nix rust.commonArgs;
};
};
devShells.${system} =
let
shells = self.devShells.${system};
in
{
default = pkgs.mkShell {
buildInputs = (with self.packages.${system}; [
nvim-alias
#prettify-log
nvim-pager
]) ++ (with pkgs; [
git
jq
yq-go
curl
]);
# environment
PAGER="${self.packages.${system}.nvim-pager}/bin/pager";
};
rust =
outputs = {
self,
nixpkgs,
rust-overlay,
...
}@inputs: let
flake = ./.;
self-lib = import ./lib { inherit flake self inputs; };
# Create overlay that includes legacy packages
overlayWithLegacy = system: final: prev:
let
rustToolchain = if builtins.pathExists ./rust-toolchain.toml then
pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml
else
pkgs.pkgsBuildHost.rust-bin.stable."1.81.0".default;
in
shells.default //
(pkgs.mkShell {
nativeBuildInputs = [
rustToolchain
pkgs.pkg-config
];
});
haskell = shells.default // (pkgs.mkShell {
buildInputs = [ pkgs.stack ];
});
};
nixosModules.${system} = {
"preset.default" = { pkgs, modulesPath, ... }: {
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
];
services.getty.autologinUser = "root";
programs.zsh.enable = true;
users.defaultUserShell = pkgs.zsh;
# Enable flakes and new 'nix' command
nix.settings.experimental-features = "nix-command flakes";
virtualisation.vmVariant.virtualisation = {
qemu.options = [
"-nographic"
"-display curses"
"-append console=ttyS0"
"-serial mon:stdio"
"-vga qxl"
];
forwardPorts = [
{ from = "host"; host.port = 40500; guest.port = 22; }
];
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
};
};
networking.firewall = {
enable = true;
allowedTCPPorts = [ ];
};
environment = {
defaultPackages = [];
systemPackages = (with pkgs; [
curl
neovim
yq-go
jq
htop-vim
]) ++ (with self.packages.${system}; [
prettify-log
nvim-pager
]);
variables = {
PAGER=with self.packages.${system}; "${nvim-pager}/bin/pager";
};
};
system.stateVersion = "24.11";
};
"hardware.hetzner" = { ... }: {
boot.loader.grub.device = "/dev/sda";
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
"xen_blkfront"
"vmw_pvscsi"
];
boot.initrd.kernelModules = [ "nvme" ];
fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; };
};
};
}) // {
overlays.default =
final: prev: (
let
version = "1.6.1";
buildHttpExt = versionSuffix: let
buildPostgresqlExtension =
prev.callPackage (import (builtins.path {
name = "extension-builder";
path = ./buildPostgresqlExtension.nix;
})) {
postgresql = prev."postgresql_${versionSuffix}";
};
in buildPostgresqlExtension {
pname = "http";
inherit version;
src = prev.fetchFromGitHub {
owner = "pramsey";
repo = "pgsql-http";
rev = "v${version}";
hash = "sha256-C8eqi0q1dnshUAZjIsZFwa5FTYc7vmATF3vv2CReWPM=";
};
nativeBuildInputs = with prev; [ pkg-config curl ];
baseOverlay = (import ./overlay { inherit flake self inputs nixpkgs; }) final prev;
legacyPackages = import ./legacy { inherit system pkgs self; };
pkgs = import nixpkgs {
inherit system;
overlays = [ (import rust-overlay) ];
};
in
{
hectic = self.packages.${prev.system};
postgresql_17 = prev.postgresql_17 // { pkgs = prev.postgresql_17.pkgs // { http = buildHttpExt "17"; }; };
postgresql_16 = prev.postgresql_16 // { pkgs = prev.postgresql_16.pkgs // { http = buildHttpExt "16"; }; };
postgresql_15 = prev.postgresql_15 // { pkgs = prev.postgresql_15.pkgs // { http = buildHttpExt "15"; }; };
postgresql_14 = prev.postgresql_14 // { pkgs = prev.postgresql_14.pkgs // { http = buildHttpExt "14"; }; };
});
lib = {
# -- For all systems --
inherit dotEnv minorEnvironment parseEnv forAllSystemsWithPkgs forSpecSystemsWithPkgs;
makeEnvironment = envVars:
builtins.listToAttrs
(map (name: { inherit name; value = self.lib.getEnv name; }) envVars);
# -- Env processing --
getEnv = varName: let
var = builtins.getEnv varName;
in
if var != "" then
var
else if minorEnvironment ? varName then
minorEnvironment."${varName}"
else
throw (envErrorMessage varName);
# -- Cargo.toml --
cargoToml = src: (builtins.fromTOML (builtins.readFile "${src}/Cargo.toml"));
ssh.keys = {
hetzner-test = {
yukkop = ''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8scy1tv6zfXX6xyaukhO/fsZwif5rC89DvXNc6XxOf'';
};
};
in
baseOverlay // legacyPackages;
overlays = [ self.overlays.default ];
in self-lib.forAllSystemsWithPkgs ([(import rust-overlay)] ++ overlays) ({
system,
pkgs,
}: {
packages.${system} = import ./package { inherit flake self inputs pkgs system; };
devShells.${system} = import ./devshell { inherit flake self inputs pkgs system; };
legacyPackages.${system} = import ./legacy { inherit flake self inputs pkgs system; };
checks.${system} = import ./test { inherit flake self inputs pkgs system; };
}) // {
lib = self-lib;
overlays.default = import ./overlay { inherit flake self inputs; };
nixosModules = import ./nixos/module { inherit flake self inputs; };
templates = import ./template { inherit flake self inputs; };
nixosConfigurations = {
# NOTE(yukkop): in bfs one of dependencies is shadow-4.17.4 that
# unsupported on aarch64-darwin
"bfs.netherland.xray|x86_64-linux" = import ./nixos/system/bfs.netherland.xray { inherit flake self inputs; system = "x86_64-linux"; };
"bfs.poland.xray|x86_64-linux" = import ./nixos/system/bfs.poland.xray { inherit flake self inputs; system = "x86_64-linux"; };
# FIXME(yukkop): some why I cannot merge nixosConfigurations from `forAllSystemsWithPkgs` with this
"neuro|x86_64-linux" = import ./nixos/system/neuro { inherit flake self inputs; system = "x86_64-linux"; };
"games|x86_64-linux" = import ./nixos/system/games { inherit flake self inputs; system = "x86_64-linux"; };
"wsl|x86_64-linux" = import ./nixos/system/wsl { inherit flake self inputs; system = "x86_64-linux"; };
"hectic-lab|x86_64-linux" = import ./nixos/system/hectic-lab { inherit flake self inputs; system = "x86_64-linux"; };
};
};
}
+8
View File
@@ -0,0 +1,8 @@
{ pkgs, ... }:
let
writers = pkgs.callPackage ./writer { };
in {
helpers = pkgs.callPackage ./helper { };
# NOTE(yukkop): duplicate writers in root of legacyPackages and writers due nixpkgs legacyPackages consistency
writers = writers;
} // writers
+4
View File
@@ -0,0 +1,4 @@
{ callPackage }: {
posix-shell = callPackage ./posix-shell {};
steam = callPackage ./steam {};
}
@@ -0,0 +1,41 @@
: "${OLD_NAMESPACE:=}"
nl=$(printf '\nx')
nl=${nl%x}
___pop_namespace() {
v=${OLD_NAMESPACE%%"$nl"*}
case $OLD_NAMESPACE in
*"$nl"*)
OLD_NAMESPACE=${OLD_NAMESPACE#*"$nl"}
;;
*)
OLD_NAMESPACE=
;;
esac
printf '%s\n' "$v"
}
___peek_namespace() {
printf '%s\n' "${OLD_NAMESPACE%%"$nl"*}"
}
___push_namespace() {
if [ -n "$OLD_NAMESPACE" ]; then
OLD_NAMESPACE=$1"$nl$OLD_NAMESPACE"
else
OLD_NAMESPACE=$1
fi
}
change_namespace() {
___push_namespace "$HECTIC_NAMESPACE"
export HECTIC_NAMESPACE="$1"
}
restore_namespace() {
HECTIC_NAMESPACE=$(___pop_namespace)
export HECTIC_NAMESPACE
}
+54
View File
@@ -0,0 +1,54 @@
NC='\033[0m'
# Regular text colors
BLACK='\033[30m'
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
MAGENTA='\033[35m'
CYAN='\033[36m'
WHITE='\033[37m'
# Bright text colors
BBLACK='\033[90m'
BRED='\033[91m'
BGREEN='\033[92m'
BYELLOW='\033[93m'
BBLUE='\033[94m'
BMAGENTA='\033[95m'
BCYAN='\033[96m'
BWHITE='\033[97m'
# Background colors
BG_BLACK='\033[40m'
BG_RED='\033[41m'
BG_GREEN='\033[42m'
BG_YELLOW='\033[43m'
BG_BLUE='\033[44m'
BG_MAGENTA='\033[45m'
BG_CYAN='\033[46m'
BG_WHITE='\033[47m'
# Bright background colors
BG_BBLACK='\033[100m'
BG_BRED='\033[101m'
BG_BGREEN='\033[102m'
BG_BYELLOW='\033[103m'
BG_BBLUE='\033[104m'
BG_BMAGENTA='\033[105m'
BG_BCYAN='\033[106m'
BG_BWHITE='\033[107m'
# Text effects
RESET='\033[0m'
BOLD='\033[1m'
DIM='\033[2m'
ITALIC='\033[3m'
UNDERLINE='\033[4m'
BLINK='\033[5m'
INVERSE='\033[7m'
HIDDEN='\033[8m'
STRIKE='\033[9m'
: "$NC" "$BLACK" "$RED" "$GREEN" "$YELLOW" "$BLUE" "$MAGENTA" "$CYAN" "$WHITE" "$BBLACK" "$BRED" "$BGREEN" "$BYELLOW" "$BBLUE" "$BMAGENTA" "$BCYAN" "$BWHITE" "$BG_BLACK" "$BG_RED" "$BG_GREEN" "$BG_YELLOW" "$BG_BLUE" "$BG_MAGENTA" "$BG_CYAN" "$BG_WHITE" "$BG_BBLACK" "$BG_BRED" "$BG_BGREEN" "$BG_BYELLOW" "$BG_BBLUE" "$BG_BMAGENTA" "$BG_BCYAN" "$BG_BWHITE" "$RESET" "$BOLD" "$DIM" "$ITALIC" "$UNDERLINE" "$BLINK" "$INVERSE" "$HIDDEN" "$STRIKE"
+27
View File
@@ -0,0 +1,27 @@
{ dash, hectic }: let
shell = "${dash}/bin/dash";
bashOptions = [
"errexit"
"nounset"
];
in {
log = hectic.writeDash "log.sh" ''
${builtins.readFile ./colors.sh}
${builtins.readFile ./log.sh}
'';
colors = hectic.writeDash "colors.sh" ''
${builtins.readFile ./colors.sh}
'';
change_namespace = hectic.writeDash "change_namespace.sh" ''
${builtins.readFile ./change_namespace.sh}
'';
quote = hectic.writeDash "quote.sh" ''
${builtins.readFile ./quote.sh}
'';
pager_or_cat = hectic.writeDash "pager_or_cat.sh" ''
${builtins.readFile ./pager_or_cat.sh}
'';
with_closed_fds = hectic.writeDash "with_closed_fds.sh" ''
${builtins.readFile ./with_closed_fds.sh}
'';
}
+155
View File
@@ -0,0 +1,155 @@
#!/bin/dash
# Hectic shell logger
#
# Usage:
# # Including
# . <this file>
#
# # Required
# colors.sh
#
# # In your script (recommended: do NOT export HECTIC_NAMESPACE)
# HECTIC_NAMESPACE="my-script" # optional, defaults to basename "$0"
# # # Then use:
# log info 'starting up'
# log debug "value=${val}"
# log error "failed: ${WHITE}${reason}${NC} red text again"
#
# # Note:
# When you use NC to reset terminal colors inside log output,
# it resets back to the log levels color instead of the terminal default.
: "${HECTIC_NAMESPACE="$(basename "$0")"}"
: "${HECTIC_LOG:=trace}" # e.g. "info;ns1=debug;ns2=trace"
validate_log_level_spec() {
spec=$HECTIC_LOG
levels="trace debug info notice warn error"
ok_level() {
for l in $levels; do
[ "$l" = "$1" ] && return 0
done
return 1
}
oldIFS=$IFS
IFS=';'
# shellcheck disable=SC2086
set -- $spec
IFS=$oldIFS
for tok; do
case $tok in
*=*)
ns=${tok%%=*}
lvl=${tok#*=}
[ -n "$ns" ] || return 1
ok_level "$lvl" || return 1
;;
*)
ok_level "$tok" || return 1
;;
esac
done
return 0
}
exec 3>&2
trap 'exec 3>&-' EXIT INT HUP
validate_log_level_spec || { printf "%b%b\n" "${BBLACK}${HECTIC_NAMESPACE}> " "${color}invalid HECTIC_LOG syntax${NC}" "$@" >&3; exit 1; }
log_level_num() {
case $1 in
trace) printf %s 0 ;;
debug) printf %s 1 ;;
info) printf %s 2 ;;
notice) printf %s 3 ;;
warn) printf %s 4 ;;
error|panic) printf %s 5 ;;
*) printf %s 2 ;; # default info
esac
}
log_effective_level() {
spec=$HECTIC_LOG
ns=$HECTIC_NAMESPACE
default_level=
ns_level=
oldIFS=$IFS
IFS=';'
# shellcheck disable=SC2086
set -- $spec
IFS=$oldIFS
for tok; do
case $tok in
*=*)
name=${tok%%=*}
lvl=${tok#*=}
[ "$name" = "$ns" ] && ns_level=$lvl
;;
*)
[ -z "$default_level" ] && default_level=$tok
;;
esac
done
printf '%s\n' "${ns_level:-${default_level:-info}}"
}
log_allowed() {
msg_level="${1:?}"
eff_level="$(log_effective_level)"
msg_n="$(log_level_num "$msg_level")"
eff_n="$(log_level_num "$eff_level")"
[ "$msg_n" -ge "$eff_n" ]
}
# log(level, text...)
log() {
delimetr=${DELIMETR:-' '};
level="${1:?}"
log_allowed "$level" || return 0
case "$level" in
trace) color="$MAGENTA" ;;
debug) color="$BLUE" ;;
info) color="$GREEN" ;;
notice) color="$CYAN" ;;
warn) color="$YELLOW" ;;
error) color="$RED" ;;
panic) color="$BRED" ;;
*)
color="$WHITE"
NO_SHIFT=1
;;
esac
[ ${NO_SHIFT+x} ] || shift
# shellcheck disable=SC2059
# shellcheck disable=SC2046
[ "$level" = panic ] && printf "${BBLACK}${HECTIC_NAMESPACE}> $BRED%b$NC\n" \
'' \
'' \
'this panic is unexpected behavior of program and/or bug' \
'please contact the developer' \
'' \
''
# shellcheck disable=SC1003
fmt="$(printf "%s$delimetr" "$@" | sed 's/\\033\[0m/''\'"$color"'/g')"
shift
# shellcheck disable=SC1003
printf "${BBLACK}${HECTIC_NAMESPACE}> %b\n" "$color$fmt$NC" >&3
}
@@ -0,0 +1,8 @@
pager_or_cat_init() {
# Pipe to pager only if stdout is a terminal, otherwise output directly
if [ -t 1 ]; then
PAGER_OR_CAT="${PAGER:-less}"
else
PAGER_OR_CAT=cat
fi
}
+1
View File
@@ -0,0 +1 @@
quote() { printf "'%s'" "$(printf %s "$1" | sed "s/'/'\\\\''/g")"; }
@@ -0,0 +1,22 @@
#!/bin/dash
# with_closed_fds -- run command with leaked file descriptors closed
#
# Shell libraries (e.g. hectic logger) may open extra file descriptors
# (like fd 3 as a dup of stderr). Child processes inherit these fds.
# Long-running daemons (postgres, postgrest) that keep fd 3 open can
# prevent the terminal from returning to the prompt even after the
# spawning script exits.
#
# Usage:
# with_closed_fds pg_ctl -D "$data" -w start
# with_closed_fds postgrest "$config" > "$log" 2>&1 &
#
# Runs the command in a subshell where fds 3-9 are redirected to
# /dev/null. The parent shell's fd table is untouched.
with_closed_fds() {
(
exec 3>/dev/null 4>/dev/null 5>/dev/null 6>/dev/null 7>/dev/null 8>/dev/null 9>/dev/null
"$@"
)
}
+27
View File
@@ -0,0 +1,27 @@
{ stdenv, steamcmd }: {
buildSteamServer = steamId: stdenv.mkDerivation {
pname = "astroneer-dedicated-server";
version = "latest";
src = null;
nativeBuildInputs = [
steamcmd
];
buildPhase = ''
export HOME=$TMPDIR
mkdir -p $out
steamcmd \
+force_install_dir $out \
+login anonymous \
+app_update ${steamId} validate \
+quit
'';
installPhase = "true";
dontFixup = true;
dontStrip = true;
};
}
+15
View File
@@ -0,0 +1,15 @@
{ callPackage }: rec {
writeShellApplication = callPackage ./writeShellApplication.nix {};
writeDash = callPackage ./writeDash.nix {};
writeC = callPackage ./writeC.nix {};
writeCBin = name: writeC "/bin/${name}";
writeMinCBin = name: includes: body: writeMinC "/bin/${name}" includes body;
writeMinC = name: includes: body:
writeC name ''
${builtins.concatStringsSep "\n" (map (h: "#include " + h) includes)}
int main(int argc, char *argv[]) {
${body}
}
'';
}
+35
View File
@@ -0,0 +1,35 @@
{ lib, writers, gcc }:
name: argsOrScript:
if
lib.isAttrs argsOrScript
&& !lib.isDerivation argsOrScript
then
writers.makeBinWriter (
argsOrScript
// {
compileScript = ''
# Force gcc to treat the input file as C code
${gcc}/bin/gcc -fsyntax-only -xc $contentPath
if [ $? -ne 0 ]; then
echo "Syntax check failed"
exit 1
fi
${gcc}/bin/gcc -xc -o $out $contentPath
'';
}
)
name
else
writers.makeBinWriter {
compileScript = ''
# Force gcc to treat the input file as C code
${gcc}/bin/gcc -fsyntax-only -xc $contentPath
if [ $? -ne 0 ]; then
echo "Syntax check failed"
exit 1
fi
${gcc}/bin/gcc -xc -o $out $contentPath
'';
}
name
argsOrScript
+5
View File
@@ -0,0 +1,5 @@
{ dash, lib, writers }: name: argsOrScript:
if lib.isAttrs argsOrScript && !lib.isDerivation argsOrScript then
writers.makeScriptWriter (argsOrScript // { interpreter = "${lib.getExe dash}"; }) name
else
writers.makeScriptWriter { interpreter = "${lib.getExe dash}"; } name argsOrScript
+157
View File
@@ -0,0 +1,157 @@
{
writeTextFile,
lib,
shellcheck-minimal,
stdenv,
runtimeShell,
}:
{
/*
The name of the script to write.
Type: String
*/
name,
/*
The shell script's text, not including a shebang.
Type: String
*/
text,
/*
Inputs to add to the shell script's `$PATH` at runtime.
Type: [String|Derivation]
*/
runtimeInputs ? [ ],
/*
Extra environment variables to set at runtime.
Type: AttrSet
*/
runtimeEnv ? null,
/*
`stdenv.mkDerivation`'s `meta` argument.
Type: AttrSet
*/
meta ? { },
/*
`stdenv.mkDerivation`'s `passthru` argument.
Type: AttrSet
*/
passthru ? { },
/*
The `checkPhase` to run. Defaults to `shellcheck` on supported
platforms and `bash -n`.
The script path will be given as `$target` in the `checkPhase`.
Type: String
*/
checkPhase ? null,
/*
Checks to exclude when running `shellcheck`, e.g. `[ "SC2016" ]`.
See <https://www.shellcheck.net/wiki/> for a list of checks.
Type: [String]
*/
excludeShellChecks ? [ ],
/*
Extra command-line flags to pass to ShellCheck.
Type: [String]
*/
extraShellCheckFlags ? [ ],
/*
Bash options to activate with `set -o` at the start of the script.
Defaults to `[ "errexit" "nounset" "pipefail" ]`.
Type: [String]
*/
bashOptions ? [
"errexit"
"nounset"
"pipefail"
],
/*
Extra arguments to pass to `stdenv.mkDerivation`.
:::{.caution}
Certain derivation attributes are used internally,
overriding those could cause problems.
:::
Type: AttrSet
*/
derivationArgs ? { },
/*
Whether to inherit the current `$PATH` in the script.
Type: Bool
*/
inheritPath ? true,
shell ? runtimeShell,
}:
writeTextFile {
inherit
name
meta
passthru
derivationArgs
;
executable = true;
destination = "/bin/${name}";
allowSubstitutes = true;
preferLocalBuild = false;
text = ''
#!${shell}
${lib.concatMapStringsSep "\n" (option: "set -o ${option}") bashOptions}
''
+ lib.optionalString (runtimeEnv != null) (
lib.concatStrings (
lib.mapAttrsToList (name: value: ''
${lib.toShellVar name value}
export ${name}
'') runtimeEnv
)
)
+ lib.optionalString (runtimeInputs != [ ]) ''
export PATH="${lib.makeBinPath runtimeInputs}${lib.optionalString inheritPath ":$PATH"}"
''
+ ''
${text}
'';
checkPhase =
let
excludeFlags = lib.optionals (excludeShellChecks != [ ]) [
"--exclude"
(lib.concatStringsSep "," excludeShellChecks)
];
# GHC (=> shellcheck) isn't supported on some platforms (such as risc-v)
# but we still want to use writeShellApplication on those platforms
shellcheckCommand = lib.optionalString shellcheck-minimal.compiler.bootstrapAvailable ''
# use shellcheck which does not include docs
# pandoc takes long to build and documentation isn't needed for just running the cli
${lib.getExe shellcheck-minimal} ${
lib.escapeShellArgs (excludeFlags ++ extraShellCheckFlags)
} "$target"
'';
in
if checkPhase == null then
''
runHook preCheck
${stdenv.shellDryRun} "$target"
${shellcheckCommand}
runHook postCheck
''
else
checkPhase;
}
+210
View File
@@ -0,0 +1,210 @@
{ flake, inputs, self }: let
nixpkgs = inputs.nixpkgs;
lib = nixpkgs.lib;
recursiveUpdate = nixpkgs.lib.recursiveUpdate;
envErrorMessage = varName: "Error: The ${varName} environment variable is not set.";
AllSystems = [
"aarch64-darwin"
"aarch64-linux"
"armv5tel-linux"
"armv6l-linux"
"armv7l-linux"
"i686-linux"
"mipsel-linux"
"powerpc64le-linux"
"riscv64-linux"
"x86_64-darwin"
"x86_64-linux"
];
commonSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forSystemsWithPkgs = supportedSystems: pkgOverlays: f:
builtins.foldl' (
acc: system: let
pkgs = import nixpkgs {
inherit system;
overlays = pkgOverlays;
};
systemOutputs = f {
system = system;
pkgs = pkgs;
};
in
recursiveUpdate acc systemOutputs
) {}
supportedSystems;
forAllSystemsWithPkgs = pkgOverlays: f: forSystemsWithPkgs AllSystems pkgOverlays f;
parseEnv = import ./parse-env.nix;
dotEnv = builtins.getEnv "DOTENV";
minorEnvironment =
if dotEnv != ""
then
if builtins.pathExists dotEnv
then parseEnv dotEnv
else throw "${dotEnv} file not exist"
else if builtins.pathExists ./.env
then parseEnv ./.env
else {};
in {
# -- For all systems --
inherit dotEnv minorEnvironment parseEnv forAllSystemsWithPkgs forSystemsWithPkgs commonSystems AllSystems;
forSystems = systems: nixpkgs.lib.genAttrs systems;
forAllSystems = nixpkgs.lib.genAttrs AllSystems;
shellModules = {
logs = builtins.readFile ./shell/logs.sh;
check-tool = builtins.readFile ./shell/check-tool.sh;
local-dir = builtins.readFile ./shell/local-dir.sh;
};
sharedShellAliases = {
jc = ''journalctl'';
sc = ''journalctl'';
nv = ''nvim'';
};
sharedShellAliasesForDevVm = self.lib.sharedShellAliases // {
sd = "shutdown now";
};
readEnvironment = { envVarsToRead, prefix ? "" }:
builtins.listToAttrs
(map (name: {
inherit name;
value = self.lib.getEnv "${prefix}${name}";
})
envVarsToRead);
# -- Env processing --
getEnv = varName: let
var = builtins.getEnv varName;
in
if var != ""
then var
else if minorEnvironment ? varName
then minorEnvironment."${varName}"
else throw (envErrorMessage varName);
# -- Cargo.toml --
cargoToml = src: (builtins.fromTOML (builtins.readFile "${src}/Cargo.toml"));
# Consolidated SQL bundles for the `hectic` schema. Single source of truth
# for everything that creates objects in the `hectic` namespace, used by
# migrator (init-time) and db-tool (postgres-init + hydrate). Consumers apply
# the full bundle via lib/hook/apply-hectic-bundle.sh.
#
# The whole hectic system shares one `versionString`; `hectic-version.sql`
# registers (`'hectic'`, versionString) into `hectic.version` and raises an
# exception on mismatch. Per-hook version rows are intentionally absent.
#
# Each entry exposes:
# * .sql — file contents as a string, with @HECTIC_VERSION@ substituted
# * .path — Nix store path (only on entries that need no substitution)
hectic = let
versionString = lib.fileContents ./hook/sql/HECTIC_VERSION;
static = path: { inherit path; sql = builtins.readFile path; };
templated = path: {
sql = builtins.replaceStrings
[ "@HECTIC_VERSION@" ]
[ versionString ]
(builtins.readFile path);
};
in {
inherit versionString;
version = templated ./hook/sql/hectic-version.sql;
secret = static ./hook/sql/hectic-secret.sql;
migration = static ./hook/sql/hectic-migration.sql;
inheritance = static ./hook/sql/hectic-inheritance.sql;
applyBundleScript = ./hook/apply-hectic-bundle.sh;
};
# Back-compat alias. Prefer `self.lib.hectic.inheritance`.
hecticInheritance = let
path = ./hook/sql/hectic-inheritance.sql;
in {
inherit path;
sql = builtins.readFile path;
};
ssh.keys = {
hetzner-test = {
yukkop = ''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8scy1tv6zfXX6xyaukhO/fsZwif5rC89DvXNc6XxOf'';
};
};
readPackages = callPackage: path: extraArgs:
with lib;
with builtins;
pipe path [
readDir
(filterAttrs (_: type: type == "directory"))
(filterAttrs (name: _: pathExists "${path}/${name}/default.nix"))
(mapAttrs (name: _: callPackage "${path}/${name}" extraArgs))
];
# Like readModulesRecursive, but reads module structure as a one-level keys,
# so that it is suited for `nix flake show`
# ```nix
# {
# "foo.bar" = import ./module/foo/bar.nix
# }
# ```
readModulesRecursive' = path: extraArgs:
with lib;
with builtins; let
paths = pipe "${path}" [
(filesystem.listFilesRecursive)
(filter (hasSuffix ".nix"))
];
pathToName = flip pipe [
(removePrefix "${path}/")
(replaceStrings ["/" ".nix"] ["." ""])
(removeSuffix ".nix")
];
attrList =
map (path': {
name = pathToName (unsafeDiscardStringContext path');
value = import path' extraArgs;
})
paths;
in
listToAttrs attrList;
nixpkgs-lib = nixpkgs.lib;
} // rec {
/* Supplied a directory, reads it's recursive structure into NixOS modules, so
that provided a `./module` dir with `module/foo/bar.nix` in it it outputs
```nix
{
foo.bar = import ./module/foo/bar.nix
}
```
*/
readModulesRecursive = path:
lib.mapAttrs' (
name: value: let
name' = builtins.replaceStrings [".nix"] [""] name;
in
if value == "regular"
then {
name = name';
value = import "${path}/${name}";
}
else {
inherit name;
value = readModulesRecursive "${path}/${name}";
}
) (builtins.readDir path);
}
+58
View File
@@ -0,0 +1,58 @@
#!/bin/dash
# Applies the full hectic SQL bundle to a PostgreSQL database, in order:
# 1. version (hard-fails on version mismatch)
# 2. secret (hectic.secret table + load_secrets_from_env + get_secret)
# 3. migration (hectic.migration table + domains + sha256_lower trigger)
# 4. inheritance (created_at/updated_at/immutable enforcement triggers)
#
# Idempotent: each SQL file uses IF NOT EXISTS / CREATE OR REPLACE.
#
# Required env (caller injects from Nix):
# HECTIC_VERSION_SQL - path to hectic-version.sql (substituted)
# HECTIC_SECRET_SQL - path to hectic-secret.sql
# HECTIC_MIGRATION_SQL - path to hectic-migration.sql
# HECTIC_INHERITANCE_SQL - path to hectic-inheritance.sql
#
# Usage:
# apply_hectic_bundle <PGURL> [<DOTENV_CONTENT>]
#
# If DOTENV_CONTENT is non-empty, it is loaded into hectic.secret via
# hectic.load_secrets_from_env() after the bundle is applied.
apply_hectic_bundle() {
pgurl="${1:-}"
env_content="${2:-}"
if [ -z "$pgurl" ]; then
printf '%s\n' 'apply-hectic-bundle: PGURL is required (arg 1)' >&2
return 3
fi
for var in HECTIC_VERSION_SQL HECTIC_SECRET_SQL HECTIC_MIGRATION_SQL HECTIC_INHERITANCE_SQL; do
eval "val=\${$var:-}"
if [ -z "$val" ]; then
printf '%s\n' "apply-hectic-bundle: $var not set" >&2
return 3
fi
if [ ! -r "$val" ]; then
printf '%s\n' "apply-hectic-bundle: $var not readable: $val" >&2
return 1
fi
done
psql "$pgurl" -v ON_ERROR_STOP=1 -f "$HECTIC_VERSION_SQL" || return 1
psql "$pgurl" -v ON_ERROR_STOP=1 -f "$HECTIC_SECRET_SQL" || return 1
psql "$pgurl" -v ON_ERROR_STOP=1 -f "$HECTIC_MIGRATION_SQL" || return 1
psql "$pgurl" -v ON_ERROR_STOP=1 -f "$HECTIC_INHERITANCE_SQL" || return 1
if [ -n "$env_content" ]; then
# Dollar-quote with $ps_env$ tag to preserve all content verbatim.
psql "$pgurl" -v ON_ERROR_STOP=1 <<SQL || return 1
SELECT hectic.load_secrets_from_env(\$ps_env\$
$env_content
\$ps_env\$);
SQL
fi
return 0
}
+1
View File
@@ -0,0 +1 @@
0.1.0
+101
View File
@@ -0,0 +1,101 @@
# hectic SQL bundle
Single source of truth for every object created in the `hectic` PostgreSQL
schema. Consumed by:
- `package/migrator` — applies the bundle on `migrator init` (mandatory).
- `package/db-tool` — applies the bundle in `database hydrate` (default; opt
out with `--no-hook`).
- External consumers (e.g. `proxydoe`) — invoke `psql -f` directly against the
paths exposed via `self.lib.hectic.*.path`.
## Layout
| File | Purpose |
| --- | --- |
| `HECTIC_VERSION` | Single version string for the whole bundle (e.g. `0.1.0`). Read via `lib.fileContents`. |
| `hectic-version.sql` | Templated. Creates `hectic.version`, inserts the current `versionString`, raises on mismatch. |
| `hectic-secret.sql` | Creates `hectic.secret`, `hectic.load_secrets_from_env(text)`, `hectic.get_secret(text)`. |
| `hectic-migration.sql` | Creates the `hectic.migration` table and supporting domains/triggers used by `migrator`. |
| `hectic-inheritance.sql` | Creates `hectic.created_at`, `hectic.updated_at`, `hectic.immutable` parent tables and the DDL event triggers that enforce inheritance, attach `BEFORE UPDATE` triggers, and block DML on immutable tables outside `migration_mode`. |
`hectic-version.sql` is templated at Nix evaluation time: `@HECTIC_VERSION@`
is substituted with the contents of `HECTIC_VERSION`. All other files are
applied verbatim.
## Apply order
The bundle MUST be applied in this order (enforced by
`apply-hectic-bundle.sh`):
1. `hectic-version.sql` — version check first; aborts the rest on mismatch.
2. `hectic-secret.sql`
3. `hectic-migration.sql`
4. `hectic-inheritance.sql`
Re-applying the bundle is idempotent — every CREATE uses
`IF NOT EXISTS` / `CREATE OR REPLACE`, and the version check accepts a row
that already matches.
## Nix API (`self.lib.hectic`)
```nix
self.lib.hectic = {
versionString; # e.g. "0.1.0"
version = { sql; }; # templated
secret = { sql; path; };
migration = { sql; path; };
inheritance = { sql; path; };
applyBundleScript; # ./hook/apply-hectic-bundle.sh
};
```
`.sql` is the file contents as a string. `.path` is the Nix store path of the
verbatim source (only available on non-templated entries; consumers needing a
materialized version of `version.sql` must do
`pkgs.runCommand "hectic-version.sql" { text = self.lib.hectic.version.sql; passAsFile = ["text"]; } ''cp "$textPath" "$out"''`).
## Shell helper (`apply-hectic-bundle.sh`)
`lib/hook/apply-hectic-bundle.sh` is a dash-compatible helper sourced by both
`migrator` and `db-tool`. Public entry point:
```sh
apply_hectic_bundle <PGURL> [<DOTENV_CONTENT>]
```
- `<PGURL>` — full PostgreSQL connection string.
- `<DOTENV_CONTENT>` — optional. When present, after applying the bundle the
helper invokes `hectic.load_secrets_from_env(<dotenv>)` inside a
dollar-quoted (`$ps_env$`) string so secret values cannot terminate the
literal.
Required environment (paths to the SQL files):
- `HECTIC_VERSION_SQL`
- `HECTIC_SECRET_SQL`
- `HECTIC_MIGRATION_SQL`
- `HECTIC_INHERITANCE_SQL`
`migrator` and `db-tool` set these via Nix at build time. External consumers
typically invoke `psql -f` against the paths directly instead of sourcing the
helper.
## Adding a new SQL file
1. Add `lib/hook/sql/hectic-<name>.sql`.
2. Wire it into `lib/default.nix` under `lib.hectic.<name>`.
3. Inject `HECTIC_<NAME>_SQL` in both `package/migrator/default.nix` and
`package/db-tool/default.nix`.
4. Append a `psql -f "$HECTIC_<NAME>_SQL"` step to
`lib/hook/apply-hectic-bundle.sh` in the correct order.
5. Bump `HECTIC_VERSION` if the new content changes existing semantics.
6. Update tests in `test/package/migrator/test/postgresql/init-hectic-bundle/`
and `test/package/db-tool/test/postgresql/hydrate-hook/`.
## Versioning
`HECTIC_VERSION` is a single global version for the bundle, not per-file.
Bump it on any breaking change to the schema. `hectic-version.sql` raises an
exception when the database row diverges from the bundle version, forcing a
deliberate migration before the rest of the bundle runs.
+257
View File
@@ -0,0 +1,257 @@
-- hectic.created_at / hectic.updated_at / hectic.immutable inheritance machinery.
--
-- Provides:
-- * schema hectic
-- * tables hectic.created_at(created_at TIMESTAMPTZ NOT NULL DEFAULT NOW())
-- hectic.updated_at(updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW())
-- hectic.immutable() -- pure marker
-- * function hectic.set_updated_at() -- BEFORE UPDATE row trigger function
-- * function hectic.block_immutable_dml()
-- BEFORE INSERT/UPDATE/DELETE/TRUNCATE row+statement trigger function;
-- allows DML iff current_setting('hectic.migration_mode', true) = 'on'.
-- * GUC hectic.inheritance_extra_excluded_schemas
-- (text, comma-separated list of schemas the enforcement trigger skips)
-- * GUC hectic.migration_mode
-- (text, 'on' enables DML on tables inheriting hectic.immutable.
-- Intended use: SET LOCAL inside a migration transaction.)
-- * event trigger hectic_enforce_created_at_inheritance
-- RAISE EXCEPTION on CREATE TABLE that does not inherit hectic.created_at
-- * event trigger hectic_attach_updated_at_trigger
-- auto-attaches BEFORE UPDATE row trigger calling hectic.set_updated_at()
-- on any new table that inherits hectic.updated_at and lacks one.
-- * event trigger hectic_attach_immutable_triggers
-- auto-attaches BEFORE INSERT/UPDATE/DELETE FOR EACH ROW and BEFORE
-- TRUNCATE FOR EACH STATEMENT triggers calling hectic.block_immutable_dml()
-- on any new table that inherits hectic.immutable and lacks them.
--
-- Idempotent: safe to run on an already-bootstrapped database.
CREATE SCHEMA IF NOT EXISTS "hectic";
CREATE TABLE IF NOT EXISTS "hectic"."created_at" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS "hectic"."updated_at" (
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS "hectic"."immutable" ();
DO $bootstrap$
BEGIN
PERFORM set_config('hectic.inheritance_extra_excluded_schemas',
current_setting('hectic.inheritance_extra_excluded_schemas', true),
false);
EXCEPTION WHEN undefined_object THEN
PERFORM set_config('hectic.inheritance_extra_excluded_schemas', '', false);
END
$bootstrap$;
DO $bootstrap_mm$
BEGIN
PERFORM set_config('hectic.migration_mode',
current_setting('hectic.migration_mode', true),
false);
EXCEPTION WHEN undefined_object THEN
PERFORM set_config('hectic.migration_mode', '', false);
END
$bootstrap_mm$;
CREATE OR REPLACE FUNCTION "hectic"."set_updated_at"() RETURNS trigger
LANGUAGE plpgsql AS $fn$
BEGIN
NEW."updated_at" := NOW();
RETURN NEW;
END
$fn$;
CREATE OR REPLACE FUNCTION "hectic"."block_immutable_dml"() RETURNS trigger
LANGUAGE plpgsql AS $fn$
DECLARE
mm text;
BEGIN
BEGIN
mm := current_setting('hectic.migration_mode', true);
EXCEPTION WHEN OTHERS THEN
mm := '';
END;
IF mm = 'on' THEN
IF TG_LEVEL = 'STATEMENT' THEN RETURN NULL; END IF;
IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
RETURN NEW;
END IF;
RAISE EXCEPTION
'hectic: table %.% inherits hectic.immutable; % blocked outside migration_mode',
quote_ident(TG_TABLE_SCHEMA), quote_ident(TG_TABLE_NAME), TG_OP
USING HINT = 'wrap the statement in a migration transaction with '
|| 'SET LOCAL hectic.migration_mode = ''on''';
END
$fn$;
CREATE OR REPLACE FUNCTION "hectic"."_is_excluded_schema"(p_schema text) RETURNS boolean
LANGUAGE plpgsql STABLE AS $fn$
DECLARE
extra text;
s text;
BEGIN
IF p_schema = 'hectic'
OR p_schema = 'information_schema'
OR p_schema LIKE 'pg\_%' ESCAPE '\'
THEN
RETURN true;
END IF;
BEGIN
extra := current_setting('hectic.inheritance_extra_excluded_schemas', true);
EXCEPTION WHEN OTHERS THEN
extra := '';
END;
IF extra IS NULL OR extra = '' THEN
RETURN false;
END IF;
FOREACH s IN ARRAY string_to_array(extra, ',') LOOP
IF btrim(s) = p_schema THEN
RETURN true;
END IF;
END LOOP;
RETURN false;
END
$fn$;
CREATE OR REPLACE FUNCTION "hectic"."_table_inherits"(p_oid oid, p_parent regclass) RETURNS boolean
LANGUAGE sql STABLE AS $fn$
SELECT EXISTS (
SELECT 1 FROM pg_inherits
WHERE inhrelid = p_oid AND inhparent = p_parent
);
$fn$;
CREATE OR REPLACE FUNCTION "hectic"."enforce_created_at_inheritance"() RETURNS event_trigger
LANGUAGE plpgsql AS $fn$
DECLARE
obj record;
rel pg_class;
schema_name text;
parent_oid oid;
BEGIN
parent_oid := 'hectic.created_at'::regclass;
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag = 'CREATE TABLE'
LOOP
SELECT * INTO rel FROM pg_class WHERE oid = obj.objid;
IF NOT FOUND THEN CONTINUE; END IF;
IF rel.relpersistence = 't' THEN CONTINUE; END IF;
IF rel.relispartition THEN CONTINUE; END IF;
SELECT nspname INTO schema_name FROM pg_namespace WHERE oid = rel.relnamespace;
IF "hectic"."_is_excluded_schema"(schema_name) THEN CONTINUE; END IF;
IF NOT "hectic"."_table_inherits"(rel.oid, parent_oid) THEN
RAISE EXCEPTION
'hectic: table %.% must INHERITS (hectic.created_at)',
quote_ident(schema_name), quote_ident(rel.relname)
USING HINT = 'add INHERITS ("hectic"."created_at") to the CREATE TABLE statement, '
|| 'or add the schema to hectic.inheritance_extra_excluded_schemas';
END IF;
END LOOP;
END
$fn$;
CREATE OR REPLACE FUNCTION "hectic"."attach_updated_at_trigger"() RETURNS event_trigger
LANGUAGE plpgsql AS $fn$
DECLARE
obj record;
rel pg_class;
schema_name text;
parent_oid oid;
trigger_name text;
has_trigger boolean;
BEGIN
parent_oid := 'hectic.updated_at'::regclass;
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag = 'CREATE TABLE'
LOOP
SELECT * INTO rel FROM pg_class WHERE oid = obj.objid;
IF NOT FOUND THEN CONTINUE; END IF;
IF rel.relpersistence = 't' THEN CONTINUE; END IF;
IF rel.relispartition THEN CONTINUE; END IF;
SELECT nspname INTO schema_name FROM pg_namespace WHERE oid = rel.relnamespace;
IF schema_name = 'hectic' THEN CONTINUE; END IF;
IF NOT "hectic"."_table_inherits"(rel.oid, parent_oid) THEN CONTINUE; END IF;
trigger_name := 'hectic_set_updated_at';
SELECT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgrelid = rel.oid AND tgname = trigger_name AND NOT tgisinternal
) INTO has_trigger;
IF has_trigger THEN CONTINUE; END IF;
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE ON %I.%I FOR EACH ROW EXECUTE FUNCTION "hectic"."set_updated_at"()',
trigger_name, schema_name, rel.relname
);
END LOOP;
END
$fn$;
DROP EVENT TRIGGER IF EXISTS "hectic_enforce_created_at_inheritance";
CREATE EVENT TRIGGER "hectic_enforce_created_at_inheritance"
ON ddl_command_end
WHEN TAG IN ('CREATE TABLE')
EXECUTE FUNCTION "hectic"."enforce_created_at_inheritance"();
DROP EVENT TRIGGER IF EXISTS "hectic_attach_updated_at_trigger";
CREATE EVENT TRIGGER "hectic_attach_updated_at_trigger"
ON ddl_command_end
WHEN TAG IN ('CREATE TABLE')
EXECUTE FUNCTION "hectic"."attach_updated_at_trigger"();
CREATE OR REPLACE FUNCTION "hectic"."attach_immutable_triggers"() RETURNS event_trigger
LANGUAGE plpgsql AS $fn$
DECLARE
obj record;
rel pg_class;
schema_name text;
parent_oid oid;
has_row boolean;
has_trunc boolean;
BEGIN
parent_oid := 'hectic.immutable'::regclass;
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands() WHERE command_tag = 'CREATE TABLE'
LOOP
SELECT * INTO rel FROM pg_class WHERE oid = obj.objid;
IF NOT FOUND THEN CONTINUE; END IF;
IF rel.relpersistence = 't' THEN CONTINUE; END IF;
IF rel.relispartition THEN CONTINUE; END IF;
SELECT nspname INTO schema_name FROM pg_namespace WHERE oid = rel.relnamespace;
IF schema_name = 'hectic' THEN CONTINUE; END IF;
IF NOT "hectic"."_table_inherits"(rel.oid, parent_oid) THEN CONTINUE; END IF;
SELECT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgrelid = rel.oid
AND tgname = 'hectic_block_immutable_dml'
AND NOT tgisinternal
) INTO has_row;
SELECT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgrelid = rel.oid
AND tgname = 'hectic_block_immutable_truncate'
AND NOT tgisinternal
) INTO has_trunc;
IF NOT has_row THEN
EXECUTE format(
'CREATE TRIGGER %I BEFORE INSERT OR UPDATE OR DELETE ON %I.%I '
|| 'FOR EACH ROW EXECUTE FUNCTION "hectic"."block_immutable_dml"()',
'hectic_block_immutable_dml', schema_name, rel.relname
);
END IF;
IF NOT has_trunc THEN
EXECUTE format(
'CREATE TRIGGER %I BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON %I.%I '
|| 'FOR EACH STATEMENT EXECUTE FUNCTION "hectic"."block_immutable_dml"()',
'hectic_block_immutable_truncate', schema_name, rel.relname
);
END IF;
END LOOP;
END
$fn$;
DROP EVENT TRIGGER IF EXISTS "hectic_attach_immutable_triggers";
CREATE EVENT TRIGGER "hectic_attach_immutable_triggers"
ON ddl_command_end
WHEN TAG IN ('CREATE TABLE')
EXECUTE FUNCTION "hectic"."attach_immutable_triggers"();
+51
View File
@@ -0,0 +1,51 @@
DO $bootstrap$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = 'hectic' AND t.typname = 'migration_name'
) THEN
CREATE DOMAIN "hectic"."migration_name" AS TEXT
CHECK (VALUE ~ '^[0-9]{14}-.*');
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_type t JOIN pg_namespace n ON n.oid = t.typnamespace
WHERE n.nspname = 'hectic' AND t.typname = 'sha256'
) THEN
CREATE DOMAIN "hectic"."sha256" AS CHAR(64)
CHECK (VALUE ~ '^[0-9a-f]{64}$');
END IF;
END
$bootstrap$;
CREATE OR REPLACE FUNCTION "hectic"."sha256_lower"() RETURNS trigger
LANGUAGE plpgsql AS $fn$
BEGIN
NEW."hash" := lower(NEW."hash");
RETURN NEW;
END
$fn$;
CREATE TABLE IF NOT EXISTS "hectic"."migration" (
"id" SERIAL PRIMARY KEY,
"name" "hectic"."migration_name" UNIQUE NOT NULL,
"hash" "hectic"."sha256" UNIQUE NOT NULL,
"applied_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
DO $trg$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger
WHERE tgname = 'hectic_t_sha256_lower'
AND tgrelid = '"hectic"."migration"'::regclass
AND NOT tgisinternal
) THEN
CREATE TRIGGER "hectic_t_sha256_lower"
BEFORE INSERT OR UPDATE ON "hectic"."migration"
FOR EACH ROW EXECUTE FUNCTION "hectic"."sha256_lower"();
END IF;
END
$trg$;
+51
View File
@@ -0,0 +1,51 @@
CREATE TABLE IF NOT EXISTS "hectic"."secret" (
"id" SERIAL PRIMARY KEY,
"key" TEXT UNIQUE NOT NULL,
"value" TEXT NOT NULL
);
CREATE OR REPLACE FUNCTION "hectic"."load_secrets_from_env"(env_content TEXT)
RETURNS void
LANGUAGE plpgsql AS $fn$
DECLARE
line TEXT;
k TEXT;
v TEXT;
BEGIN
TRUNCATE TABLE "hectic"."secret";
FOR line IN
SELECT regexp_split_to_table(env_content, E'\n')
LOOP
line := btrim(line);
IF line = '' OR line LIKE '#%' THEN
CONTINUE;
END IF;
k := split_part(line, '=', 1);
v := substring(line FROM position('=' IN line) + 1);
k := btrim(k);
v := btrim(v);
IF v ~ '^".*"$' OR v ~ '^''.*''$' THEN
v := substring(v FROM 2 FOR char_length(v) - 2);
END IF;
INSERT INTO "hectic"."secret" ("key", "value") VALUES (k, v);
END LOOP;
END
$fn$;
CREATE OR REPLACE FUNCTION "hectic"."get_secret"(k TEXT)
RETURNS TEXT
LANGUAGE plpgsql AS $fn$
BEGIN
RETURN (
SELECT "value"
FROM "hectic"."secret"
WHERE "key" = k
);
END
$fn$;
+26
View File
@@ -0,0 +1,26 @@
CREATE SCHEMA IF NOT EXISTS "hectic";
CREATE TABLE IF NOT EXISTS "hectic"."version" (
"name" TEXT PRIMARY KEY,
"version" TEXT NOT NULL,
"installed_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
DO $check$
DECLARE
existing TEXT;
BEGIN
SELECT "version" INTO existing
FROM "hectic"."version"
WHERE "name" = 'hectic';
IF existing IS NULL THEN
INSERT INTO "hectic"."version" ("name", "version")
VALUES ('hectic', '@HECTIC_VERSION@');
ELSIF existing <> '@HECTIC_VERSION@' THEN
RAISE EXCEPTION
'hectic schema version mismatch: database has %, code expects %',
existing, '@HECTIC_VERSION@';
END IF;
END
$check$;
+13
View File
@@ -0,0 +1,13 @@
file: let
envText = builtins.readFile file;
envLines = builtins.split "\n" envText;
lines = builtins.filter (line: (builtins.match "^.*=.*" line) != null) envLines;
#attributes = builtins.listToAttrs (builtins.map (line: let
# parts = builtins.split "=" line;
# key = builtins.substring 0 (builtins.stringLength parts[0] - 3) parts[0]; # Remove "var" prefix
# value = parts[1];
#in {
# name = key;
# value = value;
#}) lines);
in { inherit envLines lines; }
+6
View File
@@ -0,0 +1,6 @@
check_tool() {
if ! command -v "$1" >/dev/null; then
echo "Required tool \`$2\` are not installed or binary \`$1\` not found." >&2
exit 1
fi
}
+14
View File
@@ -0,0 +1,14 @@
printf '\033[0;34mDetecting local directories...\033[0m\n'
if git_root=$($BIN_GIT rev-parse --show-toplevel 2>/dev/null); then
LOCAL_DIR="$git_root"
printf '\033[0;32mFound git root: \033[1;37m%s\033[0m\n' "$LOCAL_DIR"
else
LOCAL_DIR="$(pwd)"
printf '\033[1;33mNot in git repo, using current dir: \033[1;37m%s\033[0m\n' "$LOCAL_DIR"
printf 'Are you realy want continue? (y/n):\n'
read -r CONTINUE
if [ "$CONTINUE" != "y" ]; then
printf '\033[0;31mAborting...\033[0m\n'
exit 0
fi
fi
+23
View File
@@ -0,0 +1,23 @@
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
MAGENTA="$PURPLE"
CYAN='\033[0;36m'
WHITE='\033[1;37m'
NC='\033[0m' # No Color
LOG_PATH="/var/log/hectic/activation.log"
if ! mkdir -p "$(dirname "$LOG_PATH")" 2>/dev/null; then
LOG_PATH="/dev/null"
fi
log_info() { text=$1; shift; printf "%b ${text}%b\n" "$BLUE" "$@" "$NC" | tee -a "$LOG_PATH" >&2; }
log_success() { text=$1; shift; printf "%b ${text}%b\n" "$GREEN" "$@" "$NC" | tee -a "$LOG_PATH" >&2; }
log_warning() { text=$1; shift; printf "%b ${text}%b\n" "$YELLOW" "$@" "$NC" | tee -a "$LOG_PATH" >&2; }
log_error() { text=$1; shift; printf "%b ${text}%b\n" "$RED" "$@" "$NC" | tee -a "$LOG_PATH" >&2; }
log_step() { text=$1; shift; printf "%b ${text}%b\n" "$PURPLE" "$@" "$NC" | tee -a "$LOG_PATH" >&2; }
log_header() { printf "\n%b=== %s ===%b\n" "$WHITE" "$@" "$NC" | tee -a "$LOG_PATH" >&2; }
+21
View File
@@ -0,0 +1,21 @@
{
flake,
self,
inputs,
}:
with builtins;
with inputs.nixpkgs.lib;
with self.lib;
let
# Combine hectic modules into one
hectic.imports = attrValues (
readModulesRecursive' ./hectic { inherit flake self inputs; }
);
# Read generic modules separately
generic = readModulesRecursive'
./generic
{ inherit flake self inputs; };
in generic // {
inherit hectic;
default = hectic;
}
@@ -0,0 +1,43 @@
{
inputs,
flake,
self,
}: {
lib,
config,
...
}: let
userNames = [
"yukkop"
"liquiz"
"vismajor"
"lvgkcfjl"
"MrAlex0O"
"Антоша"
"snuff"
];
adminNames = [ "yukkop" ];
in {
hectic.generic.matrix-cluster.users = builtins.listToAttrs (
map (name: {
inherit name;
value = {
passwordFile = config.sops.secrets."matrix/users/${name}/password".path;
} // lib.optionalAttrs (builtins.elem name adminNames) {
admin = true;
};
}) userNames
);
sops.secrets = builtins.listToAttrs (
map (name: {
name = "matrix/users/${name}/password";
value = {
key = "matrix/users/${name}/password";
owner = "matrix-synapse";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
}) userNames
);
}
+592
View File
@@ -0,0 +1,592 @@
{
inputs,
flake,
self,
}: {
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.generic.matrix-cluster;
s3Cfg = cfg.objectStorage.s3;
s3Plugin = pkgs.matrix-synapse-plugins.matrix-synapse-s3-storage-provider;
s3ConfigDir = "/run/matrix-synapse";
s3ConfigFile = "${s3ConfigDir}/s3-media-storage.yaml";
pgDataDir = "/var/lib/postgresql/17";
matrixUsers = builtins.attrNames cfg.users;
mkUserRegistration = name: let
user = cfg.users.${name};
adminFlag = if user.admin then "--admin" else "--no-admin";
in ''
if [ ! -r "${user.passwordFile}" ]; then
printf 'Missing Matrix password file for %s: %s\n' '${name}' '${user.passwordFile}' >&2
exit 1
fi
${pkgs.matrix-synapse}/bin/register_new_matrix_user \
-u '${name}' \
-p "$(tr -d '\n' < "${user.passwordFile}")" \
-k "$REGISTRATION_SHARED_SECRET" \
${adminFlag} \
http://127.0.0.1:8008 || true
'';
synapseEnabled =
if cfg.overrideEnableSynapse != null
then cfg.overrideEnableSynapse
else cfg.role == "primary";
mkS3Config = ''
if [ ! -r "${s3Cfg.credentialsFile}" ]; then
printf 'Missing Matrix object storage credentials file: %s\n' '${s3Cfg.credentialsFile}' >&2
exit 1
fi
. "${s3Cfg.credentialsFile}"
if [ -z "$ACCESS_KEY_ID" ] || [ -z "$SECRET_ACCESS_KEY" ]; then
printf 'ACCESS_KEY_ID or SECRET_ACCESS_KEY missing in %s\n' '${s3Cfg.credentialsFile}' >&2
exit 1
fi
mkdir -p "${s3ConfigDir}"
cat > "${s3ConfigFile}" <<EOF
media_storage_providers:
- module: s3_storage_provider.S3StorageProviderBackend
store_local: ${lib.boolToString s3Cfg.storeLocal}
store_remote: ${lib.boolToString s3Cfg.storeRemote}
store_synchronous: ${lib.boolToString s3Cfg.storeSynchronous}
config:
bucket: ${s3Cfg.bucket}
endpoint_url: ${s3Cfg.endpointUrl}
region_name: ${s3Cfg.regionName}
prefix: "${s3Cfg.prefix}"
storage_class: "${s3Cfg.storageClass}"
threadpool_size: ${toString s3Cfg.threadpoolSize}
access_key_id: $ACCESS_KEY_ID
secret_access_key: $SECRET_ACCESS_KEY
EOF
chown matrix-synapse:matrix-synapse "${s3ConfigFile}"
chmod 0400 "${s3ConfigFile}"
'';
in {
options.hectic.generic.matrix-cluster = {
enable = lib.mkEnableOption "Matrix Synapse active/passive cluster node";
role = lib.mkOption {
type = lib.types.enum [ "primary" "standby" ];
description = ''
Cluster role of this node. The primary runs Synapse and accepts WAL
streaming connections; the standby runs a hot-standby Postgres replica
only and keeps Synapse disabled until failover.
'';
};
matrixDomain = lib.mkOption {
type = lib.types.str;
description = "Matrix server_name (also nginx vhost / ACME cert name).";
};
signingKeyFile = lib.mkOption {
type = lib.types.path;
description = ''
Path to the Synapse homeserver signing key. Mounted into place at
/var/lib/matrix-synapse/homeserver.signing.key on activation.
'';
};
secretsFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Extra Synapse YAML config (registration_shared_secret, macaroon_secret_key,
form_secret). Loaded via matrix-synapse extraConfigFiles. Required when
Synapse is enabled on this node (primary, or standby after failover).
'';
};
turnSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Shared secret file used by coturn for Matrix voice/video calls.
When set together with `publicIp`, the active Synapse node also enables
coturn and publishes TURN URIs to clients.
'';
};
publicIp = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Public IP address advertised to coturn for listening and relaying.
'';
};
maxUploadSize = lib.mkOption {
type = lib.types.str;
default = "2G";
};
enableRegistration = lib.mkOption {
type = lib.types.bool;
default = false;
};
users = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
passwordFile = lib.mkOption { type = lib.types.str; };
admin = lib.mkOption { type = lib.types.bool; default = false; };
};
});
default = {};
description = "Declarative Matrix users provisioned via register_new_matrix_user.";
};
overrideEnableSynapse = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
description = ''
When non-null, forces Synapse on/off regardless of role. Used during
failover: set to true on the standby once it has been promoted, or
false on the primary to drain it.
'';
};
objectStorage.s3 = {
bucket = lib.mkOption { type = lib.types.str; };
regionName = lib.mkOption { type = lib.types.str; };
endpointUrl = lib.mkOption { type = lib.types.str; };
credentialsFile = lib.mkOption {
type = lib.types.path;
description = ''
env-style file with ACCESS_KEY_ID= and SECRET_ACCESS_KEY=. MUST be
the SAME credentials/bucket on both primary and standby.
'';
};
mediaStorePath = lib.mkOption {
type = lib.types.str;
default = "/var/lib/matrix-synapse/media_store";
};
prefix = lib.mkOption { type = lib.types.str; default = ""; };
storageClass = lib.mkOption { type = lib.types.str; default = "STANDARD"; };
threadpoolSize = lib.mkOption { type = lib.types.int; default = 40; };
storeLocal = lib.mkOption { type = lib.types.bool; default = true; };
storeRemote = lib.mkOption { type = lib.types.bool; default = true; };
storeSynchronous = lib.mkOption { type = lib.types.bool; default = true; };
};
replication = {
peerHost = lib.mkOption {
type = lib.types.str;
description = "Public IP/hostname of the other cluster node.";
};
peerPort = lib.mkOption {
type = lib.types.port;
default = 5432;
};
passwordFile = lib.mkOption {
type = lib.types.path;
description = ''
File containing either a raw replication password or a libpq passfile
line. Used as `passfile=` in primary_conninfo on the standby and to
set the password of the `replication` Postgres role on the primary.
'';
};
allowedSourceIPs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = ''
CIDRs allowed to connect to Postgres for replication. Used on the
primary in pg_hba.conf hostssl entries and to gate the firewall.
'';
};
sslMode = lib.mkOption {
type = lib.types.str;
default = "require";
};
};
acme = {
enable = lib.mkEnableOption "Porkbun DNS-01 ACME for matrixDomain";
email = lib.mkOption {
type = lib.types.str;
default = "hectic.yukkop.it@gmail.com";
description = "ACME registration email (passed to security.acme.defaults.email).";
};
porkbunApiKeyFile = lib.mkOption {
type = lib.types.path;
description = "File containing PORKBUN_API_KEY value.";
};
porkbunSecretApiKeyFile = lib.mkOption {
type = lib.types.path;
description = "File containing PORKBUN_SECRET_API_KEY value.";
};
};
jitsi.preferredDomain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Optional self-hosted Jitsi Meet domain to advertise to Matrix/Element
clients alongside the cluster-managed homeserver.
'';
};
};
config = lib.mkIf cfg.enable (lib.mkMerge [
{
# signing key mount: copy into matrix-synapse data dir with correct perms
# regardless of whether Synapse is currently enabled on this node, so a
# failover flip does not need a separate provisioning step.
systemd.tmpfiles.rules = [
"d /var/lib/matrix-synapse 0750 matrix-synapse matrix-synapse -"
];
systemd.services.matrix-cluster-signing-key = {
description = "Install Matrix Synapse signing key from secrets";
wantedBy = [ "multi-user.target" ];
before = lib.optional synapseEnabled "matrix-synapse.service";
requiredBy = lib.optional synapseEnabled "matrix-synapse.service";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -eu
install -d -o matrix-synapse -g matrix-synapse -m 0750 /var/lib/matrix-synapse
install -o matrix-synapse -g matrix-synapse -m 0400 \
"${cfg.signingKeyFile}" \
/var/lib/matrix-synapse/homeserver.signing.key
'';
};
users.users.matrix-synapse = {
isSystemUser = true;
group = "matrix-synapse";
};
users.groups.matrix-synapse = {};
}
(lib.mkIf synapseEnabled {
assertions = [
{
assertion = cfg.secretsFile != null;
message = "hectic.generic.matrix-cluster.secretsFile must be set when Synapse runs on this node.";
}
{
assertion = (cfg.turnSecretFile == null) == (cfg.publicIp == null);
message = "hectic.generic.matrix-cluster.turnSecretFile and publicIp must be set together.";
}
];
services.coturn = lib.mkIf (cfg.turnSecretFile != null) rec {
enable = true;
realm = cfg.matrixDomain;
use-auth-secret = true;
static-auth-secret-file = cfg.turnSecretFile;
cert = "${config.security.acme.certs.${realm}.directory}/full.pem";
pkey = "${config.security.acme.certs.${realm}.directory}/key.pem";
listening-ips = [ cfg.publicIp ];
relay-ips = [ cfg.publicIp ];
listening-port = 3478;
tls-listening-port = 5349;
no-cli = true;
extraConfig = ''
verbose
'';
};
services.matrix-synapse = {
enable = true;
plugins = [ s3Plugin ];
extraConfigFiles = [ cfg.secretsFile s3ConfigFile ];
settings = {
server_name = cfg.matrixDomain;
public_baseurl = "https://${cfg.matrixDomain}";
max_upload_size = cfg.maxUploadSize;
media_store_path = s3Cfg.mediaStorePath;
signing_key_path = "/var/lib/matrix-synapse/homeserver.signing.key";
experimental_features = {
msc3266_enabled = true;
msc4140_enabled = true;
msc4143_enabled = true;
msc4222_enabled = true;
};
matrix_rtc = {
transports = [
{
type = "livekit";
livekit_service_url = "https://${cfg.matrixDomain}/livekit/jwt";
}
];
};
listeners = [
{
port = 8008;
bind_addresses = [ "0.0.0.0" ];
type = "http";
tls = false;
resources = [
{
names = [ "client" "federation" "openid" ];
compress = false;
}
];
}
];
enable_registration = cfg.enableRegistration;
enable_registration_without_verification = cfg.enableRegistration;
} // lib.optionalAttrs (cfg.turnSecretFile != null) {
turn_uris = [
"turn:${cfg.matrixDomain}:3478?transport=udp"
"turn:${cfg.matrixDomain}:3478?transport=tcp"
"turns:${cfg.matrixDomain}:5349?transport=udp"
"turns:${cfg.matrixDomain}:5349?transport=tcp"
];
turn_user_lifetime = 86400000;
turn_allow_guests = true;
};
};
environment.systemPackages = [ pkgs.matrix-synapse ];
systemd.services.matrix-synapse-s3-config = {
description = "Generate Synapse S3 media storage config";
before = [ "matrix-synapse.service" ];
requiredBy = [ "matrix-synapse.service" ];
serviceConfig.Type = "oneshot";
script = mkS3Config;
};
services.nginx = {
enable = true;
virtualHosts.${cfg.matrixDomain} = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://127.0.0.1:8008";
extraConfig = ''
client_max_body_size ${cfg.maxUploadSize};
'';
};
locations."=/.well-known/matrix/server" = {
extraConfig = ''
default_type application/json;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization";
'';
return = "200 '{\"m.server\": \"${cfg.matrixDomain}:443\"}'";
};
};
};
networking.firewall = lib.mkIf (cfg.turnSecretFile != null) {
allowedUDPPorts = [ 3478 5349 ];
allowedTCPPorts = [ 3478 5349 ];
allowedTCPPortRanges = [
{
from = 49152;
to = 65535;
}
];
allowedUDPPortRanges = [
{
from = 49152;
to = 65535;
}
];
};
systemd.services.matrix-synapse-users = lib.mkIf (matrixUsers != []) {
description = "Provision Matrix Synapse users";
wantedBy = [ "multi-user.target" ];
after = [ "matrix-synapse.service" ];
requires = [ "matrix-synapse.service" ];
path = with pkgs; [ curl coreutils gawk ];
serviceConfig = {
Type = "oneshot";
User = "matrix-synapse";
};
script = ''
until curl -sf http://127.0.0.1:8008/_matrix/client/versions >/dev/null; do
sleep 2
done
REGISTRATION_SHARED_SECRET="$(awk -F': *' '$1 == "registration_shared_secret" { print $2; exit }' "${cfg.secretsFile}")"
if [ -z "$REGISTRATION_SHARED_SECRET" ]; then
printf 'registration_shared_secret not found in %s\n' '${cfg.secretsFile}' >&2
exit 1
fi
${lib.concatStringsSep "\n" (map mkUserRegistration matrixUsers)}
'';
};
})
{
services.postgresql = {
enable = true;
package = pkgs.postgresql_17;
enableTCPIP = true;
initdbArgs = [ "--locale=C" "--encoding=UTF8" ];
settings = {
wal_level = "replica";
max_wal_senders = 4;
hot_standby = "on";
};
};
}
(lib.mkIf (cfg.role == "primary") {
services.postgresql = {
authentication = lib.concatStringsSep "\n" ([
"local all all trust"
"host sameuser all 127.0.0.1/32 scram-sha-256"
"host sameuser all ::1/128 scram-sha-256"
"host all all ::1/128 scram-sha-256"
"host all all 0.0.0.0/0 scram-sha-256"
"host replication postgres 127.0.0.1/32 scram-sha-256"
"host replication postgres ::1/128 scram-sha-256"
] ++ map (cidr:
"hostssl replication replication ${cidr} scram-sha-256"
) cfg.replication.allowedSourceIPs);
ensureUsers = [
{
name = "replication";
ensureClauses = {
login = true;
replication = true;
};
}
];
};
# Apply replication password from SOPS-mounted file after postgres start.
systemd.services.matrix-cluster-replication-password = {
description = "Set Postgres replication role password from SOPS";
wantedBy = [ "multi-user.target" ];
after = [ "postgresql.service" ];
requires = [ "postgresql.service" ];
serviceConfig = {
Type = "oneshot";
User = "postgres";
RemainAfterExit = true;
};
script = ''
set -eu
PW="$(tr -d '\n' < "${cfg.replication.passwordFile}")"
${config.services.postgresql.package}/bin/psql -v ON_ERROR_STOP=1 -c \
"ALTER ROLE replication WITH LOGIN REPLICATION PASSWORD '$PW';"
'';
};
})
(lib.mkIf (cfg.role == "standby") {
systemd.targets.postgresql.requires = lib.mkForce [
"postgresql.service"
];
# Hot-standby bootstrap: standby.signal + primary_conninfo with passfile.
# pg_basebackup must be run manually (see runbook) before this activates
# for the first time.
systemd.services.matrix-cluster-standby-bootstrap = {
description = "Configure Matrix Postgres hot standby";
wantedBy = [ "postgresql.service" ];
before = [ "postgresql.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -eu
if [ ! -d "${pgDataDir}" ]; then
echo "Postgres data dir ${pgDataDir} missing; run pg_basebackup first (see MATRIX-FAILOVER-RUNBOOK.md)" >&2
exit 0
fi
# Materialize a libpq passfile from the raw password secret.
PASSFILE=/var/lib/postgresql/.matrix-cluster-replication.passfile
PW="$(tr -d '\n' < "${cfg.replication.passwordFile}")"
umask 077
printf '%s:%d:replication:replication:%s\n' \
'${cfg.replication.peerHost}' \
${toString cfg.replication.peerPort} \
"$PW" > "$PASSFILE"
chown postgres:postgres "$PASSFILE"
chmod 0600 "$PASSFILE"
touch "${pgDataDir}/standby.signal"
chown postgres:postgres "${pgDataDir}/standby.signal"
CONF="${pgDataDir}/postgresql.auto.conf"
touch "$CONF"
chown postgres:postgres "$CONF"
# Strip any prior primary_conninfo line, then append fresh one.
${pkgs.gnused}/bin/sed -i '/^primary_conninfo/d' "$CONF"
printf "primary_conninfo = 'host=%s port=%d user=replication passfile=%s sslmode=%s'\n" \
'${cfg.replication.peerHost}' \
${toString cfg.replication.peerPort} \
"$PASSFILE" \
'${cfg.replication.sslMode}' >> "$CONF"
'';
};
})
(lib.mkIf cfg.acme.enable {
security.acme = {
acceptTerms = true;
defaults.email = lib.mkDefault cfg.acme.email;
certs.${cfg.matrixDomain} = {
dnsProvider = "porkbun";
webroot = lib.mkForce null;
environmentFile = "/run/matrix-cluster/porkbun.env";
};
};
systemd.services.matrix-cluster-acme-env = {
description = "Assemble Porkbun ACME environment file";
wantedBy = [ "multi-user.target" ];
before = [ "acme-${cfg.matrixDomain}.service" ];
requiredBy = [ "acme-${cfg.matrixDomain}.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
script = ''
set -eu
install -d -m 0755 /run/matrix-cluster
API="$(tr -d '\n' < "${cfg.acme.porkbunApiKeyFile}")"
SEC="$(tr -d '\n' < "${cfg.acme.porkbunSecretApiKeyFile}")"
OUT=/run/matrix-cluster/porkbun.env
umask 077
{
printf 'PORKBUN_API_KEY=%s\n' "$API"
printf 'PORKBUN_SECRET_API_KEY=%s\n' "$SEC"
} > "$OUT"
chmod 0400 "$OUT"
'';
};
})
]);
}
+97
View File
@@ -0,0 +1,97 @@
{
inputs,
self,
flake
}:
{
pkgs,
config,
lib,
...
}: let
system = pkgs.stdenv.hostPlatform.system;
cfg = config.services.postgresql;
extensionFlags = {
pg_cron = false;
pgjwt = false;
pg_net = false;
pg_smtp_client = false;
http = false;
plsh = false;
hemar = false;
};
in {
options = {
services.postgresql = {
lazzyExtensions = lib.mkOption {
type = lib.types.attrsOf lib.types.bool;
default = extensionFlags;
};
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = {};
};
script = lib.mkOption {
type = with lib; types.nullOr types.path;
default = null;
example = lib.literalExpression ''
pkgs.writeText "init-sql-script" '''
alter user postgres with password 'myPassword';
''';'';
description = ''
A file containing SQL statements to execute on stratup or any time you change it.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.postgresql-script= lib.mkIf (cfg.script != null) {
description = "Some postgresql settings";
after = [ "postgresql.service" ];
wants = [ "postgresql.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.dash}/bin/dash ${pkgs.writeText "sql-script" ''
#!/${pkgs.dash}/bin/dash
set -e
alias psql='${cfg.package}/bin/psql -v ON_ERROR_STOP=1 -p "${builtins.toString cfg.port}" -U postgres -d postgres'
${builtins.readFile cfg.script}
''}";
};
path = [ cfg.package ];
wantedBy = [ "multi-user.target" ];
};
systemd.services.postgresql.environment = cfg.environment;
#services.postgresql = {
# settings.shared_preload_libraries =
# lib.concatStringsSep ", "
# (lib.attrNames (
# lib.filterAttrs (n: v: v &&
# n != "http"
# && n != "plsh"
# && n != "pgjwt"
# && n != "pg_smtp_client"
# ) cfg.lazzyExtensions));
# extensions = let
# packages = {
# inherit (cfg.package.pkgs) pg_net pgjwt pg_cron http pg_smtp_client plsh;
# };
# in
# lib.attrValues (
# lib.filterAttrs (n: v: v != null)
# (lib.mapAttrs' (
# name: enabled:
# if enabled
# then lib.nameValuePair name (packages.${name} or (throw "Package ${name} not found in pkgs"))
# else null
# )
# cfg.lazzyExtensions)
# );
#};
};
}
+169
View File
@@ -0,0 +1,169 @@
# INFO(nrv): This is standalone shadowsocks module. Instance-specific is at ./shadowsocks.nix
{
...
}:
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.shadowsocks-rust;
opts = {
server = cfg.localAddress;
server_port = cfg.port;
method = cfg.encryptionMethod;
mode = cfg.mode;
user = "nobody";
fast_open = cfg.fastOpen;
} // optionalAttrs (cfg.plugin != null) {
plugin = cfg.plugin;
plugin_opts = cfg.pluginOpts;
} // optionalAttrs (cfg.password != null) {
password = cfg.password;
} // cfg.extraConfig;
configFile = pkgs.writeText "shadowsocks.json" (builtins.toJSON opts);
in
{
###### interface
options = {
services.shadowsocks-rust = {
enable = mkOption {
type = types.bool;
default = false;
description = lib.mdDoc ''
Whether to run shadowsocks-rust shadowsocks server.
'';
};
localAddress = mkOption {
type = types.str;
default = "0.0.0.0";
description = lib.mdDoc ''
Local addresses to which the server binds.
'';
};
port = mkOption {
type = types.port;
default = 8388;
description = lib.mdDoc ''
Port which the server uses.
'';
};
password = mkOption {
type = types.nullOr types.str;
default = null;
description = lib.mdDoc ''
Password for connecting clients.
'';
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
description = lib.mdDoc ''
Password file with a password for connecting clients.
'';
};
mode = mkOption {
type = types.enum [ "tcp_only" "tcp_and_udp" "udp_only" ];
default = "tcp_and_udp";
description = lib.mdDoc ''
Relay protocols.
'';
};
fastOpen = mkOption {
type = types.bool;
default = true;
description = lib.mdDoc ''
use TCP fast-open
'';
};
encryptionMethod = mkOption {
type = types.str;
default = "chacha20-ietf-poly1305";
description = lib.mdDoc ''
Encryption method. See <https://github.com/shadowsocks/shadowsocks-org/wiki/AEAD-Ciphers>.
'';
};
plugin = mkOption {
type = types.nullOr types.str;
default = null;
example = literalExpression ''"''${pkgs.shadowsocks-v2ray-plugin}/bin/v2ray-plugin"'';
description = lib.mdDoc ''
SIP003 plugin for shadowsocks
'';
};
pluginOpts = mkOption {
type = types.str;
default = "";
example = "server;host=example.com";
description = lib.mdDoc ''
Options to pass to the plugin if one was specified
'';
};
extraConfig = mkOption {
type = types.attrs;
default = {};
example = {
nameserver = "8.8.8.8";
};
description = lib.mdDoc ''
Additional configuration for shadowsocks that is not covered by the
provided options. The provided attrset will be serialized to JSON and
has to contain valid shadowsocks options. Unfortunately most
additional options are undocumented but it's easy to find out what is
available by looking into the source code of
<https://github.com/shadowsocks/shadowsocks-rust/blob/master/src/jconf.c>
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
assertions = singleton
{ assertion = cfg.password == null || cfg.passwordFile == null;
message = "Cannot use both password and passwordFile for shadowsocks-rust";
};
systemd.services.shadowsocks-rust = {
description = "shadowsocks-rust Daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.shadowsocks-rust ]
++ optional (cfg.plugin != null) cfg.plugin
++ optional (cfg.passwordFile != null) pkgs.jq;
serviceConfig.PrivateTmp = true;
script = ''
${optionalString (cfg.passwordFile != null) ''
cat ${configFile} | jq --arg password "$(cat "${cfg.passwordFile}")" '. + { password: $password }' > /run/shadowsocks.json
''}
exec ssserver --config ${if cfg.passwordFile != null then "/run/shadowsocks.json" else configFile}
'';
};
};
}
+28
View File
@@ -0,0 +1,28 @@
{
...
}:
{
pkgs,
config,
...
}:
{
sops.secrets."ss-bfs/password" = {};
services.shadowsocks-rust = {
enable = true;
plugin = "${pkgs.shadowsocks-v2ray-plugin}/bin/v2ray-plugin";
# TODO: setup dnscrypt or a private DNS server for this
# extraConfig = {
# nameserver = "185.12.64.1"; # FIXME: this can vary across instances.
# };
port = 55228;
pluginOpts = "server";
# TODO: setup a TLS certs for this (look: (README.md) https://github.com/shadowsocks/v2ray-plugin/)
#pluginOpts = "server;tls;host=ss.bfs.band";
passwordFile = config.sops.secrets."ss-bfs/password".path;
mode = "tcp_and_udp"; # default
localAddress = "0.0.0.0";
fastOpen = true; # default
encryptionMethod = "chacha20-ietf-poly1305"; # default
};
}
+101
View File
@@ -0,0 +1,101 @@
{
inputs,
flake,
self,
}: {
lib,
pkgs,
modulesPath,
config,
...
}: let
cfg = config.hectic.generic.xray-system;
xrayPort = 10086;
in {
imports = [
self.nixosModules.hectic
inputs.sops-nix.nixosModules.sops
];
options.hectic.generic.xray-system = {
enable = lib.mkEnableOption "generic xray VPN server system configuration";
defaultSopsFile = lib.mkOption {
type = lib.types.path;
description = ''
SOPS-encrypted secrets file used as `sops.defaultSopsFile`.
Must define the `config` and `init-postgresql` secrets.
'';
example = lib.literalExpression "../../../sus/bfs.xray.yaml";
};
};
config = lib.mkIf cfg.enable {
services.xray = {
enable = true;
settingsFile = config.sops.secrets."config".path;
};
users.users.root.openssh.authorizedKeys.keys = [
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOn1KflaIX1RU9YS/qLb0GInmndYxx2vTLZC9OA+eXZl''
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBKPbIJATVyAw7F7vBZbHkCODXFo5gvDyqhuU0gnNUNH''
];
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
"xen_blkfront"
] ++ (if pkgs.stdenv.hostPlatform.system != "aarch64-linux" then [ "vmw_pvscsi" ] else []);
boot.initrd.kernelModules = ["nvme"];
disko.devices = {
disk.vda = {
device = lib.mkDefault "/dev/vda";
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02";
priority = 1;
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
hectic = {
archetype.base.enable = true;
archetype.dev.enable = true;
};
sops = {
gnupg.sshKeyPaths = [ ];
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile = cfg.defaultSopsFile;
secrets."config" = {};
secrets."init-postgresql" = {};
};
networking.firewall = {
enable = true;
allowedTCPPorts = [
xrayPort 8443
80 443 # for acme
];
};
environment.systemPackages = with pkgs; [
xray
];
};
}
+46
View File
@@ -0,0 +1,46 @@
{
inputs,
flake,
self,
}: {
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.archetype.base;
in {
imports = [
inputs.disko.nixosModules.default
inputs.nixos-mailserver.nixosModules.mailserver
];
options.hectic.archetype.base.enable = lib.mkEnableOption "Enable archetupe.dev";
config = lib.mkIf cfg.enable {
hectic = {
program.zsh.enable = lib.mkDefault true;
program.tmux.enable = lib.mkDefault true;
program.nixvim.enable = lib.mkDefault true;
};
users.defaultUserShell = pkgs.zsh;
# Enable flakes and new 'nix' command
nix.settings.experimental-features = "nix-command flakes";
networking.firewall.enable = true;
environment = {
defaultPackages = [];
systemPackages = (with self.packages.${pkgs.stdenv.hostPlatform.system}; [
nvim-pager
]);
variables = {
PAGER = with self.packages.${pkgs.stdenv.hostPlatform.system}; "${nvim-pager}/bin/pager";
};
};
system.stateVersion = "25.05";
};
}
+1
View File
@@ -0,0 +1 @@
{ ... }: {}
+70
View File
@@ -0,0 +1,70 @@
{
inputs,
flake,
self,
}: {
pkgs,
modulesPath,
lib,
config,
...
}: let
cfg = config.hectic.archetype.dev;
in {
# necessary imports:
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
];
options.hectic.archetype.dev.enable = lib.mkEnableOption "Enable archetupe.dev";
config = lib.mkIf cfg.enable {
hectic.archetype.base.enable = true;
services.getty.autologinUser = "root";
virtualisation.vmVariant.virtualisation = {
qemu.options = [
"-nographic"
"-display curses"
"-append console=ttyS0"
"-serial mon:stdio"
"-vga qxl"
];
forwardPorts = [
{
from = "host";
host.port = 40500;
guest.port = 22;
}
];
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
};
};
environment = {
systemPackages =
(with pkgs; [
curl
neovim
yq-go
jq
htop-vim
]);
};
# Adjust zsh prompt for dev archetype: show '#' instead of '%'
home-manager.sharedModules = lib.mkAfter [
{
programs.zsh.initContent = lib.mkAfter ''
PROMPT="# %~ "
'';
}
];
};
}
@@ -0,0 +1,3 @@
{ ... }: { lib, ... }: {
options.hectic.archetype.explosive.enable = lib.mkEnableOption "Enable impermanence usage";
}
+117
View File
@@ -0,0 +1,117 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.hardware.cloudzy;
in {
options.hectic.hardware.cloudzy = {
enable = lib.mkEnableOption "Enable hetzner-cloud hardware configurations";
ipGateway = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
example = "188.243.124.1";
description = ''
'';
};
ipv4 = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
example = "188.243.124.246";
description = ''
'';
};
prefixLength = lib.mkOption {
type = lib.types.int;
example = 24;
description = ''
'';
};
device = lib.mkOption {
type = lib.types.str;
default = "/dev/sda";
example = "/dev/disk/by-uuid/f184a16b-6eca-41cb-b48a-ff37cdce1d79";
description = ''
boot device uuid
if it is null then will use "/dev/sda"
/dev/sda - default hetzner cloud device
!! But can changes on reboot if server have volumes
!! So use IDs
'';
};
networkMatchConfigName = lib.mkOption {
type = lib.types.str;
example = "enp1s0";
description = ''
type of network conection,
on older hetzner servers may be `ens3`
on newer probably `enp1s0`
you can use `networkctl list` on server to know it
'';
};
};
config = lib.mkIf cfg.enable {
boot.loader.systemd-boot.enable = false;
boot.loader.efi.canTouchEfiVariables = false;
boot.loader.grub = {
enable = true;
device = cfg.device;
efiSupport = false;
forceInstall = true;
};
disko.devices.disk.main = {
device = cfg.device;
type = "disk";
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
networking.useDHCP = false;
networking.interfaces."30-wan" = {
matchConfig.Name = cfg.networkMatchConfigName;
ipv4.addresses = [
{ address = cfg.ipv4; prefixLength = cfg.prefixLength; }
];
};
networking.defaultGateway = cfg.ipGateway;
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
"xen_blkfront"
] ++ (if pkgs.stdenv.hostPlatform.system != "aarch64-linux" then [ "vmw_pvscsi" ] else []);
};
}
@@ -0,0 +1,102 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.hardware.geo-hosting;
in {
options.hectic.hardware.geo-hosting = {
enable = lib.mkEnableOption "Enable geo-hosting hardware configurations";
ipv4Gateway = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
example = "188.243.124.1";
description = ''
'';
};
ipv4 = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
example = "188.243.124.246";
description = ''
'';
};
device = lib.mkOption {
type = lib.types.str;
default = "/dev/vda";
example = "/dev/disk/by-uuid/f184a16b-6eca-41cb-b48a-ff37cdce1d79";
description = ''
boot device uuid
if it is null then will use "/dev/vda"
/dev/sva - default geo hosting device
!! But can changes on reboot if server have volumes
!! So use IDs
'';
};
networkMatchConfigName = lib.mkOption {
type = lib.types.strMatching "^(enp1s0|ens3)$";
example = "ens3";
description = ''
type of network conection
you can use `networkctl list` on server to know it
'';
};
};
config = lib.mkIf cfg.enable {
boot.loader.systemd-boot.enable = false;
boot.loader.efi.canTouchEfiVariables = false;
boot.loader.grub = {
enable = true;
device = cfg.device;
efiSupport = false;
forceInstall = true;
};
disko.devices.disk.vda = {
device = cfg.device;
type = "disk";
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
networking.useDHCP = false;
networking.interfaces.${cfg.networkMatchConfigName} = {
ipv4.addresses = [
{ address = cfg.ipv4; prefixLength = 24; }
];
};
networking.defaultGateway = cfg.ipv4Gateway;
networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
};
}
@@ -0,0 +1,133 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.hardware.hetzner-cloud;
in {
options.hectic.hardware.hetzner-cloud = {
enable = lib.mkEnableOption "Enable hetzner-cloud hardware configurations";
#bootParUuid = lib.mkOption {
# type = with lib.types; nullOr oneOf [
# (lib.types.strMatching "^[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}$")
# (lib.types.strMatching "^[0-9a-fA-F-]{36}$")
# ];
# default = null;
# example = "5628-19B6";
# description = ''
# boot partition uuid if it is null
# then will use "/dev/sda15" (default hetzner cloud boot device)
# '';
#};
ipv4 = lib.mkOption {
type = lib.types.strMatching "^([0-9]{1,3}\\.){3}[0-9]{1,3}$";
example = "188.243.124.246";
description = ''
'';
};
ipv6 = lib.mkOption {
type = lib.types.strMatching "^([0-9a-fA-F]{1,4}:){3}[0-9a-fA-F]{1,4}$";
example = "2a01:4f8:1c1a:d883";
description = ''
'';
};
device = lib.mkOption {
type = lib.types.str;
default = "/dev/sda";
example = "/dev/disk/by-uuid/f184a16b-6eca-41cb-b48a-ff37cdce1d79";
description = ''
boot device uuid
if it is null then will use "/dev/sda"
/dev/sda - default hetzner cloud device
!! But can changes on reboot if server have volumes
!! So use IDs
'';
};
networkMatchConfigName = lib.mkOption {
type = lib.types.strMatching "^(enp1s0|ens3)$";
example = "enp1s0";
description = ''
type of network conection,
on older hetzner servers may be `ens3`
on newer probably `enp1s0`
you can use `networkctl list` on server to know it
'';
};
};
config = lib.mkIf cfg.enable (lib.mkMerge
[
{
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
"xen_blkfront"
] ++ (if pkgs.stdenv.hostPlatform.system != "aarch64-linux" then [ "vmw_pvscsi" ] else []);
networking.useDHCP = false;
networking.useNetworkd = true;
systemd.network.enable = true;
systemd.network.networks."30-wan" = {
matchConfig.Name = cfg.networkMatchConfigName;
networkConfig.DHCP = "no";
address = [
"${cfg.ipv4}/32"
"${cfg.ipv6}::/64"
];
routes = [
{ Gateway = "172.31.1.1"; GatewayOnLink = true; }
{ Gateway = "fe80::1"; }
];
};
disko.devices = {
disk = {
main = {
type = "disk";
device = cfg.device;
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02";
priority = 1;
};
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
};
}
(lib.mkIf (pkgs.stdenv.hostPlatform.system == "aarch64-linux") {
boot.initrd.kernelModules = [ "virtio_gpu" ];
boot.kernelParams = [ "console=tty" ];
})
]);
}
@@ -0,0 +1,227 @@
{
inputs,
...
}:
{
lib,
config,
modulesPath,
pkgs,
...
}: let
cfg = config.hectic.hardware.lenovo-ideapad-15arh7;
hasDisko = false;
in {
# FIXME: FUCK
#imports = [
# "${inputs.nixos-hardware}/common/cpu/amd"
# "${inputs.nixos-hardware}/common/cpu/amd/pstate.nix"
# "${inputs.nixos-hardware}/common/gpu/amd"
# "${inputs.nixos-hardware}/common/gpu/nvidia/prime-sync.nix"
# "${inputs.nixos-hardware}/common/pc/laptop"
# "${inputs.nixos-hardware}/common/pc/laptop/ssd"
#];
options.hectic.hardware.lenovo-ideapad-15arh7 = {
enable = lib.mkEnableOption "Enable lenovo-legion hardware configurations";
swapSize = lib.mkOption {
type = lib.types.either (lib.types.enum [ "100%" ]) (lib.types.strMatching "[0-9]+[KMGTP]?");
default = "0";
description = ''
Size of the partition, in sgdisk format.
sets end automatically with the + prefix
can be 100% for the whole remaining disk, will be done last in that case.
'';
};
device = lib.mkOption {
type = lib.types.str;
default = "0";
description = ''
Size of the partition, in sgdisk format.
sets end automatically with the + prefix
can be 100% for the whole remaining disk, will be done last in that case.
'';
};
};
config = lib.mkIf cfg.enable {
/* common */
hardware.nvidia = {
modesetting.enable = true;
prime = {
amdgpuBusId = "PCI:5:0:0";
nvidiaBusId = "PCI:1:0:0";
};
};
environment.systemPackages = with pkgs; [
vulkan-tools
];
/* */
/* boot */
boot.initrd.availableKernelModules = [
"nvme"
"xhci_pci"
"usb_storage"
"usbhid"
"sd_mod"
];
boot.initrd.kernelModules = [ "dm-snapshot" "amdgpu" ];
boot.kernelModules = [ "kvm-amd" ];
boot.extraModulePackages = [ ];
/* */
networking.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
/* cpu */
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
/* gpu */
services.xserver.videoDrivers = [
"nvidia"
#"amdgpu" # NOTE: probably useles with nvidia optimus prime
#"nouveau" # NOTE: open source nvidia
];
hardware.opengl = {
enable = true;
driSupport32Bit = true;
extraPackages = with pkgs; [
vulkan-loader
vulkan-validation-layers
vulkan-extension-layer
amdvlk
];
extraPackages32 = with pkgs; [
pkgsi686Linux.vulkan-loader
pkgsi686Linux.vulkan-validation-layers
pkgsi686Linux.vulkan-extension-layer
driversi686Linux.amdvlk
];
};
#environment.variables.VK_DRIVER_FILES=/run/opengl-driver/share/vulkan/icd.d/nvidia_icd.x86_64.json;
#environment.sessionVariables.VK_DRIVER_FILES = "/run/opengl-driver/share/vulkan/icd.d/nvidia_icd.x86_64.json";
#environment.sessionVariables = rec {
# VK_ICD_FILENAMES =
# "${config.hardware.nvidia.package}/share/vulkan/icd.d/nvidia_icd.x86_64.json";
# #:${config.environment.variables.VK_ICD_FILENAMES or ""}";
#};
hardware.nvidia = {
# Nvidia power management. Experimental, and can cause sleep/suspend to fail.
# Enable this if you have graphical corruption issues or application crashes after waking
# up from sleep. This fixes it by saving the entire VRAM memory to /tmp/ instead
# of just the bare essentials.
powerManagement.enable = false;
# Fine-grained power management. Turns off GPU when not in use.
# Experimental and only works on modern Nvidia GPUs (Turing or newer).
powerManagement.finegrained = false;
# Use the NVidia open source kernel module (not to be confused with the
# independent third-party "nouveau" open source driver).
# Support is limited to the Turing and later architectures. Full list of
# supported GPUs is at:
# https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus
# Only available from driver 515.43.04+
# Currently alpha-quality/buggy, so false is currently the recommended setting.
open = false;
# Enable the Nvidia settings menu,
# accessible via `nvidia-settings`.
nvidiaSettings = true;
# nvidia package overwrive
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
/* */
/* sound */
hardware.pulseaudio.enable = true;
hardware.pulseaudio.support32Bit = true;
/* */
/* disk */
disko.devices = {
disk.main = {
inherit (cfg) device;
type = "disk";
content = {
type = "gpt";
partitions = {
boot = {
name = "boot";
size = "1M";
type = "EF02";
};
esp = {
name = "ESP";
size = "500M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
swap = {
size = cfg.swapSize;
content = {
type = "swap";
resumeDevice = true;
};
};
root = {
name = "root";
size = "100%";
content = {
type = "lvm_pv";
vg = "root_vg";
};
};
};
};
};
lvm_vg = {
root_vg = {
type = "lvm_vg";
lvs = {
root = {
size = "100%FREE";
content = {
type = "btrfs";
extraArgs = ["-f"];
subvolumes = lib.mkMerge [
{
"/root" = {
mountpoint = "/";
};
"/nix" = {
mountOptions = ["subvol=nix" "noatime"];
mountpoint = "/nix";
};
}
(if config.hectic.archetype.explosive.enable then {
"/persist" = {
mountOptions = ["subvol=persist" "noatime"];
mountpoint = "/persist";
};
} else {})
];
};
};
};
};
};
};
};
}
+70
View File
@@ -0,0 +1,70 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.hardware.zombro;
in {
options.hectic.hardware.zombro = {
enable = lib.mkEnableOption "Enable zombro hardware configurations";
device = lib.mkOption {
type = lib.types.str;
default = "/dev/vda";
example = "/dev/disk/by-uuid/f184a16b-6eca-41cb-b48a-ff37cdce1d79";
description = ''
boot device uuid
if it is null then will use "/dev/vda"
/dev/vda - default zombro device
!! But can changes on reboot if server have volumes
!! So use IDs
'';
};
};
config = lib.mkIf cfg.enable (lib.mkMerge
[
{
boot.loader.grub.device = cfg.device;
boot.initrd.availableKernelModules = [
"ata_piix"
"uhci_hcd"
"xen_blkfront"
] ++ (if pkgs.stdenv.hostPlatform.system != "aarch64-linux" then [ "vmw_pvscsi" ] else []);
boot.initrd.kernelModules = ["nvme"];
disko.devices = {
disk.master = {
device = cfg.device;
content = {
type = "gpt";
partitions = {
boot = {
size = "1M";
type = "EF02";
priority = 1;
};
root = {
size = "100%";
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
};
};
};
};
};
};
}
(lib.mkIf (pkgs.stdenv.hostPlatform.system == "aarch64-linux") {
boot.initrd.kernelModules = [ "virtio_gpu" ];
boot.kernelParams = [ "console=tty" ];
})
]);
}
+197
View File
@@ -0,0 +1,197 @@
{
inputs,
flake,
self,
}: {
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.program.nixvim;
in {
imports = [
inputs.nixvim.nixosModules.nixvim
];
options.hectic.program.nixvim.enable = lib.mkEnableOption "Enable hectic nixvim config";
config = lib.mkIf cfg.enable {
programs.nixvim = {
enable = true;
extraPackages = with pkgs; [ gcc ];
colorschemes.kanagawa = {
enable = true;
settings.colors.theme.all = {};
};
opts = {
spell = true;
spelllang = [ "en" "ru" "it" ];
tabstop = 2;
shiftwidth = 2;
softtabstop = 2;
expandtab = true;
};
extraFiles = {
"spell/ru.utf-8.spl".source = pkgs.fetchurl {
url = "https://ftp.nluug.nl/vim/runtime/spell/ru.utf-8.spl";
sha256 = "sha256-6y0714ogILMLzAp8/r2s6/t6QnWBEU9muIpXeubaxU0=";
};
"spell/ru.utf-8.sug".source = pkgs.fetchurl {
url = "https://ftp.nluug.nl/vim/runtime/spell/ru.utf-8.sug";
sha256 = "sha256-6r2GForYXVv7gGiAjPeYK6sDdK/CmctJ7MidcWFvOTs=";
};
"spell/it.utf-8.spl".source = pkgs.fetchurl {
url = "https://ftp.nluug.nl/vim/runtime/spell/it.utf-8.spl";
hash = "sha256-2AczkD6DbVN5DAq4wcLyn2Y8oqd67ns4Guprh2KudBM=";
};
"spell/it.utf-8.sug".source = pkgs.fetchurl {
url = "https://ftp.nluug.nl/vim/runtime/spell/it.utf-8.sug";
hash = "sha256-4LsXYaeScJJrdaj69PTQ2EDVWis0UY/Y5RKSfCckzko=";
};
"ftdetect/hemar.vim".text = ''
au BufRead,BufNewFile *.hemar setfiletype hemar
'';
"queries/hemar/highlights.scm".text = ''
(interpolation) @keyword
(for "for" @keyword)
(for "in" @keyword)
(done "done" @keyword)
(path) @field
(string) @string
(text) @text
(for
"{[" @punctuation.bracket
"]}" @punctuation.bracket)
(done
"{[" @punctuation.bracket
"]}" @punctuation.bracket)
(interpolation
"{[" @punctuation.bracket
"]}" @punctuation.bracket)
'';
};
extraConfigLuaPre = /* lua */ ''
-- map leader
vim.api.nvim_set_keymap("", "<Space>", "<Nop>", { noremap = true, silent = true })
vim.g.mapleader = ' '
-- render markdown
require('render-markdown').setup({
link = {
enabled = true,
render_modes = false,
},
})
-- nowrap for *.nowrap.* markdown files
vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
callback = function()
if vim.fn.expand("%:t"):find("%.nowrap%.") then vim.opt_local.wrap = false end
end,
})
-- toggle conceallevel
vim.keymap.set("n", "<leader>tc", ":setlocal <C-R>=&conceallevel ? 'conceallevel=0' : 'conceallevel=2'<CR><CR>", { desc = "[T]oggle [C]onceallevel" })
-- tree-sitter: register hemar parser
local parser_config = require("nvim-treesitter.parsers").get_parser_configs()
parser_config.hemar = {
install_info = {
url = "https://github.com/hectic-lab/util.nix",
files = { "package/hemar/grammar/tree-sitter/src/parser.c" },
generate_requires_npm = false,
requires_generate_from_grammar = false,
},
filetype = "hemar",
}
'';
extraConfigLuaPost = /* lua */ ''
vim.cmd [[
hi Normal guibg=none ctermbg=none
hi NonText guibg=none ctermbg=none
]]
'';
keymaps = [
{ mode = "n"; key = "<leader>o"; options.silent = true; action = "<cmd>Oil<CR>"; }
{ mode = "n"; key = "<leader>dd"; action = "<cmd>lua vim.diagnostic.open_float()<CR>"; }
{ mode = "n"; key = "<leader>dn"; action = "<cmd>lua vim.diagnostic.goto_next()<CR>"; }
{ mode = "n"; key = "<leader>dp"; action = "<cmd>lua vim.diagnostic.goto_prev()<CR>"; }
];
extraPlugins = with pkgs.vimPlugins; [
nvim-treesitter-parsers.templ
vim-shellcheck
vim-grammarous
];
plugins = {
render-markdown.enable = true;
fidget.enable = true;
oil.enable = true;
treesitter = {
enable = true;
settings = {
ensure_installed = [ "hemar" ];
highlight.enable = true;
};
};
lsp = {
enable = true;
keymaps.lspBuf = {
"<leader>lh" = "hover";
"<leader>ld" = "definition";
"<leader>lD" = "references";
"<leader>lr" = "rename";
"<leader>li" = "implementation";
"<leader>lt" = "type_definition";
"<leader>lf" = "format";
"<leader>la" = "code_action";
};
servers = {
rust_analyzer = {
enable = true;
installRustc = false;
installCargo = false;
};
nixd = {
enable = true;
};
nil_ls = {
enable = true;
extraOptions.formatting.command = [ "nixpkgs-fmt" ];
};
clangd.enable = true;
ts_ls.enable = true;
gopls.enable = true;
templ.enable = true;
bashls.enable = true;
kotlin_language_server.enable = true;
metals = {
enable = true;
cmd = [ "metals" ];
};
sqls.enable = true;
java_language_server.enable = true;
pyright.enable = true;
};
};
};
};
};
}
+62
View File
@@ -0,0 +1,62 @@
{
inputs,
flake,
self,
}: {
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.program.tmux;
in {
imports = [
inputs.home-manager.nixosModules.home-manager
];
options.hectic.program.tmux.enable = lib.mkEnableOption "Enable hectic tmux config";
config = lib.mkIf cfg.enable {
programs.tmux.enable = true;
programs.tmux.terminal = lib.mkOverride 50 "tmux-256color";
# alias depends on newSession = true (auto-creates session on attach)
programs.zsh.shellAliases.tmux = "tmux a";
programs.bash.shellAliases.tmux = "tmux a";
home-manager.sharedModules = [
{
programs.tmux = {
enable = true;
plugins = with pkgs.tmuxPlugins; [ resurrect continuum ];
keyMode = "vi";
escapeTime = 500;
historyLimit = 50000;
newSession = true;
extraConfig = ''
# resurrect
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-capture-pane-contents 'on'
resurrect_dir="$HOME/.tmux/resurrect"
set -g @resurrect-dir $resurrect_dir
set -g @resurrect-hook-post-save-all 'target=$(readlink -f $resurrect_dir/last); sed "s| --cmd .*-vim-pack-dir||g; s|/etc/profiles/per-user/$USER/bin/||g; s|/home/$USER/.nix-profile/bin/||g" $target | sponge $target'
# continuum
set -g @continuum-restore 'on'
set -g @continuum-boot 'on'
set -g @continuum-save-interval '10'
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key O select-pane -t :.-
'';
};
}
];
home-manager.users.root.home.stateVersion = lib.mkDefault "25.05";
};
}
+55
View File
@@ -0,0 +1,55 @@
{
inputs,
flake,
self,
}: {
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.program.zsh;
in {
imports = [
inputs.home-manager.nixosModules.home-manager
];
options.hectic.program.zsh.enable = lib.mkEnableOption "Enable hectic zsh config";
config = lib.mkIf cfg.enable {
# system-level zsh must be on for home-manager zsh to work
programs.zsh.enable = true;
users.defaultUserShell = pkgs.zsh;
# Share the same zsh config with all home-manager users
home-manager.sharedModules = [
{
programs.zsh = {
enable = true;
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
history = {
size = 10000;
path = "$HOME/.zsh/.zsh_history";
};
oh-my-zsh = {
enable = true;
theme = "terminalparty";
};
shellAliases = self.lib.sharedShellAliases;
initContent = ''
set -ovi
'';
};
}
];
# Still define root for stateVersion; config comes from sharedModules
home-manager.users.root.home.stateVersion = lib.mkDefault "25.05";
};
}
+11
View File
@@ -0,0 +1,11 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let in { }
+132
View File
@@ -0,0 +1,132 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
legacyCfg = config.hectic.services.matrix;
clusterCfg = config.hectic.generic.matrix-cluster;
clusterSynapseEnabled =
clusterCfg.enable
&& (if clusterCfg.overrideEnableSynapse != null then clusterCfg.overrideEnableSynapse else clusterCfg.role == "primary");
enabled = legacyCfg.enable || clusterSynapseEnabled;
matrixDomain = if legacyCfg.enable then legacyCfg.matrixDomain else clusterCfg.matrixDomain;
in {
config = lib.mkIf enabled (let
keyFile = "/run/livekit.key";
in {
services.livekit = {
enable = true;
openFirewall = true;
settings.room.auto_create = false;
inherit keyFile;
};
services.lk-jwt-service = {
enable = true;
livekitUrl = "wss://${matrixDomain}/livekit/sfu";
inherit keyFile;
};
systemd.services.livekit-key = {
before = [ "lk-jwt-service.service" "livekit.service" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [ livekit coreutils gawk ];
script = ''
echo "Key missing, generating key"
echo "lk-jwt-service: $(livekit-server generate-keys | tail -1 | awk '{print $3}')" > "${keyFile}"
'';
serviceConfig.Type = "oneshot";
unitConfig.ConditionPathExists = "!${keyFile}";
};
systemd.services.lk-jwt-service.environment.LIVEKIT_FULL_ACCESS_HOMESERVERS =
matrixDomain;
services.nginx = {
enable = true;
virtualHosts.${matrixDomain} = {
forceSSL = true;
enableACME = true;
locations."=/.well-known/matrix/client" = {
extraConfig = ''
default_type application/json;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization";
'';
return = ''200 '{
"m.homeserver": {
"base_url": "https://${matrixDomain}"
},
"m.identity_server": {
"base_url": "https://vector.im"
},
"org.matrix.msc3575.proxy": {
"url": "https://${matrixDomain}"
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://${matrixDomain}/livekit/jwt"
}
]
}' '';
};
locations."= /livekit/jwt" = {
priority = 500;
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/";
};
locations."^~ /livekit/jwt/" = {
priority = 400;
proxyPass = "http://[::1]:${toString config.services.lk-jwt-service.port}/";
};
locations."= /livekit/sfu" = {
priority = 500;
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/";
proxyWebsockets = true;
extraConfig = ''
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
proxy_set_header Accept-Encoding gzip;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
'';
};
locations."^~ /livekit/sfu/" = {
priority = 400;
proxyPass = "http://[::1]:${toString config.services.livekit.settings.port}/";
proxyWebsockets = true;
extraConfig = ''
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
proxy_set_header Accept-Encoding gzip;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
'';
};
};
};
networking.firewall = {
enable = true;
allowedTCPPorts = [
8080
7880
7881
];
};
});
}
+53
View File
@@ -0,0 +1,53 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
legacyCfg = config.hectic.services.matrix;
clusterCfg = config.hectic.generic.matrix-cluster;
clusterSynapseEnabled =
clusterCfg.enable
&& (if clusterCfg.overrideEnableSynapse != null then clusterCfg.overrideEnableSynapse else clusterCfg.role == "primary");
enabled = legacyCfg.enable || clusterSynapseEnabled;
matrixDomain = if legacyCfg.enable then legacyCfg.matrixDomain else clusterCfg.matrixDomain;
jitsiPreferredDomain =
if legacyCfg.enable && config.hectic.services.jitsi.enable
then config.hectic.services.jitsi.hostName
else clusterCfg.jitsi.preferredDomain;
in {
config = lib.mkIf enabled {
services.nginx.virtualHosts."element.${matrixDomain}" = {
enableACME = true;
forceSSL = true;
locations."= /config.element.${matrixDomain}.json".return = "302 /config.json";
root = pkgs.element-web.override {
conf = {
default_server_config = {
"m.homeserver".base_url = "https://${matrixDomain}";
"m.homeserver".server_name = matrixDomain;
"m.identity_server".base_url = "https://vector.im";
};
room_directory.servers = [
matrixDomain
];
jitsi = lib.optionalAttrs (jitsiPreferredDomain != null) {
preferred_domain = jitsiPreferredDomain;
};
default_theme = "dark";
show_labs_settings = true;
};
};
};
};
}
+97
View File
@@ -0,0 +1,97 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.services.jitsi;
in {
options = {
hectic.services.jitsi = {
enable = lib.mkEnableOption "Jitsi Meet video conferencing with Prosody XMPP backend";
hostName = lib.mkOption {
type = lib.types.str;
description = ''
FQDN for the Jitsi Meet instance (e.g. "meet.example.org").
Prosody VirtualHosts, nginx, and ACME certs are derived from this.
'';
};
secureDomain = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Require authentication to create rooms. Guests can still join
existing rooms anonymously.
'';
};
lockdown = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Restrict Prosody to localhost only (no S2S federation, c2s
only on 127.0.0.1). Set to false when running alongside a
general-purpose XMPP server (hectic.services.xmpp).
'';
};
videobridgePasswordFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a file containing the Jitsi Videobridge XMPP password.
If null, a random password is auto-generated.
'';
};
};
};
config = lib.mkIf cfg.enable {
services.jitsi-meet = {
enable = true;
hostName = cfg.hostName;
prosody = {
enable = true;
lockdown = cfg.lockdown;
};
nginx.enable = true;
videobridge = {
enable = true;
} // lib.optionalAttrs (cfg.videobridgePasswordFile != null) {
passwordFile = cfg.videobridgePasswordFile;
};
jicofo.enable = true;
secureDomain = lib.mkIf cfg.secureDomain {
enable = true;
};
};
services.jitsi-videobridge.openFirewall = true;
services.nginx.virtualHosts.${cfg.hostName} = {
enableACME = true;
forceSSL = true;
};
security.acme = {
acceptTerms = true;
defaults = {
email = lib.mkDefault "hectic.yukkop.it@gmail.com";
enableDebugLogs = lib.mkDefault true;
};
};
networking.firewall = {
allowedTCPPorts = [
80 443 # HTTP/HTTPS (nginx + ACME)
5222 # XMPP c2s (if not locked down)
];
};
};
}
@@ -0,0 +1,65 @@
{
inputs,
flake,
self,
}:
{
lib,
config,
...
}: let
cfg = config.services.mailserver;
transformLoginAccounts = domain: input:
builtins.listToAttrs (map (key: {
name = key + "@" + domain;
value = input.${key};
}) (builtins.attrNames input));
in {
options = {
services.mailserver.enable = lib.mkEnableOption "Mail server";
services.mailserver.domain = lib.mkOption {
type = lib.types.str;
description = "The domain name of the mail server";
};
services.mailserver.loginAccounts = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
hashedPassword = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
hashedPasswordFile = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Full path to a file containing the hashed password suitable
for use with `chpasswd -e`.
'';
};
};
});
default = {};
description = "Login accounts for the mail server";
};
};
config = lib.mkIf cfg.enable {
mailserver = {
enable = true;
fqdn = "mail." + cfg.domain;
domains = [ cfg.domain ];
loginAccounts = transformLoginAccounts cfg.domain cfg.loginAccounts;
certificateScheme = "acme-nginx";
};
# NOTE(yukkop): avoid Gmail rejection due to missing IPv6 PTR records
services.postfix.settings.main.inet_protocols = lib.mkDefault "ipv4";
security.acme.acceptTerms = true;
security.acme.defaults.email = "security@" + cfg.domain;
};
}
+508
View File
@@ -0,0 +1,508 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.services.matrix;
s3Cfg = cfg.objectStorage.s3;
matrixUsers = builtins.attrNames cfg.users;
s3Plugin = pkgs.matrix-synapse-plugins.matrix-synapse-s3-storage-provider;
s3ConfigDir = "/run/matrix-synapse";
s3ConfigFile = "${s3ConfigDir}/s3-media-storage.yaml";
mkUserRegistration = name: let
user = cfg.users.${name};
adminFlag = if user.admin then "--admin" else "--no-admin";
in ''
if [ ! -r "${user.passwordFile}" ]; then
printf 'Missing Matrix password file for %s: %s\n' '${name}' '${user.passwordFile}' >&2
exit 1
fi
${pkgs.matrix-synapse}/bin/register_new_matrix_user \
-u '${name}' \
-p "$(tr -d '\n' < "${user.passwordFile}")" \
-k "$REGISTRATION_SHARED_SECRET" \
${adminFlag} \
http://127.0.0.1:8008 || true
'';
mkS3Config = ''
if [ ! -r "${s3Cfg.credentialsFile}" ]; then
printf 'Missing Matrix object storage credentials file: %s\n' '${s3Cfg.credentialsFile}' >&2
exit 1
fi
. "${s3Cfg.credentialsFile}"
if [ -z "$ACCESS_KEY_ID" ] || [ -z "$SECRET_ACCESS_KEY" ]; then
printf 'ACCESS_KEY_ID or SECRET_ACCESS_KEY missing in %s\n' '${s3Cfg.credentialsFile}' >&2
exit 1
fi
mkdir -p "${s3ConfigDir}"
cat > "${s3ConfigFile}" <<EOF
media_storage_providers:
- module: s3_storage_provider.S3StorageProviderBackend
store_local: ${lib.boolToString s3Cfg.storeLocal}
store_remote: ${lib.boolToString s3Cfg.storeRemote}
store_synchronous: ${lib.boolToString s3Cfg.storeSynchronous}
config:
bucket: ${s3Cfg.bucket}
endpoint_url: ${s3Cfg.endpointUrl}
region_name: ${s3Cfg.regionName}
prefix: "${s3Cfg.prefix}"
storage_class: "${s3Cfg.storageClass}"
threadpool_size: ${toString s3Cfg.threadpoolSize}
access_key_id: $ACCESS_KEY_ID
secret_access_key: $SECRET_ACCESS_KEY
EOF
chown matrix-synapse:matrix-synapse "${s3ConfigFile}"
chmod 0400 "${s3ConfigFile}"
'';
mkS3SyncScript = ''
${s3Plugin}/bin/s3_media_upload write
${s3Plugin}/bin/s3_media_upload upload "${s3Cfg.mediaStorePath}" "${s3Cfg.bucket}" \
--endpoint-url "${s3Cfg.endpointUrl}" \
--storage-class "${s3Cfg.storageClass}" \
--prefix "${s3Cfg.prefix}" \
${lib.optionalString s3Cfg.sync.deleteLocalAfterUpload "--delete"}
cat > /tmp/synapse-merge-config.py << 'PYEOF'
import yaml
with open("${config.services.matrix-synapse.configFile}") as f:
config = yaml.safe_load(f)
with open("${cfg.secretsFile}") as f:
secrets = yaml.safe_load(f)
config.update(secrets)
config.setdefault("database", {}).setdefault("args", {})
config["database"]["args"].setdefault("password", "")
config["database"]["args"].setdefault("host", "/run/postgresql")
config["database"]["args"].setdefault("port", 5432)
with open("/tmp/synapse-combined-config.yaml", "w") as f:
yaml.dump(config, f, default_flow_style=False)
PYEOF
${pkgs.python3.withPackages (ps: [ps.pyyaml])}/bin/python3 /tmp/synapse-merge-config.py
${s3Plugin}/bin/s3_media_upload update-db --homeserver-config-path /tmp/synapse-combined-config.yaml 0s
rm -f /tmp/synapse-combined-config.yaml
${s3Plugin}/bin/s3_media_upload check-deleted "${s3Cfg.mediaStorePath}"
'';
in {
options = {
hectic.services.matrix = {
enable = lib.mkEnableOption "Matrix Synapse homeserver with PostgreSQL and nginx";
secretsFile = lib.mkOption {
type = lib.types.path;
description = ''
path to env file with matrix secrets
content:
registration_shared_secret:
macroon_secret_key
form_secret
'';
};
postgresql = {
port = lib.mkOption {
type = lib.types.port;
default = 5432;
description = ''
postgres port
'';
};
initialEnvFile = lib.mkOption {
type = lib.types.path;
description = ''
path to env file with postgresql initial secrets
content:
POSTGRESQL_PASSWORD=
'';
};
};
matrixDomain = lib.mkOption {
type = lib.types.str;
description = ''
domain to matrix
'';
};
maxUploadSize = lib.mkOption {
type = lib.types.str;
default = "2G";
description = ''
Maximum file upload size accepted by Synapse and nginx.
'';
};
enableRegistration = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Allow open user registration on the homeserver.
'';
};
objectStorage.s3 = {
enable = lib.mkEnableOption "S3-compatible object storage for Matrix media";
bucket = lib.mkOption {
type = lib.types.str;
description = ''
Bucket name used for Matrix media objects.
'';
};
regionName = lib.mkOption {
type = lib.types.str;
description = ''
Region name passed to the Synapse S3 storage provider.
'';
};
endpointUrl = lib.mkOption {
type = lib.types.str;
description = ''
S3-compatible endpoint URL.
'';
};
credentialsFile = lib.mkOption {
type = lib.types.path;
description = ''
Path to an env-style file containing:
ACCESS_KEY_ID=
SECRET_ACCESS_KEY=
'';
};
mediaStorePath = lib.mkOption {
type = lib.types.str;
default = "/var/lib/matrix-synapse/media_store";
description = ''
Local Synapse media store path used before upload to object storage.
'';
};
prefix = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Optional object key prefix inside the bucket.
'';
};
storageClass = lib.mkOption {
type = lib.types.str;
default = "STANDARD";
description = ''
Storage class passed to the upload tool.
'';
};
threadpoolSize = lib.mkOption {
type = lib.types.int;
default = 40;
description = ''
Worker pool size for the Synapse S3 storage provider.
'';
};
storeLocal = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Mirror local uploads to object storage.
'';
};
storeRemote = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Mirror remotely-fetched media to object storage.
'';
};
storeSynchronous = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Wait for object storage upload before completing the client request.
'';
};
sync = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Periodically migrate older local media to object storage.
'';
};
olderThan = lib.mkOption {
type = lib.types.str;
default = "1d";
description = ''
Age threshold passed to `s3_media_upload update`.
'';
};
onCalendar = lib.mkOption {
type = lib.types.str;
default = "hourly";
description = ''
systemd timer schedule for media sync.
'';
};
deleteLocalAfterUpload = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Remove local media after successful object storage upload.
'';
};
};
};
users = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
options = {
passwordFile = lib.mkOption {
type = lib.types.str;
description = ''
Full path to a file containing the Matrix user's password.
'';
};
admin = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to create the Matrix user as an admin.
'';
};
};
});
default = {};
description = ''
Declarative Matrix users to provision after Synapse starts.
'';
};
};
};
config = lib.mkMerge [
(lib.mkIf cfg.enable {
services.matrix-synapse = {
enable = true;
plugins = lib.optional s3Cfg.enable s3Plugin;
extraConfigFiles = [
cfg.secretsFile
] ++ lib.optional s3Cfg.enable s3ConfigFile;
settings = {
server_name = cfg.matrixDomain;
public_baseurl = "https://${cfg.matrixDomain}";
max_upload_size = cfg.maxUploadSize;
media_store_path = lib.mkIf s3Cfg.enable s3Cfg.mediaStorePath;
experimental_features = {
msc3266_enabled = true;
msc4140_enabled = true;
msc4143_enabled = true;
msc4222_enabled = true;
};
matrix_rtc = {
transports = [
{
type = "livekit";
livekit_service_url = "https://${cfg.matrixDomain}/livekit/jwt";
}
];
};
listeners = [
{
port = 8008;
bind_addresses = [ "0.0.0.0" ];
type = "http";
tls = false;
resources = [
{
names = [
"client"
# Ability speak between different matrix servers and get
# global id, requires .well-known
"federation"
"openid"
];
compress = false;
}
];
}
];
enable_registration = cfg.enableRegistration;
enable_registration_without_verification = cfg.enableRegistration;
};
};
environment.systemPackages = [
pkgs.matrix-synapse
];
services.postgresql = {
enable = true;
package = pkgs.postgresql_17;
initdbArgs = [
"--locale=C"
"--encoding=UTF8"
];
enableTCPIP = true;
settings.port = cfg.postgresql.port;
authentication = builtins.concatStringsSep "\n" [
"local all all trust"
"host sameuser all 127.0.0.1/32 scram-sha-256"
"host sameuser all ::1/128 scram-sha-256"
"host all all ::1/128 scram-sha-256"
"host all all 0.0.0.0/0 scram-sha-256"
"host replication postgres 127.0.0.1/32 scram-sha-256"
"host replication postgres ::1/128 scram-sha-256"
];
settings = {
wal_level = "replica";
max_wal_senders = 10;
};
ensureUsers = [
{
name = "matrix-synapse";
ensureClauses.login = true;
ensureDBOwnership = true;
}
];
ensureDatabases = [
"matrix-synapse"
];
initialScript = pkgs.writeText "init-sql-script" ''
-- setup password from env/sops
DO $$#!${pkgs.dash}/bin/dash
set -e
. ${cfg.postgresql.initialEnvFile}
psql -Atc "ALTER USER postgres WITH PASSWORD '$POSTGRESQL_PASSWORD'";
$$ LANGUAGE plsh;
CREATE ROLE myuser LOGIN PASSWORD 'matrix-synapse';
'';
};
services.nginx = {
enable = true;
virtualHosts.${cfg.matrixDomain} = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://127.0.0.1:8008";
extraConfig = ''
client_max_body_size ${cfg.maxUploadSize};
'';
};
locations."=/.well-known/matrix/server" = {
extraConfig = ''
default_type application/json;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization";
'';
return = "200 '{\"m.server\": \"${cfg.matrixDomain}:443\"}'";
};
};
};
security.acme = {
acceptTerms = true;
defaults = {
email = "hectic.yukkop.it@gmail.com";
enableDebugLogs = true;
};
};
systemd.services.matrix-synapse-users = lib.mkIf (matrixUsers != []) {
description = "Provision Matrix Synapse users";
wantedBy = [ "multi-user.target" ];
after = [ config.services.matrix-synapse.serviceUnit ];
requires = [ config.services.matrix-synapse.serviceUnit ];
path = with pkgs; [ curl coreutils gawk ];
serviceConfig = {
Type = "oneshot";
User = "matrix-synapse";
};
script = ''
until curl -sf http://127.0.0.1:8008/_matrix/client/versions >/dev/null; do
sleep 2
done
REGISTRATION_SHARED_SECRET="$(awk -F': *' '$1 == "registration_shared_secret" { print $2; exit }' "${cfg.secretsFile}")"
if [ -z "$REGISTRATION_SHARED_SECRET" ]; then
printf 'registration_shared_secret not found in %s\n' '${cfg.secretsFile}' >&2
exit 1
fi
${builtins.concatStringsSep "\n" (map mkUserRegistration matrixUsers)}
'';
};
})
(lib.mkIf (cfg.enable && s3Cfg.enable) {
systemd.services.matrix-synapse-s3-config = {
description = "Generate Synapse S3 media storage config";
before = [ config.services.matrix-synapse.serviceUnit ];
requiredBy = [ config.services.matrix-synapse.serviceUnit ];
serviceConfig.Type = "oneshot";
script = mkS3Config;
};
systemd.services.matrix-synapse-s3-media-sync = lib.mkIf s3Cfg.sync.enable {
description = "Sync Matrix media to S3-compatible object storage";
after = [ config.services.matrix-synapse.serviceUnit ];
wants = [ config.services.matrix-synapse.serviceUnit ];
serviceConfig = {
Type = "oneshot";
User = "matrix-synapse";
WorkingDirectory = "/var/lib/matrix-synapse";
};
script = mkS3SyncScript;
};
systemd.timers.matrix-synapse-s3-media-sync = lib.mkIf s3Cfg.sync.enable {
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = s3Cfg.sync.onCalendar;
};
})
];
}
@@ -0,0 +1,99 @@
{
inputs,
flake,
self,
}: {
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.services.media-browser;
mediaBrowserApp = pkgs.hectic.media-browser;
in {
options.hectic.services.media-browser = {
enable = lib.mkEnableOption "Matrix media browser web app";
port = lib.mkOption {
type = lib.types.port;
default = 3000;
description = "Port to bind the media browser web server.";
};
mediaStorePath = lib.mkOption {
type = lib.types.str;
default = "/var/lib/matrix-synapse/media_store";
description = "Path to Synapse media store.";
};
s3CredentialsFile = lib.mkOption {
type = lib.types.path;
description = "Path to S3 credentials file (ACCESS_KEY_ID=..., SECRET_ACCESS_KEY=...).";
};
s3Bucket = lib.mkOption {
type = lib.types.str;
description = "S3 bucket name.";
};
s3Endpoint = lib.mkOption {
type = lib.types.str;
description = "S3 endpoint URL.";
};
s3Region = lib.mkOption {
type = lib.types.str;
default = "hel1";
description = "S3 region name.";
};
s3Prefix = lib.mkOption {
type = lib.types.str;
default = "";
description = "S3 object key prefix.";
};
dbName = lib.mkOption {
type = lib.types.str;
default = "matrix-synapse";
description = "PostgreSQL database name.";
};
dbUser = lib.mkOption {
type = lib.types.str;
default = "matrix-synapse";
description = "PostgreSQL database user.";
};
};
config = lib.mkIf cfg.enable {
systemd.services.media-browser = {
description = "Matrix Media Browser";
after = [ "network.target" "postgresql.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
User = "matrix-synapse";
Group = "matrix-synapse";
ExecStart = "${mediaBrowserApp}/bin/media-browser-wrapped";
Restart = "on-failure";
RestartSec = 5;
};
environment = {
FLASK_ENV = "production";
PORT = toString cfg.port;
MEDIA_STORE_PATH = cfg.mediaStorePath;
S3_BUCKET = cfg.s3Bucket;
S3_ENDPOINT = cfg.s3Endpoint;
S3_REGION = cfg.s3Region;
S3_PREFIX = cfg.s3Prefix;
DB_NAME = cfg.dbName;
DB_USER = cfg.dbUser;
DB_HOST = "/run/postgresql";
DB_PORT = "5432";
};
serviceConfig.EnvironmentFile = cfg.s3CredentialsFile;
};
};
}
+187
View File
@@ -0,0 +1,187 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
system = pkgs.stdenv.hostPlatform.system;
cfg = config.hectic.services."sentinèlla";
probePort = 5988;
peersSrv = "_sentinella._tcp.hectic-lab.com";
in {
options = {
hectic.services."sentinèlla" = {
probe = {
enable = lib.mkEnableOption "sentinèlla probe HTTP server exposing this node's health";
urls = lib.mkOption {
type = with lib.types; listOf str;
default = [];
description = "URLs the probe health-checks on GET /status.";
};
volumes = lib.mkOption {
type = with lib.types; listOf str;
default = [];
description = "Mount points reported on GET /disk. Empty means all volumes.";
};
authFile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
example = "config.sops.secrets.\"sentinella-probe-auth\".path";
description = "Path to a file with lines of the form user:pass for Basic Auth.";
};
environmentFile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = ''
Optional environment file for secrets. Supported variables:
PORT=
URLS=
VOLUMES=
AUTH_FILE=
'';
};
};
watcher = {
enable = lib.mkEnableOption "sentinèlla watcher polls peers discovered via DNS and sends Telegram alerts";
self = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
example = "1.2.3.4";
description = ''
Override the auto-detected local IP. When null (default) the watcher
uses hostname -I to find all local IPs and excludes them from the
peer list automatically. Set this only if the node is behind NAT or
has a floating IP that hostname -I does not report correctly.
'';
};
peersScheme = lib.mkOption {
type = lib.types.str;
default = "http";
description = "URL scheme used when connecting to peers (http or https).";
};
pollingIntervalSec = lib.mkOption {
type = lib.types.int;
default = 3;
description = "Seconds between polling rounds.";
};
tgToken = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "Telegram bot token. Prefer environmentFile for secrets.";
};
tgChatId = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "Telegram chat ID. Prefer environmentFile for secrets.";
};
environmentFile = lib.mkOption {
type = with lib.types; nullOr path;
default = config.sops.secrets."sentinèlla/watcher/environment".path;
defaultText = lib.literalExpression
"config.sops.secrets.\"sentinèlla/watcher/environment\".path";
example = "config.sops.secrets.\"sentinella-watcher-env\".path";
description = ''
Environment file for secrets. Defaults to the auto-declared SOPS
secret sentinèlla/watcher/environment (resolved from
sus/sentinella-default.yaml in the flake). Override the sopsFile
via sops.secrets."sentinèlla/watcher/environment".sopsFile if you
need a host-specific file instead.
Supported variables:
TG_TOKEN=
TG_CHAT_ID=
PEERS_TOKEN= # Basic Auth token sent to all peers
SELF=
PEERS_SRV=
'';
};
};
};
};
config = lib.mkMerge [
(lib.mkIf cfg.probe.enable {
networking.firewall = {
enable = true;
allowedTCPPorts = [
probePort
];
};
systemd.services."sentinella-probe" = {
description = "sentinèlla probe node health HTTP server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = lib.mkMerge [
{
Type = "simple";
ExecStart = "${self.packages.${system}."sentinèlla"}/bin/probe";
Restart = "always";
RestartSec = "5s";
TimeoutStopSec = "30s";
KillSignal = "SIGTERM";
KillMode = "mixed";
RemainAfterExit = false;
StandardOutput = "journal";
StandardError = "journal";
Environment = lib.filter (s: s != "") [
"PORT=${builtins.toString probePort}"
(lib.optionalString (cfg.probe.urls != []) "URLS=${lib.concatStringsSep " " cfg.probe.urls}")
(lib.optionalString (cfg.probe.volumes != []) "VOLUMES=${lib.concatStringsSep " " cfg.probe.volumes}")
(lib.optionalString (cfg.probe.authFile != null) "AUTH_FILE=${cfg.probe.authFile}")
];
}
(lib.mkIf (cfg.probe.environmentFile != null) {
EnvironmentFile = cfg.probe.environmentFile;
})
];
};
})
(lib.mkIf cfg.watcher.enable {
sops.secrets."sentinèlla/watcher/environment" = lib.mkDefault {
sopsFile = "${flake}/sus/sentinella-default.yaml";
};
systemd.services."sentinella-watcher" = {
description = "sentinèlla watcher p2p peer monitor";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = lib.mkMerge [
{
Type = "simple";
ExecStart = "${self.packages.${system}."sentinèlla"}/bin/watcher";
Restart = "always";
RestartSec = "5s";
TimeoutStopSec = "30s";
KillSignal = "SIGTERM";
KillMode = "mixed";
RemainAfterExit = false;
StandardOutput = "journal";
StandardError = "journal";
StateDirectory = "sentinella";
Environment = lib.filter (s: s != "") [
"PEERS_SRV=${peersSrv}"
(lib.optionalString (cfg.watcher.self != null) "SELF=${cfg.watcher.self}")
"PEERS_SCHEME=${cfg.watcher.peersScheme}"
"POLLING_INTERVAL_SEC=${builtins.toString cfg.watcher.pollingIntervalSec}"
"STATE_DIR=/var/lib/sentinella"
(lib.optionalString (cfg.watcher.tgToken != null) "TG_TOKEN=${cfg.watcher.tgToken}")
(lib.optionalString (cfg.watcher.tgChatId != null) "TG_CHAT_ID=${cfg.watcher.tgChatId}")
];
}
(lib.mkIf (cfg.watcher.environmentFile != null) {
EnvironmentFile = cfg.watcher.environmentFile;
})
];
};
})
];
}
+104
View File
@@ -0,0 +1,104 @@
{ inputs, flake, self }:
{ config, pkgs, lib, ... }: let
cfg = config.hectic.services.support-bot;
system = pkgs.stdenv.hostPlatform.system;
packagesAttr = lib.mapAttrs (packageName: packageConfig:
packageConfig // {
name = packageName;
}) cfg;
packagesArr = builtins.attrValues packagesAttr;
in {
options = {
hectic.services.support-bot = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
redisHost = lib.mkOption {
type = lib.types.str;
default = "localhost";
example = "localhost";
description = ''
redis db host
if localhost - module spawns redis
'';
};
redisPort = lib.mkOption {
type = lib.types.port;
example = "42069";
description = ''redis db port'';
};
redisDb = lib.mkOption {
type = lib.types.int;
apply = x: if x >= 0 && x <= 15 then x else throw "must be 0..15";
default = 0;
example = "0";
description = ''redis db number (0-15)'';
};
environmentPath = lib.mkOption {
type = lib.types.path;
example = ''
config.sops.secrets."name-of-service/environment".path
'';
description = ''
BOT_TOKEN=
BOT_DEV_ID=
BOT_GROUP_ID=
BOT_EMOJI_ID=
'';
};
};
}
);
default = { };
example = lib.literalExpression /* nix */ ''
{
"name-of-service" = {
environmentPath = config.sops.secrets."name-of-service/environment".path;
redisDb = 3;
redisPort = 42069;
redisHost = localhost;
};
};
'';
description = "Declarative support bot config";
};
};
config = {
services.redis.servers = lib.mkMerge (map (supportConfig: {
"support-bot-${supportConfig.name}" = lib.mkIf (supportConfig.redisHost == "localhost") {
enable = true;
port = supportConfig.redisPort;
};
}) packagesArr);
systemd.services = lib.mkMerge (map (supportConfig: {
"support-bot-${supportConfig.name}" = {
description = "Support Bot Service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${self.packages.${system}.support-bot}/bin/support-bot";
Restart = "always";
RestartSec = "5s";
EnvironmentFile = supportConfig.environmentPath;
Environment = [
"REDIS_HOST=${supportConfig.redisHost}"
"REDIS_PORT=${builtins.toString supportConfig.redisPort}"
"REDIS_DB=${builtins.toString supportConfig.redisDb}"
];
# Shutdown configuration
TimeoutStopSec = "30s";
KillSignal = "SIGTERM";
KillMode = "mixed";
# Security and process management
RemainAfterExit = false;
StandardOutput = "journal";
StandardError = "journal";
};
};
}) packagesArr);
};
}
@@ -0,0 +1,71 @@
{
inputs,
flake,
self,
}:
{
lib,
config,
...
}: let
cfg = config.hectic.services.matrix;
in {
options = {
hectic.services.matrix = {
turnSecretFile = lib.mkOption {
type = lib.types.path;
description = ''
path to env file with matrix secrets
just raw secret
'';
};
publicIp = lib.mkOption {
type = lib.types.str;
description = ''
public IP address of the server, used by coturn for
listening and relay
'';
};
};
};
config = lib.mkIf cfg.enable {
services.coturn = rec {
enable = true;
realm = cfg.matrixDomain;
use-auth-secret = true;
static-auth-secret-file = cfg.turnSecretFile;
cert = "${config.security.acme.certs.${realm}.directory}/full.pem";
pkey = "${config.security.acme.certs.${realm}.directory}/key.pem";
listening-ips = [cfg.publicIp];
no-tcp-relay = true;
relay-ips = [cfg.publicIp];
listening-port = 3478;
tls-listening-port = 5349;
no-cli = true;
extraConfig = ''
verbose
'';
};
networking.firewall = {
allowedUDPPorts = [ 3478 5349 ];
allowedTCPPorts = [ 3478 5349 ];
allowedUDPPortRanges = [
{ from = 49152; to = 65535; }
];
};
services.matrix-synapse.settings = {
turn_uris = [
"turn:${cfg.matrixDomain}:3478?transport=udp"
"turn:${cfg.matrixDomain}:3478?transport=tcp"
"turns:${cfg.matrixDomain}:5349?transport=udp"
"turns:${cfg.matrixDomain}:5349?transport=tcp"
];
turn_user_lifetime = 86400000;
turn_allow_guests = true;
};
};
}
+119
View File
@@ -0,0 +1,119 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.services.xmpp;
in {
options = {
hectic.services.xmpp = {
enable = lib.mkEnableOption "General-purpose XMPP server (Prosody) for messaging clients like Monocles Chat";
domain = lib.mkOption {
type = lib.types.str;
description = ''
Primary XMPP domain. Users will have JIDs like user@domain.
'';
};
admins = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
example = [ "admin@example.org" ];
description = ''
List of admin JIDs.
'';
};
allowRegistration = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Allow in-band account registration from clients.
If false, create accounts with: prosodyctl register <user> <domain> <password>
'';
};
uploadFileSizeLimit = lib.mkOption {
type = lib.types.int;
default = 10485760;
description = ''
Maximum file upload size in bytes (default 10 MiB).
'';
};
};
};
config = lib.mkIf cfg.enable {
services.prosody = {
enable = true;
admins = cfg.admins;
allowRegistration = cfg.allowRegistration;
ssl = {
cert = "/var/lib/acme/${cfg.domain}/fullchain.pem";
key = "/var/lib/acme/${cfg.domain}/key.pem";
};
virtualHosts.${cfg.domain} = {
enabled = true;
domain = cfg.domain;
ssl = {
cert = "/var/lib/acme/${cfg.domain}/fullchain.pem";
key = "/var/lib/acme/${cfg.domain}/key.pem";
};
};
muc = [
{ domain = "conference.${cfg.domain}"; }
];
httpFileShare = {
domain = "upload.${cfg.domain}";
size_limit = cfg.uploadFileSizeLimit;
};
};
# Grant prosody read access to ACME certs (group is "nginx" since
# the nginx vhost requests the cert via enableACME)
users.users.prosody.extraGroups = [ "nginx" ];
# nginx vhost handles ACME HTTP-01 challenge for the XMPP domain.
# The cert also covers conference.* and upload.* subdomains.
services.nginx = {
enable = true;
virtualHosts.${cfg.domain} = {
enableACME = true;
forceSSL = true;
locations."/".return = "301 https://meet.${cfg.domain}";
};
};
security.acme = {
acceptTerms = true;
defaults = {
email = "hectic.yukkop.it@gmail.com";
enableDebugLogs = true;
};
# Add MUC + upload subdomains to the nginx-managed cert
certs.${cfg.domain} = {
extraDomainNames = [
"conference.${cfg.domain}"
"upload.${cfg.domain}"
];
reloadServices = [ "prosody" ];
};
};
networking.firewall = {
allowedTCPPorts = [
5222 # c2s (client-to-server)
5269 # s2s (server-to-server federation)
80 # ACME HTTP-01 challenge
443 # HTTPS
];
};
};
}
+79
View File
@@ -0,0 +1,79 @@
{
self,
inputs,
...
}:
{
config,
pkgs,
lib,
...
}: let
name = "yukkop";
#name = builtins.baseNameOf ./.;
home = "/home/${name}";
cfg = config.hectic.user.yukkop;
in {
imports = [
inputs.home-manager.nixosModules.home-manager
];
options.hectic.user.yukkop.enable = lib.mkEnableOption "Enable user.yukkop";
config = lib.mkIf cfg.enable {
users.users.${name} = {
isNormalUser = true;
initialPassword = "kk";
extraGroups = [ "wheel" "docker" "owner" ];
};
home-manager.users.${name} = {
home.stateVersion = "24.05";
home.packages = with pkgs; [
pinentry-tty
];
programs.password-store = {
enable = true;
package = (pkgs.pass.override {
x11Support = false;
waylandSupport = false;
dmenuSupport = false;
}).withExtensions (exts: with exts; [
pass-otp
]);
settings.PASSWORD_STORE_DIR = "${home}/.pass";
};
programs.gpg = {
enable = true;
homedir = "${home}/.gnupg";
};
services.gpg-agent = {
enable = true;
pinentry.package = pkgs.pinentry-tty;
enableZshIntegration = true;
defaultCacheTtl = 60 * 60;
maxCacheTtl = 60 * 60 * 24;
};
programs.bash.shellAliases = {
dev = "nix develop -c zsh";
# system-specific rebuild aliases can be added per-system
};
programs.git = {
enable = true;
lfs.enable = true;
settings = {
user.name = "yukkop";
user.email = "hectic.yukkop@gmail.com";
push.autoSetupRemote = true;
init.defaultBranch = "master";
};
};
};
};
}
@@ -0,0 +1,26 @@
{
inputs,
flake,
self,
}: {
lib,
pkgs,
modulesPath,
config,
...
}: {
# TODO:
# white list
# torent
# rate limit
# ping - game and speak
imports = [
self.nixosModules.xray-system
];
hectic.generic.xray-system = {
enable = true;
defaultSopsFile = ../../../sus/bfs.xray.yaml;
};
}
@@ -0,0 +1,21 @@
{
flake,
self,
inputs,
system,
...
}: let
# Use folder name as name of this system; sanitize for hostName (no dots)
name = builtins.baseNameOf ./.;
hostName = builtins.replaceStrings ["."] ["-"] name;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
};
modules = [
{ networking.hostName = hostName; }
(import ./${name}.nix { inherit flake self inputs; })
];
}
@@ -0,0 +1,230 @@
# Matrix Cluster Failover Runbook (`accord.tube`)
Primary: `hectic-lab` (NL, `128.140.75.58`)
Standby: `bfs.poland.xray` (PL, `91.198.166.181`)
Module: `hectic.generic.matrix-cluster` (`nixos/module/generic/matrix-cluster.nix`).
Shared secrets: `sus/matrix-cluster.yaml`.
All `psql` and `pg_ctl` invocations use PostgreSQL **17** at data dir
`/var/lib/postgresql/17`.
## Initial setup
### 1. Provision shared SOPS file (`sus/matrix-cluster.yaml`)
On a workstation with both yukkop and yukkop-alt age keys available:
```sh
sudo cat /var/lib/matrix-synapse/homeserver.signing.key # on NL (hectic-lab)
# Copy the single line value into the buffer for the next step.
sops sus/matrix-cluster.yaml
```
Populate the editor with:
```yaml
matrix:
signing-key: <paste verbatim signing-key line from NL>
postgres-replication-password: <openssl rand -base64 32>
object-storage:
credentials: |
ACCESS_KEY_ID=<verbatim copy from sus/hectic-lab.yaml>
SECRET_ACCESS_KEY=<verbatim copy from sus/hectic-lab.yaml>
porkbun-api-key: <PORKBUN_API_KEY>
porkbun-secret-api-key: <PORKBUN_SECRET_API_KEY>
```
Verify recipients:
```sh
sops updatekeys sus/matrix-cluster.yaml
sops -d sus/matrix-cluster.yaml | grep -E 'signing-key|porkbun-api-key|object-storage'
```
Expected: all five keys present, exit 0.
### 2. Deploy NL primary first
```sh
nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux' --target-host root@128.140.75.58
```
Verify on NL:
```sh
sudo systemctl status matrix-synapse postgresql matrix-cluster-replication-password
sudo -u postgres psql -c "select rolname, rolreplication from pg_roles where rolname='replication';"
# Expected: replication | t
```
### 3. Seed PL replica with `pg_basebackup`
On PL:
```sh
sudo systemctl stop postgresql
sudo rm -rf /var/lib/postgresql/17
sudo -u postgres install -d -m 0700 /var/lib/postgresql/17
sudo -u postgres PGPASSWORD="$(sudo cat /run/secrets/matrix/postgres-replication-password)" \
pg_basebackup \
-h 128.140.75.58 \
-p 5432 \
-U replication \
-D /var/lib/postgresql/17 \
-Fp -Xs -P -R \
--no-password
```
`-R` writes `standby.signal` and an initial `primary_conninfo`. The
matrix-cluster module's `matrix-cluster-standby-bootstrap` service will
overwrite `primary_conninfo` to use a libpq passfile on next boot.
### 4. Deploy PL standby
```sh
nixos-rebuild switch --flake .#'bfs.poland.xray|x86_64-linux' --target-host root@91.198.166.181
sudo systemctl start postgresql
```
Verify streaming on NL:
```sh
sudo -u postgres psql -c 'select client_addr, state, sync_state from pg_stat_replication;'
# Expected: 91.198.166.181 | streaming | async
```
Verify standby on PL:
```sh
sudo -u postgres psql -c 'select pg_is_in_recovery();'
# Expected: t
sudo systemctl is-active matrix-synapse
# Expected: inactive (standby keeps Synapse off)
```
### 5. Remove duplicate S3 credentials from `sus/hectic-lab.yaml`
Only AFTER NL is confirmed healthy reading from the new shared file:
```sh
sops sus/hectic-lab.yaml
# Delete the matrix/object-storage/credentials block.
sudo nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux'
```
## Normal operations
```sh
# NL: replication health
sudo -u postgres psql -c 'select * from pg_stat_replication;'
# Expected: 1 row, state=streaming, sync_state=async
# PL: replay status
sudo -u postgres psql -c 'select now() - pg_last_xact_replay_timestamp() as lag;'
# Both: cert renewal
sudo systemctl status acme-accord.tube.timer
sudo journalctl -u acme-accord.tube.service --since '24 hours ago'
# Synapse health (NL primary)
curl -sf https://accord.tube/_matrix/client/versions | head
```
## Planned failover (NL -> PL)
```sh
# 1. Drain NL: stop accepting writes.
sudo systemctl stop matrix-synapse
sudo systemctl stop postgresql # ensure no new WAL after this point
# 2. Promote PL replica.
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote
# Wait until pg_is_in_recovery() returns f:
sudo -u postgres psql -c 'select pg_is_in_recovery();'
# 3. Make the role switch declarative before rebuilding.
# Edit the flake so rebuilds match the promoted database state:
# - nixos/system/bfs.poland.xray/bfs.poland.xray.nix:
# hectic.generic.matrix-cluster.role = "primary";
# hectic.generic.matrix-cluster.overrideEnableSynapse = true;
# hectic.generic.matrix-cluster.secretsFile = config.sops.secrets."matrix/secrets".path;
# - nixos/system/hectic-lab/hectic-lab.nix:
# hectic.generic.matrix-cluster.role = "standby";
# hectic.generic.matrix-cluster.overrideEnableSynapse = false;
# hectic.generic.matrix-cluster.replication.peerHost = "91.198.166.181";
# hectic.generic.matrix-cluster.replication.allowedSourceIPs = [ "128.140.75.58/32" ];
# (You will also need a matrix/secrets entry on PL - copy from NL via SOPS.)
sudo nixos-rebuild switch --flake .#'bfs.poland.xray|x86_64-linux'
sudo nixos-rebuild switch --flake .#'hectic-lab|x86_64-linux'
sudo systemctl status matrix-synapse
# 4. Swap DNS A record at Porkbun:
# accord.tube A 91.198.166.181 (was 128.140.75.58)
# TTL: set to 300 in advance of any planned failover.
# Porkbun UI: https://porkbun.com/account/domainsSpeedy -> accord.tube -> DNS -> edit A record.
# Or via API:
sudo curl -sX POST https://api.porkbun.com/api/json/v3/dns/editByNameType/accord.tube/A \
-H 'content-type: application/json' \
-d "$(jq -n --arg k "$PORKBUN_API_KEY" --arg s "$PORKBUN_SECRET_API_KEY" \
'{secretapikey:$s,apikey:$k,content:"91.198.166.181",ttl:"300"}')"
# 5. Federation smoke test.
curl -s 'https://federationtester.matrix.org/api/report?server_name=accord.tube' | jq .FederationOK
# Expected: true
```
Expected after the rebuilds:
- `bfs.poland.xray` evaluates and runs as `role = "primary"`.
- `hectic-lab` evaluates as `role = "standby"` with Synapse forced off.
- Future `nixos-rebuild` runs preserve the promoted topology instead of reapplying standby settings to PL.
## Failback (PL -> NL)
```sh
# 1. Stop NL postgres if still up; clear its data dir.
sudo systemctl stop postgresql matrix-synapse
sudo rm -rf /var/lib/postgresql/17
# 2. Re-seed NL from PL (now the live primary).
sudo -u postgres install -d -m 0700 /var/lib/postgresql/17
sudo -u postgres PGPASSWORD="$(sudo cat /run/secrets/matrix/postgres-replication-password)" \
pg_basebackup -h 91.198.166.181 -p 5432 -U replication \
-D /var/lib/postgresql/17 -Fp -Xs -P -R --no-password
# 3. Temporarily flip roles in the flake:
# - hectic-lab.nix: role = "standby"; peerHost = "91.198.166.181";
# - bfs.poland.xray.nix: role = "primary"; peerHost = "128.140.75.58";
# Rebuild both.
# 4. Once NL is streaming green, do the reverse failover dance:
sudo systemctl stop matrix-synapse # on PL
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote # on NL
# Then revert the flake role assignments back to NL=primary / PL=standby and
# rebuild both hosts.
# 5. Swap DNS back at Porkbun (A -> 128.140.75.58).
```
## Disaster recovery (NL permanently lost)
```sh
# 1. Promote PL as the new permanent primary.
sudo -u postgres pg_ctl -D /var/lib/postgresql/17 promote
# 2. Edit nixos/system/bfs.poland.xray/bfs.poland.xray.nix:
# hectic.generic.matrix-cluster.role = "primary";
# hectic.generic.matrix-cluster.overrideEnableSynapse = lib.mkForce null;
# hectic.generic.matrix-cluster.replication.peerHost = "<new-standby-ip>";
# hectic.generic.matrix-cluster.replication.allowedSourceIPs = [ "<new-standby-ip>/32" ];
# 3. Provision a new host (any region with Porkbun-managed DNS) and import
# self.nixosModules.matrix-cluster with role = "standby" pointed at PL's IP.
# 4. Bootstrap the new standby via pg_basebackup from PL exactly as in
# "Initial setup" step 3, replacing 128.140.75.58 with PL's IP.
# 5. Update Porkbun A record to PL's IP permanently.
```
@@ -0,0 +1,229 @@
{
inputs,
flake,
self,
}: {
lib,
pkgs,
modulesPath,
config,
...
}: let
matrixBackend = "https://128.140.75.58";
matrixHost = "accord.tube";
jitsiHost = "meet.accord.tube";
elementEntryDomain = "element.bfs.band";
polandEntryDomain = "bfs.band";
backendProxyConfig = ''
proxy_ssl_server_name on;
proxy_ssl_name ${matrixHost};
proxy_set_header Host ${matrixHost};
'';
in {
imports = [
self.nixosModules.xray-system
self.nixosModules.matrix-cluster
self.nixosModules.matrix-cluster-users
];
hectic.generic.xray-system = {
enable = true;
defaultSopsFile = ../../../sus/bfs.xray.yaml;
};
hectic.generic.matrix-cluster = {
enable = true;
role = "primary";
matrixDomain = "accord.tube";
signingKeyFile = config.sops.secrets."matrix/signing-key".path;
secretsFile = config.sops.secrets."matrix/secrets".path;
turnSecretFile = config.sops.secrets."matrix/turn-secret".path;
publicIp = "91.198.166.181";
objectStorage.s3 = {
bucket = "matrix-hectic-lab";
regionName = "hel1";
endpointUrl = "https://hel1.your-objectstorage.com";
credentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
};
replication = {
peerHost = "128.140.75.58";
passwordFile = config.sops.secrets."matrix/postgres-replication-password".path;
};
acme = {
enable = false;
porkbunApiKeyFile = config.sops.secrets."matrix/porkbun-api-key".path;
porkbunSecretApiKeyFile = config.sops.secrets."matrix/porkbun-secret-api-key".path;
};
jitsi.preferredDomain = jitsiHost;
};
hectic.services.media-browser = {
enable = true;
port = 3001;
s3Bucket = "matrix-hectic-lab";
s3Endpoint = "https://hel1.your-objectstorage.com";
s3Region = "hel1";
s3CredentialsFile = config.sops.secrets."matrix/object-storage/credentials".path;
};
hectic.services.jitsi = {
enable = true;
hostName = jitsiHost;
};
security.acme = {
acceptTerms = true;
defaults.email = "security@bfs.band";
};
services.nginx = {
enable = true;
virtualHosts.${polandEntryDomain} = {
enableACME = true;
forceSSL = true;
locations."/".return = "302 https://${elementEntryDomain}";
locations."=/.well-known/matrix/client" = {
extraConfig = ''
default_type application/json;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
add_header Access-Control-Allow-Headers "X-Requested-With, Content-Type, Authorization";
'';
return = ''200 '{
"m.homeserver": {
"base_url": "https://${polandEntryDomain}"
},
"m.identity_server": {
"base_url": "https://vector.im"
},
"org.matrix.msc3575.proxy": {
"url": "https://${polandEntryDomain}"
},
"org.matrix.msc4143.rtc_foci": [
{
"type": "livekit",
"livekit_service_url": "https://${polandEntryDomain}/livekit/jwt"
}
]
}' '';
};
locations."= /livekit/jwt" = {
proxyPass = "${matrixBackend}/livekit/jwt";
extraConfig = backendProxyConfig;
};
locations."^~ /livekit/jwt/" = {
proxyPass = "${matrixBackend}/livekit/jwt/";
extraConfig = backendProxyConfig;
};
locations."= /livekit/sfu" = {
proxyPass = "${matrixBackend}/livekit/sfu";
proxyWebsockets = true;
extraConfig = backendProxyConfig + ''
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
proxy_set_header Accept-Encoding gzip;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
'';
};
locations."^~ /livekit/sfu/" = {
proxyPass = "${matrixBackend}/livekit/sfu/";
proxyWebsockets = true;
extraConfig = backendProxyConfig + ''
proxy_send_timeout 120;
proxy_read_timeout 120;
proxy_buffering off;
proxy_set_header Accept-Encoding gzip;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
'';
};
locations."^~ /_matrix/" = {
proxyPass = "${matrixBackend}/_matrix/";
extraConfig = backendProxyConfig;
};
locations."^~ /_synapse/client/" = {
proxyPass = "${matrixBackend}/_synapse/client/";
extraConfig = backendProxyConfig;
};
};
virtualHosts.${elementEntryDomain} = {
enableACME = true;
forceSSL = true;
locations."= /config.${elementEntryDomain}.json".return = "302 /config.json";
root = pkgs.element-web.override {
conf = {
default_server_config = {
"m.homeserver".base_url = "https://${polandEntryDomain}";
"m.homeserver".server_name = matrixHost;
"m.identity_server".base_url = "https://vector.im";
};
jitsi = {
preferred_domain = jitsiHost;
};
room_directory.servers = [ matrixHost ];
default_theme = "dark";
show_labs_settings = true;
};
};
};
};
sops.secrets."matrix/signing-key" = {
key = "matrix/signing-key";
owner = "matrix-synapse";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/postgres-replication-password" = {
key = "matrix/postgres-replication-password";
owner = "postgres";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/object-storage/credentials" = {
key = "matrix/object-storage/credentials";
owner = "matrix-synapse";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/secrets" = {
key = "matrix/secrets";
owner = "matrix-synapse";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/turn-secret" = {
key = "matrix/turn-secret";
owner = "turnserver";
group = "turnserver";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/porkbun-api-key" = {
key = "matrix/porkbun-api-key";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
sops.secrets."matrix/porkbun-secret-api-key" = {
key = "matrix/porkbun-secret-api-key";
mode = "0400";
sopsFile = "${flake}/sus/matrix-cluster.yaml";
};
}
+25
View File
@@ -0,0 +1,25 @@
{
flake,
self,
inputs,
system,
...
}: let
# Use folder name as name of this system; sanitize for hostName (no dots)
name = builtins.baseNameOf ./.;
hostName = builtins.replaceStrings ["."] ["-"] name;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
# jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x)
config.permittedInsecurePackages = [
"jitsi-meet-1.0.8792"
];
};
modules = [
{ networking.hostName = hostName; }
(import ./${name}.nix { inherit flake self inputs; })
];
}
+20
View File
@@ -0,0 +1,20 @@
{
flake,
self,
inputs,
system,
...
}: let
# Use folder name as name of this system
name = builtins.baseNameOf ./.;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
};
modules = [
{ networking.hostName = name; }
(import ./${name}.nix { inherit flake self inputs; })
];
}
+91
View File
@@ -0,0 +1,91 @@
{
inputs,
flake,
self
}: {
modulesPath,
pkgs,
lib,
...
}: {
imports = [
self.nixosModules.hectic
(modulesPath + "/profiles/qemu-guest.nix")
];
hectic = {
archetype.dev.enable = true;
hardware.hetzner-cloud.enable = true;
};
users.users.root.openssh.authorizedKeys.keys = [
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICrbBG+U07f7OKvOxYIGYCaNvyozzxQF+I9Fb5TYZErK yukkop vm-postgres''
];
services.postgresql =
let
package = pkgs.postgresql_15;
in {
enable = true;
package = package;
settings =
{
port = 64317;
listen_addresses = lib.mkForce "*";
shared_preload_libraries = "";
};
extensions = [ package.pkgs.hemar ];
authentication = builtins.concatStringsSep "\n" [
"local all all trust"
"host sameuser all 127.0.0.1/32 scram-sha-256"
"host sameuser all ::1/128 scram-sha-256"
];
initialScript = pkgs.writeText "init-sql-script" ''
SET log_min_messages TO DEBUG1;
SET client_min_messages TO DEBUG1;
ALTER DATABASE postgres SET log_min_messages TO DEBUG1;
ALTER DATABASE postgres SET client_min_messages TO DEBUG1;
CREATE EXTENSION "hemar";
\i ${flake}/package/c/hemar/test/mod.sql
'';
};
environment.systemPackages = with pkgs; [
gdb
hectic.nvim-pager
#(writeScriptBin "check" ''
# journalctl -u postgresql.service | grep postgresql-post-start | sed 's|psql:/nix/store/[^:]*:[0-9]*: ||' | sed 's|^[^:]*:[^:]*:[^:]*: ||' | grep -v '^\[.*\]' | ${hectic.prettify-log}/bin/prettify-log --color-output
#'')
];
programs.zsh.shellAliases = self.lib.sharedShellAliasesForDevVm // {
conn = "sudo su postgres -c 'psql -p 64317'";
};
virtualisation = {
vmVariant = {
systemd.services.fix-root-perms = {
description = "Fix root directory permissions";
after = [ "local-fs.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.coreutils}/bin/chmod 755 /";
};
};
virtualisation = {
diskSize = 1024*6;
diskImage = null;
forwardPorts = [ ];
};
};
};
networking.firewall = {
enable = true;
allowedTCPPorts = [
80
];
};
}
+20
View File
@@ -0,0 +1,20 @@
{
flake,
self,
inputs,
system,
...
}: let
# Use folder name as name of this system
name = builtins.baseNameOf ./.;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
};
modules = [
{ networking.hostName = name; }
(import ./${name}.nix { inherit flake self inputs; })
];
}
@@ -0,0 +1,91 @@
{
inputs,
flake,
self,
}: {
lib,
pkgs,
modulesPath,
config,
...
}:
{
imports = [
self.nixosModules.hectic
(modulesPath + "/profiles/qemu-guest.nix")
];
hectic = {
archetype.dev.enable = true;
hardware.hetzner-cloud.enable = true;
hardware.lenovo-ideapad-15arh7.enable = true;
};
environment.systemPackages = with pkgs.hectic.writers; [
(writeMinCBin "minc-hello-world" ["<stdio.h>"] /*c*/ ''
printf("hello world\n");
'')
(writeMinCBin "minc-env" ["<stdio.h>" "<stdlib.h>"] /*c*/ ''
char *env_name;
if (argc > 1) {
env_name = argv[1];
} else {
env_name = "HOME";
}
char *value = getenv(env_name);
if (value) {
printf("%s: %s\n", env_name, value);
} else {
printf("Environment variable %s not found.\n", env_name);
}
'')
(writeMinCBin "minc-env-check" ["<stdio.h>" "<stdlib.h>"] /*c*/ ''
char *env_name;
if (argc > 1) {
env_name = argv[1];
} else {
env_name = "HOME";
}
char *value = getenv(env_name);
if (value) {
char buffer[128];
sprintf(buffer, "echo $%s\n", env_name);
system(buffer);
} else {
printf("Environment variable %s not found.\n", env_name);
}
'')
];
users.users.root.openssh.authorizedKeys.keys = [
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICrbBG+U07f7OKvOxYIGYCaNvyozzxQF+I9Fb5TYZErK yukkop vm-postgres''
];
programs.zsh.shellAliases = self.lib.sharedShellAliasesForDevVm;
virtualisation = {
vmVariant = {
systemd.services.fix-root-perms = {
description = "Fix root directory permissions";
after = [ "local-fs.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.coreutils}/bin/chmod 755 /";
};
};
virtualisation = {
diskSize = 1024*6;
diskImage = null;
forwardPorts = [ ];
};
};
};
networking.firewall = {
enable = true;
allowedTCPPorts = [
80
];
};
}
+17
View File
@@ -0,0 +1,17 @@
{ pkgs, ... }: let
astroneerServer = pkgs.hectic.helpers.steam.buildSteamServer 728470;
in {
options = {
};
config = {
systemd.services."hectic-astroneer-server" = {
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
path = with pkgs; [ steamcmd ];
script = ''
echo zalupa
'';
};
};
}
+22
View File
@@ -0,0 +1,22 @@
{
flake,
self,
inputs,
system,
...
}: let
# Use folder name as name of this system
name = builtins.baseNameOf ./.;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
config.allowUnfreePredicate = pkg:
builtins.elem (inputs.nixpkgs.lib.getName pkg) [ "steamcmd" "steam-unwrapped" ];
};
modules = [
{ networking.hostName = name; }
(import ./${name}.nix { inherit flake self inputs; })
];
}
+44
View File
@@ -0,0 +1,44 @@
{
inputs ? null,
flake ? null,
self ? null,
...
}:
{
config ? null,
pkgs ? null,
lib ? null,
modulesPath ? null,
...
}: {
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
self.nixosModules.hectic
inputs.sops-nix.nixosModules.sops
./astroneer.nix
];
hectic = {
archetype.dev.enable = true;
hardware.hetzner-cloud = {
enable = true;
networkMatchConfigName = "enp1s0";
ipv4 = "91.98.127.6";
ipv6 = "2a01:4f8:1c1b:6f10";
};
};
sops = {
gnupg.sshKeyPaths = [ ];
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile = ../../../sus/games.yaml;
secrets."env" = {};
};
environment.systemPackages = (with pkgs; [ rsync git steamcmd hectic.AstroTuxLauncher ]);
users.users.root.openssh.authorizedKeys.keys = [
''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKAaObjLBslsdTlqEcYaS1TqX4x9aVJu75y27/8MFevO''
];
}
+108
View File
@@ -0,0 +1,108 @@
{
inputs ? null,
flake ? null,
self ? null,
...
}:
{
config ? null,
pkgs ? null,
lib ? null,
modulesPath ? null,
...
}:
with builtins;
with lib;
# with inputs.dream.lib;
let
in {
# networking.nat = {
# enable = true;
# internalInterfaces = [ "ve-+" ];
# externalInterface = "lo";
# # Lazy IPv6 connectivity for the container
# enableIPv6 = true;
# };
# containers.webserver = {
# autoStart = true;
# privateNetwork = true;
# hostAddress = "192.168.115.10";
# localAddress = "192.168.115.11";
# hostAddress6 = "fc00::1";
# localAddress6 = "fc00::2";
# config = import "${inputs.quteproxy}/nixos/system/quteproxy-staging/quteproxy-staging.nix" {
# self = inputs.quteproxy;
# inputs = inputs.quteproxy.inputs;
# flake = inputs.quteproxy;
# };
# };
# environment.etc.nixos.source = self;
# boot.kernelModules = [ "kvm" ];
# microvm.autostart = [
# "myvm1"
# ];
# microvm.vms = {
# myvm1 = {
# flake = self;
# updateFlake = "git+file:///etc/nixos";
# };
# };
# microvm = {
# mem = 1024*3;
# vcpu = 4;
# storeOnDisk = false;
# shares = [
# {
# proto = "9p";
# # securityModel = "mapped";
# tag = "ro-store";
# source = "/nix/store";
# mountPoint = "/nix/.ro-store";
# }
# {
# proto = "9p";
# securityModel = "mapped";
# tag = "fsRoot";
# source = "/media/pool/mythos/vm/work/vproxy/pr";
# mountPoint = "/home/devbox-user/pr";
# }
# ];
# interfaces = [
# {
# type = "user";
#
# # interface name on the host
# id = "vm-seht";
#
# # Ethernet address of the MicroVM's interface, not the host's
# # Locally administered have one of 2/6/A/E in the second nibble.
# mac = "02:00:00:00:00:01";
# }
# ];
# forwardPorts = [
# { from = "host"; host.port = 40500; guest.port = 22; }
# ];
#
# writableStoreOverlay = "/nix/.rw-store";
# volumes = [
# {
# autoCreate = true;
# size = 1024*32;
#
# image = "/media/pool/mythos/vm/work/vproxy/nix-store-overlay.img";
# mountPoint = config.microvm.writableStoreOverlay;
# }
# {
# autoCreate = true;
# size = 1024*32;
#
# image = "/media/pool/mythos/vm/work/vproxy/root.img";
# mountPoint = "/";
# }
# ];
# };
}
+20
View File
@@ -0,0 +1,20 @@
{
flake,
self,
inputs,
system ? "x86_64-linux",
...
}: let
# Use folder name as name of this system
name = builtins.baseNameOf ./.;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
};
modules = [
{ networking.hostName = name; }
(import ./${name}.nix { inherit flake self inputs; })
];
}
+306
View File
@@ -0,0 +1,306 @@
{
inputs,
flake,
self,
...
}:
{
config,
pkgs,
lib,
modulesPath,
...
}:
with builtins;
with lib;
let
domain = "hectic-lab.com";
matrixDomain = "accord.tube";
sslOpts = {
sslCertificate = config.sops.secrets."ssl/porkbun/${domain}/domain.cert.pem".path;
sslCertificateKey = config.sops.secrets."ssl/porkbun/${domain}/private.key.pem".path;
};
in {
imports = [
self.nixosModules.hectic
self.nixosModules.matrix-cluster
inputs.sops-nix.nixosModules.sops
self.nixosModules."shadowsocks-rust" # NOTE(nrv): impl
self.nixosModules."shadowsocks" # NOTE(nrv): usage/instance
inputs.hectic-landing.nixosModules.hectic-landing
(import ./containers.nix { inherit flake self inputs; })
(import ./mechabellum.nix { inherit flake self inputs domain sslOpts; })
(import (./. + "/sentinèlla.nix") { inherit flake self inputs domain sslOpts; })
];
services.hectic-landing = {
enable = true;
package = inputs.hectic-landing.packages.${pkgs.stdenv.hostPlatform.system}.hectic-landing;
domain = domain;
port = 3000;
host = "127.0.0.1";
};
# NOTE(yukkop): both nixos-mailserver and hectic-landing module set
# security.acme.defaults.email. Force the mailserver-aligned address.
security.acme.defaults.email = lib.mkForce "security@${domain}";
hectic = {
archetype.dev.enable = true;
hardware.hetzner-cloud = {
enable = true;
networkMatchConfigName = "enp1s0";
ipv4 = "128.140.75.58";
ipv6 = "2a01:4f8:c2c:d54a";
};
services.matrix = {
enable = false;
};
};
# NOTE(yukkop): disk was provisioned by Hetzner rescue image, disko was never
# run, so partition labels don't exist. Override fileSystems with actual UUIDs.
fileSystems."/" = lib.mkForce {
device = "/dev/disk/by-uuid/48ba7286-d019-4cdc-9784-459767979b07";
fsType = "ext4";
};
fileSystems."/boot" = lib.mkForce {
device = "/dev/disk/by-uuid/71F2-4E98";
fsType = "vfat";
options = [ "umask=0077" ];
};
programs.zsh.enable = true;
programs.zsh.interactiveShellInit = ''
setopt vi
'';
environment.systemPackages = with pkgs; [
git
rsync
python311
kitty
];
# Secrets config
sops = {
gnupg.sshKeyPaths = [ ];
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile = "${flake}/sus/hectic-lab.yaml";
};
users.users.root.openssh.authorizedKeys.keys = [
# yukkop
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMuP5NSfEQmO6m77xBWZvZ3hk7cw1q2k2vbsFd37rybU u0_a327@localhost"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBLxMo5icX2Xyng7mcWGnIi+c4ZbVygjPhuU8noCkfZ"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxgLlX/15Fk7PgIc9FSrA7oRtA8qK4GXfOhj7ZlNUaJ nix-on-droid@localhost"
# snuff
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFouceNUxI3bGC24/hfA8J3VuBpvTcZh3KhixgrMiLte"
# nrv
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE/EhBI6sJb2yHbTkqhZiCzUrsLE6t+CZe7RhS22z7w5 nrv@adamantia"
# github workflow
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKPEUArBxu7NUULT7Pi8ArtVxY1uVbIBSaeRKtqz1sz1"
];
users.users.ds4d = { # NOTE(nrv): artishoque
isNormalUser = true;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINcjBc57N6MxtMYAHEB/nwZ+OGsG3P1KWO1ZXvzQyhKn ds4d@ds4d"
];
};
users.users.sshuttle = {
isNormalUser = true;
openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKd4iU2E5fiwPwBbeo1ZPo0YBFEj9qBPew/KitaO+OHU"
];
};
sops.secrets."mailserver/security/hashedPassword" = {};
sops.secrets."mailserver/yukkop/hashedPassword" = {};
sops.secrets."mailserver/daniil-perlyk/hashedPassword" = {};
sops.secrets."mailserver/snuff/hashedPassword" = {};
sops.secrets."mailserver/antoshka/hashedPassword" = {};
sops.secrets."mailserver/founders/hashedPassword" = {};
sops.secrets."mailserver/lvgkcfjl/hashedPassword" = {};
sops.secrets."init-postgresql" = {
key = "init-postgresql";
};
services.mailserver = {
enable = true;
domain = domain;
loginAccounts = {
"security" = {
hashedPasswordFile = config.sops.secrets."mailserver/security/hashedPassword".path;
};
"founders" = {
hashedPasswordFile = config.sops.secrets."mailserver/founders/hashedPassword".path;
};
"lvgkcfjl" = {
hashedPasswordFile = config.sops.secrets."mailserver/lvgkcfjl/hashedPassword".path;
};
"yukkop" = {
hashedPasswordFile = config.sops.secrets."mailserver/yukkop/hashedPassword".path;
};
"daniil-perlyk" = {
hashedPasswordFile = config.sops.secrets."mailserver/daniil-perlyk/hashedPassword".path;
};
"snuff" = {
hashedPasswordFile = config.sops.secrets."mailserver/snuff/hashedPassword".path;
};
"antoshka" = {
hashedPasswordFile = config.sops.secrets."mailserver/antoshka/hashedPassword".path;
};
};
};
mailserver.stateVersion = 3;
services.redis.servers."vproxy-bot-test-state" = {
enable = true;
port = 6379;
};
services.mysql = {
enable = true;
package = pkgs.mariadb;
};
networking.firewall = {
allowedTCPPorts = [
80
443
3306 # mysql
11012 # gitea ssh
25565
55228 # ss-bfs
];
allowedUDPPorts = [
51820 # wg-bfs
55228 # ss-bfs
];
# Postgres replication: only the PL standby peer may reach 5432.
extraInputRules = ''
ip saddr 91.198.166.181/32 tcp dport 5432 accept
'';
};
virtualisation.docker.enable = true;
systemd.tmpfiles.rules = [
"d /var/www/store 0755 nginx nginx -"
];
sops.secrets."ssl/porkbun/${domain}/domain.cert.pem" = { group = "nginx"; mode = "0440"; };
sops.secrets."ssl/porkbun/${domain}/private.key.pem" = { group = "nginx"; mode = "0440"; };
sops.secrets."ssl/porkbun/${domain}/public.key.pem" = { group = "nginx"; mode = "0440"; };
services.nginx = {
enable = true;
# NOTE(yukkop): virtualHosts.${domain} is owned by the hectic-landing module
# (ACME-managed). See services.hectic-landing above.
virtualHosts."store.${domain}" = sslOpts // {
forceSSL = true;
root = "/var/www/store";
locations."/" = {
extraConfig = ''
autoindex on;
'';
};
};
virtualHosts."snuff.${domain}" = sslOpts // {
forceSSL = true;
locations."/" = {
extraConfig = ''
proxy_pass http://188.32.215.29:3993/;
proxy_redirect off;
'';
};
};
virtualHosts."nrv.${domain}" = sslOpts // {
forceSSL = true;
locations."/" = {
extraConfig = ''
proxy_pass http://127.0.0.1:22842/;
proxy_redirect off;
'';
};
};
virtualHosts."yukkop.${domain}" = sslOpts // {
forceSSL = true;
locations."/" = {
extraConfig = ''
proxy_pass http://127.0.0.1:9855/;
proxy_redirect off;
'';
};
};
virtualHosts."gitea.${domain}" = {
enableACME = true;
forceSSL = true;
locations."/" = {
extraConfig = ''
proxy_pass http://127.0.0.1:11011/;
proxy_redirect off;
'';
};
};
};
services = {
gitea = {
enable = true;
settings.service.DISABLE_REGISTRATION = false;
settings.server = {
HTTP_PORT = 11011;
SSH_PORT = 11012;
};
database = {
createDatabase = true;
type = "postgres";
socket = "/run/postgresql";
user = "gitea";
name = "gitea";
};
};
};
# === WireGuard (disabled) ===
sops.secrets."wg-bfs/private-key" = {};
# networking.wireguard.interfaces = let
# subnet = "10.13.37";
# externalInterface = "eth0";
# in {
# wg-bfs = {
# ips = [ "${subnet}.1/24" ];
# listenPort = 51820;
# postSetup = ''
# ${pkgs.iptables}/bin/iptables -t 'nat' -A 'POSTROUTING' -s '${subnet}.0/24' -o '${externalInterface}' -j 'MASQUERADE'
# '';
# postShutdown = ''
# ${pkgs.iptables}/bin/iptables -t 'nat' -D 'POSTROUTING' -s '${subnet}.0/24' -o '${externalInterface}' -j 'MASQUERADE'
# '';
# privateKeyFile = config.sops.secrets."wg-bfs/private-key".path;
# generatePrivateKeyFile = false;
# peers = with lib; with builtins; let
# pubkeys = [
# "3dVzf1jxnVVTkLAyxedW+kRQBexZDzYDwpaLIcTrLjc=" # nrv (host: 2)
# "Kk2d0ncj24rO0qbuKh4V4t1OLnmVYbeaYvuEnL2OPFM=" # lysmi (host: 3)
# "BkM/NEDbR/XQ6WYQ0Yt+nJrc2HFCVsoW4QxBmkqxHn8=" # yukkop (host: 4)
# ];
# hosts = lists.range 2 254;
# zipped = zipLists pubkeys hosts;
# in flip map zipped ({ fst, snd }: {
# publicKey = "${fst}";
# allowedIPs = [ "${subnet}.${toString snd}/32" ];
# });
# };
# };
}
+47
View File
@@ -0,0 +1,47 @@
{
inputs,
domain,
sslOpts,
...
}: {
pkgs,
...
}: let
mechDomain = "mechabellum.${domain}";
apiHost = "127.0.0.1";
apiPort = 8010;
system = pkgs.stdenv.hostPlatform.system;
in {
imports = [
inputs.mechabellum-replay-analysis.nixosModules.default
];
mechabellum.api = {
enable = true;
host = apiHost;
port = apiPort;
extraEnvironment = {
CORS_ALLOWED_ORIGINS = "https://${mechDomain}";
};
};
mechabellum.worker = {
enable = true;
};
services.nginx.virtualHosts."${mechDomain}" = sslOpts // {
forceSSL = true;
root = inputs.mechabellum-replay-analysis.packages.${system}.frontend;
locations."/api" = {
proxyPass = "http://${apiHost}:${builtins.toString apiPort}";
extraConfig = ''
proxy_http_version 1.1;
'';
};
locations."/" = {
tryFiles = "$uri $uri/ /index.html";
};
};
}
+27
View File
@@ -0,0 +1,27 @@
{
inputs,
flake,
self,
domain,
sslOpts,
...
}: { ... }: {
hectic.services."sentinèlla" = {
probe.enable = true;
watcher = {
enable = true;
pollingIntervalSec = 60;
# TG_TOKEN= and TG_CHAT_ID= are read from sus/sentinella-default.yaml
# (auto-declared by the module as sops.secrets."sentinèlla/watcher/environment")
};
};
services.nginx = {
virtualHosts."probe.${domain}" = sslOpts // {
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:5988";
};
};
};
}
+204
View File
@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Counter App</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.tab-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-button {
padding: 10px 20px;
border: none;
background: #e5e7eb;
cursor: pointer;
border-radius: 4px;
}
.tab-button.active {
background: #3b82f6;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.counter-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.counter-button {
padding: 15px;
border: 1px solid #e5e7eb;
background: white;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
.counter-button:hover {
background: #f3f4f6;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.control-button {
padding: 10px 20px;
background: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.control-button:hover {
background: #2563eb;
}
</style>
</head>
<body>
<div class="controls">
<button class="control-button" onclick="resetCounters()">Reset</button>
<button class="control-button" onclick="saveCounters()">Save</button>
<input type="file" id="loadFile" style="display: none" onchange="loadCounters(event)">
<button class="control-button" onclick="document.getElementById('loadFile').click()">Load</button>
</div>
<div class="tab-buttons">
<button class="tab-button active" onclick="showTab('counters')">Counters</button>
<button class="tab-button" onclick="showTab('chart')">Chart</button>
</div>
<div id="counters" class="tab-content active">
<div class="counter-grid" id="counterGrid"></div>
</div>
<div id="chart" class="tab-content">
<canvas id="counterChart"></canvas>
</div>
<script>
let counters = Array(20).fill(0);
let chart = null;
function initializeCounters() {
const grid = document.getElementById('counterGrid');
grid.innerHTML = '';
for (let i = 0; i < 20; i++) {
const button = document.createElement('button');
button.className = 'counter-button';
button.innerHTML = `Button ${i + 1}<br>Count: ${counters[i]}`;
button.onclick = () => incrementCounter(i);
grid.appendChild(button);
}
updateChart();
}
function incrementCounter(index) {
counters[index]++;
updateCounterDisplay();
updateChart();
}
function updateCounterDisplay() {
const buttons = document.querySelectorAll('.counter-button');
buttons.forEach((button, i) => {
button.innerHTML = `Button ${i + 1}<br>Count: ${counters[i]}`;
});
}
function updateChart() {
if (chart) {
chart.destroy();
}
const ctx = document.getElementById('counterChart').getContext('2d');
chart = new Chart(ctx, {
type: 'bar',
data: {
labels: Array.from({length: 20}, (_, i) => `Button ${i + 1}`),
datasets: [{
label: 'Click Count',
data: counters,
backgroundColor: '#3b82f6'
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
}
function showTab(tabId) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-button').forEach(button => {
button.classList.remove('active');
});
document.getElementById(tabId).classList.add('active');
document.querySelector(`[onclick="showTab('${tabId}')"]`).classList.add('active');
if (tabId === 'chart') {
updateChart();
}
}
function resetCounters() {
counters = Array(20).fill(0);
updateCounterDisplay();
updateChart();
}
function saveCounters() {
const data = JSON.stringify(counters);
const blob = new Blob([data], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'counters.json';
a.click();
URL.revokeObjectURL(url);
}
function loadCounters(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
counters = JSON.parse(e.target.result);
updateCounterDisplay();
updateChart();
} catch (error) {
alert('Invalid file format');
}
};
reader.readAsText(file);
}
}
initializeCounters();
</script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>tg test</title>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="https://telegram.org/js/telegram-web-app.js?56"></script>
<script src="test.js"></script>
</head>
<body>
<div class="container">
<div class="content">
<p>TEST (again)</p>
</div>
</div>
</body>
</html>
+41
View File
@@ -0,0 +1,41 @@
function webappInit() {
console.log("Init start");
window.Telegram.WebApp.BackButton.isVisible = true;
window.Telegram.WebApp.backgroundColor = "#E60C0C";
let initData = window.Telegram.WebApp.initData;
if (initData) {
console.log("InitData", initData);
validate(initData);
}
console.log("Init end");
}
function validate(initData) {
const urlencodedData = initData;
const decodedData = decodeURIComponent(urlencodedData);
fetch(
"http://localhost:52022/rpc/webapp_auth",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Profile": "qutegate",
},
body: JSON.stringify({ raw_init_data: btoa(decodedData) }),
}
)
}
function waitForWebApp() {
if (window.Telegram && window.Telegram.WebApp) {
console.log("Telegram WebApp is available");
webappInit();
} else {
console.log("Telegram WebApp is not available yet");
setTimeout(waitForWebApp, 100);
}
}
waitForWebApp();
+61
View File
@@ -0,0 +1,61 @@
# Router Access (TP-Link)
The server `neuro` is behind a NAT router at `192.168.0.1`.
## Network Info
| Device | IP |
|--------|-----|
| Router (TP-Link) | 192.168.0.1 |
| neuro (internal) | 192.168.0.10 |
| neuro (external) | 95.31.254.84 |
## Access Router Admin Panel
The router blocks requests with non-local `Host` headers, so SSH port forwarding
with `-L` returns 403. Use a SOCKS proxy instead:
1. Start SSH SOCKS proxy:
```sh
ssh -D 1080 neuro
```
2. Launch browser with SOCKS proxy:
```sh
nix run nixpkgs#chromium -- --proxy-server="socks5://localhost:1080"
# or
nix run nixpkgs#firefox -- # then configure manually (see below)
```
3. Navigate to **http://192.168.0.1**
### Firefox Manual Configuration
1. Settings -> Network Settings -> Settings...
2. Select "Manual proxy configuration"
3. SOCKS Host: `localhost`, Port: `1080`, SOCKS v5
4. Click OK
## Port Forwarding
Ports that need to be forwarded from router to `192.168.0.10`:
| External Port | Internal Port | Protocol | Service |
|---------------|---------------|----------|---------|
| 22 | 22 | TCP | SSH |
| 80 | 80 | TCP | HTTP |
| 443 | 443 | TCP | HTTPS |
| 4443 | 4443 | TCP | Jitsi |
| 5222 | 5222 | TCP | XMPP (c2s) |
| 5269 | 5269 | TCP | XMPP (s2s) |
| 10000 | 10000 | UDP | Jitsi Videobridge |
| 25565 | 25565 | TCP | Minecraft |
## Troubleshooting
Check if ports are open externally:
```sh
nix run nixpkgs#nmap -- -Pn -p 80,443 95.31.254.84
```
Expected: `open` (not `filtered`)
+55
View File
@@ -0,0 +1,55 @@
{
flake,
self,
inputs,
system,
...
}: let
# Use folder name as name of this system
name = builtins.baseNameOf ./.;
in self.lib.nixpkgs-lib.nixosSystem {
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [
self.overlays.default
inputs.nix-minecraft.overlay
];
config.allowUnfreePredicate = pkg: builtins.elem (self.lib.nixpkgs-lib.getName pkg) [
"minecraft-server"
"neoforge"
"nvidia-x11"
"cuda_nvcc"
"cuda_cudart"
"cuda_cuobjdump"
"cuda_cupti"
"cuda_nvdisasm"
"cuda_cccl"
"cuda_nvml_dev"
"cuda_nvrtc"
"cuda_nvtx"
"cuda_profiler_api"
"libcusparse_lt"
"libcublas"
"libcufft"
"libcufile"
"libcurand"
"libcusolver"
"libnvjitlink"
"libcusparse"
"cudnn"
];
# jitsi-meet depends on libolm which is marked insecure (CVE-2024-4519x)
config.permittedInsecurePackages = [
"jitsi-meet-1.0.8792"
];
};
modules = [
{ networking.hostName = name; }
(import ./${name}.nix { inherit flake self inputs; })
inputs.nix-minecraft.nixosModules.minecraft-servers
];
}
+10
View File
@@ -0,0 +1,10 @@
{ ... }:
{
services.xserver.videoDrivers = [ "nvidia" ];
hardware.graphics.enable = true;
hardware.nvidia = {
open = false;
nvidiaSettings = false;
modesetting.enable = false;
};
}
+61
View File
@@ -0,0 +1,61 @@
{
pkgs,
...
}:
{
services.minecraft-servers = {
enable = true;
eula = true;
openFirewall = true;
servers = {
vanilla = {
enable = true;
jvmOpts = "-Xmx6G -Xms2G";
package = pkgs.minecraftServers.vanilla-1_21_11;
serverProperties = {
server-port = 25565;
difficulty = "hard";
online-mode = true;
view-distance = 32;
level-seed = "8306359138650378643";
pause-when-empty-seconds = 0;
};
};
createAeronautics = {
enable = true;
jvmOpts = "-Xmx8G -Xms2G";
package = pkgs.minecraftServers.neoforge-1_21_1;
symlinks = {
mods = pkgs.linkFarmFromDrvs "mods" (
builtins.attrValues {
Sable = pkgs.fetchurl {
url = "https://cdn.modrinth.com/data/T9PomCSv/versions/g8CObHcP/sable-neoforge-1.21.1-1.1.3.jar";
sha512 = "8180e214681c171c9e3b7fa307f7a92bd7de0b8125d671291425f04a4ba26b408758d8ea80a6386d8e73bb1e6b02caf3f20afb9b91ecedd48c37ed44363ac961";
};
Create = pkgs.fetchurl {
url = "https://cdn.modrinth.com/data/LNytGWDc/versions/UjX6dr61/create-1.21.1-6.0.10.jar";
sha512 = "11cc8fc049d2f67f6548c7abfada6b82a3adb5c7ca410a742de04bbca76e03862c518721b88d806f6e6d768a4d68531fdb903a85859b25d1484d550cc7bafd4b";
};
CreateAeronautics = pkgs.fetchurl {
url = "https://cdn.modrinth.com/data/oWaK0Q19/versions/1sv6OtSz/create-aeronautics-bundled-1.21.1-1.1.3.jar";
sha512 = "94831bc4702b3864524258fa0a73a50ab3cd37e9c157b5c6688a6845b866ec5838452804050b55e490549d91dad909fc37f0d619f354c5676e2e2651b9c15ec6";
};
}
);
};
serverProperties = {
server-port = 25566;
difficulty = "hard";
online-mode = true;
view-distance = 20;
pause-when-empty-seconds = 0;
};
};
};
};
}
+272
View File
@@ -0,0 +1,272 @@
{
inputs,
flake,
self,
}: {
lib,
pkgs,
config,
...
}: let
ollamaLibraryPath = lib.makeLibraryPath [
pkgs.stdenv.cc.cc.lib
pkgs.zlib
];
ollamaWrapperBundledLibraryPath = "$out/lib/ollama:$out/lib/ollama/cuda_v12:$out/lib/ollama/cuda_v13";
ollamaServiceBundledLibraryPath = "${ollamaPrebuilt}/lib/ollama:${ollamaPrebuilt}/lib/ollama/cuda_v12:${ollamaPrebuilt}/lib/ollama/cuda_v13";
ollamaPrebuilt = pkgs.stdenvNoCC.mkDerivation {
pname = "ollama";
version = "0.22.1";
src = pkgs.fetchurl {
url = "https://github.com/ollama/ollama/releases/download/v0.22.1/ollama-linux-amd64.tar.zst";
hash = "sha256-4nwP6PYKgkFi+Bzge0v9p2fc5PNX12LhSbPQ3gq62fs=";
};
nativeBuildInputs = [
pkgs.autoPatchelfHook
pkgs.gnutar
pkgs.makeWrapper
pkgs.zstd
];
buildInputs = [
pkgs.stdenv.cc.cc.lib
pkgs.zlib
];
autoPatchelfIgnoreMissingDeps = [
"libcuda.so.1"
];
unpackPhase = ''
runHook preUnpack
tar --zstd -xf $src
runHook postUnpack
'';
installPhase = ''
runHook preInstall
mkdir -p $out
cp -R . $out/
test -x $out/bin/ollama
mv $out/bin/ollama $out/bin/.ollama-unwrapped
makeWrapper $out/bin/.ollama-unwrapped $out/bin/ollama \
--set-default LD_LIBRARY_PATH "${ollamaLibraryPath}:/run/opengl-driver/lib:${ollamaWrapperBundledLibraryPath}"
runHook postInstall
'';
meta.mainProgram = "ollama";
};
in {
imports = [
self.nixosModules.hectic
inputs.sops-nix.nixosModules.sops
./minecraft.nix
./hardware.nix
];
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETMumAHP+htbRvbrmzVoeesbT0+WcH1Wz8htk+7Ik+6"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEJZFglwpPMFLnQDOqi84nlMFktZSSu1GzUIafvClUaD"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGj7u/JuY9RwjoxnmO2b+pwC8XbMn+QOy44UpuN0Y1do riquizu"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBbR42mLupcsF64ydGSx7HdB+qMVJq41a43UZMI7VvOj"
];
# disko.devices = {
# disk.master = {
# device = lib.mkDefault "/dev/disk/by-id/nvme-eui.00000000000000000026b7686dfafe35";
# type = "disk";
# content = {
# type = "gpt";
# partitions = {
# ESP = {
# size = "1G";
# type = "EF00";
# content = {
# type = "filesystem";
# format = "vfat";
# mountpoint = "/boot";
# };
# };
# root = {
# size = "100%";
# content = {
# type = "filesystem";
# format = "ext4";
# mountpoint = "/";
# };
# };
# };
# };
# };
# };
#hectic.services.matrix = {
# enable = true;
# secretsFile = config.sops.secrets."matrix/secrets".path;
# turnSecretFile = config.sops.secrets."matrix/turn-secret".path;
# postgresql = {
# port = 5432;
# initialEnvFile = config.sops.secrets."init-postgresql".path;
# };
# matrixDomain = "accord.tube";
#};
#hectic.services.jitsi = {
# enable = true;
# hostName = "meet.accord.tube";
#};
#hectic.services.xmpp = {
# enable = true;
# domain = "accord.tube";
# admins = [ "yukkop@accord.tube" ];
#};
# Static landing page for bfs.band on :80 and :443 (no HTTP->HTTPS redirect).
security.acme = {
acceptTerms = true;
defaults.email = "security@bfs.band";
};
services.nginx = {
enable = true;
virtualHosts."bfs.band" = let
site = pkgs.runCommand "bfs-band-site" {} ''
mkdir -p $out
cat > $out/index.html <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>bfs.band</title>
</head>
<body>
<h1>bfs.band</h1>
<p>Coming soon.</p>
</body>
</html>
EOF
'';
in {
serverAliases = [ "www.bfs.band" ];
enableACME = true;
addSSL = true; # serve on both :80 and :443, no forced redirect
forceSSL = false;
root = "${site}";
};
};
services.ollama = {
enable = true;
host = "127.0.0.1";
port = 11434;
package = ollamaPrebuilt;
home = "/var/lib/ollama";
models = "/var/lib/ollama/models";
environmentVariables = {
LD_LIBRARY_PATH = "${ollamaLibraryPath}:/run/opengl-driver/lib:${ollamaServiceBundledLibraryPath}";
OLLAMA_NEW_ENGINE = "true";
};
loadModels = [ "qwen3.6:27b" "qwen3.5:9b" "gemma3:4b" "gpt-oss:20b" ];
openFirewall = false;
};
networking = {
networkmanager.enable = true;
useDHCP = lib.mkDefault true;
interfaces.enp6s0 = {
useDHCP = lib.mkDefault true;
wakeOnLan.enable = true;
};
firewall = {
enable = true;
allowedTCPPorts = [
80 443 # HTTP, HTTPS
];
allowedUDPPorts = [ 9 ]; # Wake on LAN
};
};
hardware.enableRedistributableFirmware = true;
hectic = {
archetype.base.enable = true;
archetype.dev.enable = true;
};
sops = {
gnupg.sshKeyPaths = [ ];
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile = ../../../sus/neuro.yaml;
#secrets."init-postgresql" = {};
#secrets."matrix/secrets" = {};
#secrets."matrix/turn-secret" = {
# owner = "turnserver";
# group = "turnserver";
# mode = "0400";
#};
};
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod" "sr_mod" "ext4" ];
boot.initrd.kernelModules = [ "nvme" ];
boot.extraModulePackages = [ ];
fileSystems."/" =
{ device = "/dev/disk/by-label/NIXROOT";
fsType = "ext4";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-label/NIXBOOT";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware = {
cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
};
swapDevices = [ ];
programs.tmux.enable = true;
zramSwap.enable = true;
zramSwap.priority = 100;
zramSwap.memoryMax = null;
zramSwap.algorithm = lib.mkDefault "zstd";
zramSwap.swapDevices = 1;
zramSwap.memoryPercent = lib.mkDefault 100;
environment.systemPackages = with pkgs; let
python-ai = python3.withPackages (ps: let
torchCuda = ps.torchWithCuda;
torchvision = ps.torchvision.override { torch = torchCuda; };
pytorch3dCuda = ps.pytorch3d.override { torch = torchCuda; };
in [
torchCuda
torchvision
pytorch3dCuda
ps.fvcore
ps.iopath
ps.tqdm
hectic.py3-openai-shap-e # Uncomment when needed; depends on torch
]);
in [
#python-ai
git
neovim
wget
ethtool
rsync
# docker
];
}

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