Compare commits

...

389 Commits

Author SHA1 Message Date
yukkop a8da44d438 feat: lab: generic functions for mail 2026-05-31 21:29:11 +00:00
yukkop 6dded5584e fix?: hetzner-cloud: newer generation 2026-05-31 11:26:26 +00:00
yukkop 7d0b335626 fix?: matrix-cluster-users 2026-05-31 11:09:52 +00:00
yukkop 5a069130f7 fix: matrix-cluster-users 2026-05-31 11:04:56 +00:00
yukkop 7d0c0370ed fix: hardware: some 2026-05-31 10:58:25 +00:00
yukkop 35de436105 fix: lab: gitea 2026-05-31 10:56:58 +00:00
yukkop 4099f2f27e fix: gitea: now works 2026-05-29 21:27:53 +00:00
snuff 4c7533a3df fix(gitea)?: syntax 2026-05-29 20:38:16 +03:00
snuff a9fadc65d0 fix(gitea)?: postgres socket peer auth for gitea 2026-05-29 20:17:08 +03:00
snuff 7817c65b9e fix?: +gitea v3 2026-05-29 20:07:27 +03:00
snuff 258f95cddc fix?: +gitea v2 2026-05-29 18:43:31 +03:00
snuff 0c45b64f1c fix?: +gitea 2026-05-29 18:37:02 +03:00
snuff 0835cf9b30 feat?: +gitea 2026-05-29 18:29:47 +03:00
yukkop 5813481d86 feat: matrix-cluster: hz 2026-05-27 19:04:56 +00:00
yukkop a53ad7780b Merge branch 'master' of github.com:hectic-lab/util.nix 2026-05-27 18:51:33 +00:00
yukkop 2acfddb03b fix!: matrix-cluster: +jitsy 2026-05-27 18:51:18 +00:00
yukkop cefef5c6a4 fix: mechabellum: api 2026-05-27 18:48:29 +00:00
yukkop e16f6add77 feat: matrix-cluster: shared users 2026-05-27 12:54:15 +00:00
yukkop 47167d09d2 feat: matrix-cluster: shared users 2026-05-27 12:41:51 +00:00
yukkop dbea099675 fix: matrix 2026-05-25 23:12:05 +00:00
yukkop a72cf624aa feat: matrix prepare move to poland 2026-05-25 07:48:16 +00:00
yukkop 8fb937d77c feat: matrix: diferent servers one home server 2026-05-25 07:11:43 +00:00
yukkop 9b8428c8ce fix: some recomendations 2026-05-24 15:08:40 +00:00
yukkop abbbcc4ba8 fix: element 2026-05-24 14:54:01 +00:00
yukkop e2448eab8d fix: element 2026-05-23 23:11:58 +00:00
yukkop 72319bdf69 some 2026-05-23 22:57:09 +00:00
yukkop 007d3a71fe fix: hectic-lab: mechabellum 2026-05-23 20:53:30 +00:00
yukkop 88a937beee feat: hectic-lab: mechabellum for lismy 2026-05-23 19:40:36 +00:00
yukkop eab1ba5064 chore: devide xrays systems 2026-05-23 18:27:02 +00:00
yukkop 9f03d1a804 chore: hectic-lab: +vismajor in matrix 2026-05-23 18:17:27 +00:00
yukkop e0a17460d9 fix 2026-05-23 17:13:49 +00:00
yukkop 95e8e041a9 chore: hecitc-lab: +lvgkcfjl email to hectic-lab 2026-05-23 14:11:27 +00:00
yukkop d2bdcb3667 fix: media-browser: view button 2026-05-23 12:18:05 +00:00
yukkop 56f369cf61 fix: media-browser: synced files 2026-05-23 12:03:56 +00:00
yukkop e4b0b3d96e feat: media-browser: preview for local files 2026-05-23 11:56:04 +00:00
yukkop 3de60e9701 feat: media-browser for matrix 2026-05-23 11:46:58 +00:00
yukkop e2720b7d5b fix: matrix: s3 sync 2026-05-23 11:22:49 +00:00
yukkop 919190e7b4 feat: matrix: s3 object storage 2026-05-23 09:59:34 +00:00
yukkop 990e184ae3 fix?: support bot 2026-05-23 08:50:10 +00:00
yukkop 16178ede49 chore: hecitc-lab: add lvgkcfjl to matrix 2026-05-23 08:49:51 +00:00
yukkop 5eb5f5985d feat: hectic-lab: matrix 100mb max file 2026-05-23 06:47:34 +00:00
yukkop 559bcddcdc feat: hectic-lab: users for matrix 2026-05-22 21:19:52 +00:00
yukkop af0fb1055c feat: module: ~matrix 2026-05-22 20:13:47 +00:00
yukkop 87aff4e447 feat: +package merge-archive 2026-05-22 07:45:47 +00:00
yukkop 2cabde19fe feat: deploy: 1 no tty option 2026-05-05 20:19:52 +00:00
yukkop 1701a07649 feat: lab: +models 2026-05-04 10:19:38 +00:00
yukkop a04e8f0849 feat: lab: configure olama models 2026-05-04 05:41:25 +00:00
yukkop 3c579f5a2f Merge branch 'master' of github.com:hectic-lab/util.nix 2026-05-03 22:07:13 +00:00
yukkop 11fd897a25 feat: lab: configure olama 2026-05-03 22:06:57 +00:00
yukkop 36052b9602 fix: db-tool: NO_TTY 2026-05-03 15:35:22 +00:00
yukkop 1dd2d5bb98 fix: posix-helpers 2026-05-03 05:23:23 +00:00
yukkop fa510a08cb 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 9d2d6c15c9 feat: windows-devshell 2026-05-02 15:31:26 +00:00
yukkop f9903206af fix: linux-devshell for ubuntu 2026-05-01 23:11:23 +00:00
yukkop 47c43e0d9d fix: linux-devshell from root 2026-05-01 22:56:03 +00:00
yukkop 7c12d2dfeb chore: fix nixpkgs 25.05 deprecation warnings 2026-05-01 22:26:29 +00:00
yukkop ff608e56c5 test: fix: . 2026-05-01 22:19:43 +00:00
yukkop 5fd0dec6fe fix: wsl system 2026-05-01 21:57:41 +00:00
yukkop 88c8c8d44f test: arch 2026-05-01 21:40:54 +00:00
yukkop 7c0a25eb15 test: linux-devshell 2026-05-01 21:13:15 +00:00
yukkop 433d8283fb feat: linux-devshell: init 2026-05-01 20:39:38 +00:00
yukkop fa97bb9d38 fix: db-tool: envcontent 2026-05-01 12:31:44 +00:00
yukkop f4e73e934b fix(db-tool): typo hydate.stdout.log -> hydrate.stdout.log 2026-04-30 23:10:46 +00:00
yukkop df8bce0132 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 0ec8e910a0 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 6ef0d8338a feat: postgres hooks 2026-04-30 21:59:53 +00:00
yukkop c2edda9c20 feat: start impliment postgres hooks 2026-04-30 21:36:22 +00:00
yukkop 5e018c2b1c feat: include hectic-landing to hectic 2026-04-30 19:33:21 +00:00
yukkop 72955d606a 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 3cf28f2dac 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 1d1c28f7f3 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 47c7fa9bf8 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 3e4ce505c3 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 94dd68587b 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 e67c2e0c47 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 26010eebbf 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 977b2cfa95 feat(\db-tool\): introduce unified db-tool package with postgres harness and tests (T0-T8) 2026-04-30 09:06:44 +00:00
yukkop 0db5b0030b Merge branch 'master' of github.com:hectic-lab/util.nix 2026-04-29 09:57:40 +00:00
snuff 208521bb34 feat?(neuro): create aeronautics minecraft server 2026-04-28 14:04:14 +03:00
yukkop d1d463fdbc 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 af04210a69 debug(package): sentinèlla: surface host(1) and SRV-parse failures in watcher 2026-04-27 17:28:01 +00:00
yukkop 63d2df5fcd 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 13b55ecf9f feat(package): sentinèlla: switch peer discovery from A records to SRV records 2026-04-27 14:08:23 +00:00
yukkop 62a7f25b8e fix(package): sentinèlla: probe — use socat EXEC pipes to avoid pty CR/LF mangling 2026-04-27 13:34:52 +00:00
yukkop def84b0890 feat: sentinella: some 2026-04-27 12:58:36 +00:00
yukkop e6df3b8456 fix: hectic-lab: email name 2026-04-26 22:22:31 +00:00
yukkop 8f3fa1e06e feat: sentinella: update 2026-04-26 22:22:15 +00:00
yukkop bb8095bea0 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 7bdaf7ffec 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 d6eb724b02 docs: +plan 2026-04-26 14:11:37 +00:00
yukkop cd19fe604a Merge branch 'merge-hectic-lab' 2026-04-26 13:52:17 +00:00
yukkop 4c978d1e88 fix: hectic-lab: something 2026-04-26 13:48:36 +00:00
yukkop 52734effa6 feat: hectic-lab: mail server 2026-04-26 10:33:28 +00:00
yukkop fdf3d97a41 chore: config 2026-04-25 15:18:01 +00:00
yukkop 9da1be0fef zalupa 2026-04-18 20:53:44 +00:00
yukkop df21de498c feat: work on profinity 2026-04-14 08:43:56 +00:00
yukkop a80f39eab6 docs: neuro: router forwarding 2026-03-31 20:09:27 +00:00
yukkop b74fe93e17 feat: bfs: add xray package 2026-03-31 19:59:40 +00:00
yukkop 292cee1e5c chore: bfs: add 3 cert 2026-03-23 19:47:54 +00:00
yukkop 695a9b3bbb some jitsi shit 2026-03-23 19:22:40 +00:00
yukkop 936aab8723 some 2026-03-22 06:55:10 +00:00
yukkop e07e931504 feat: wsl: some things 2026-03-15 10:59:19 +00:00
yukkop 739981cb91 fix: deploy: logs 2026-03-01 09:36:22 +00:00
yukkop fee41074aa feat: migrator: +multifiles migrations 2026-02-28 21:19:29 +00:00
yukkop 356542d059 chore: 2026-02-28 20:25:13 +00:00
yukkop ed961d59c0 feat: merge own wsl config 2026-02-28 01:57:42 +00:00
yukkop 6928fa9969 feat: init hectic-lab merge 2026-02-27 19:57:44 +00:00
yukkop fa31b8ac06 feat: neuro: some 2026-02-27 12:53:51 +00:00
yukkop 594ca4156f fix: some warnings 2026-02-23 18:27:36 +00:00
yukkop 8e242979b6 refactor: derustification 2026-02-20 17:36:20 +00:00
yukkop 60cdb2f97e fix: +fixed nixpkgs 2026-02-20 17:30:36 +00:00
yukkop db86014b28 refactor: update to 25.11 2026-02-20 17:18:38 +00:00
yukkop 9b1118ec00 feat: nixvim + zsh, fix: warnings 2026-02-20 16:41:18 +00:00
yukkop e12e8dcccd apply patch from neuro 2026-02-20 12:51:32 +00:00
yukkop 519f531dd5 feat: +neuro devshell 2026-02-20 12:06:55 +00:00
yukkop 2d289c7f0c refactor: deploy: update logs 2026-02-20 12:04:15 +00:00
yukkop 707305e65d fix: migrator: more accurate check for hectic.migrator isntalled 2026-02-17 16:50:08 +00:00
yukkop fa691cdb05 chore: something about hemar tree sitter 2026-02-17 16:17:47 +00:00
yukkop 91e70b9734 feat: +pol bfs server 2026-02-17 16:13:58 +00:00
yukkop 4be09990d1 fix: nvim-pager close all pages on q 2026-02-17 08:55:41 +00:00
yukkop ae60db5632 hz 2026-02-14 18:35:04 +00:00
yukkop 87fc53c8c0 hz 2026-02-14 15:59:13 +00:00
yukkop cdc4f37c40 feat: migrator: colorize help in migrator.sh 2026-02-06 08:38:36 +00:00
yukkop 31d1aa3820 feat: stong &2 in log.sh 2026-02-05 06:50:54 +00:00
yukkop 2751eec8ea feat(nixos): postgresql: +settings script 2026-01-29 18:56:22 +00:00
yukkop a229f6649e Merge branch 'master' of github.com:hectic-lab/util.nix 2026-01-16 16:16:43 +00:00
yukkop 0f2f57a34f feat(nixos): +zombro hardware 2026-01-16 16:16:13 +00:00
snuff 91c856efee chore: removed excess variables 2026-01-13 18:13:52 +03:00
yukkop 4c19f20f0f iso 2026-01-13 15:04:33 +00:00
yukkop 2e1838b82b feat(template): +nodejs 2026-01-04 20:40:43 +00:00
yukkop e5307a51bb feat(nixos): some moves to nixos server 2025-12-28 19:18:15 +00:00
yukkop c923a5f1e9 feat(nixos): +neuro system 2025-12-26 20:04:42 +00:00
yukkop d152170e1e fix(package): migrator: sqlite transactions 2025-12-23 19:31:44 +00:00
yukkop ce9f3c87c6 fix(package): migrator: handle sqlite posible settings 2025-12-23 18:30:26 +00:00
yukkop cc918864fd fix(pakcage): migrator: -z -> +x 2025-12-23 18:18:51 +00:00
yukkop f840c6e062 fix(package): migrator: awk dependency 2025-12-21 13:19:46 +00:00
yukkop d9f997868d feat(pakcage): migrator: add dependency 2025-12-20 01:19:40 +00:00
yukkop caada00fe6 feat(package): migrator: up to latest 2025-12-18 00:45:09 +00:00
yukkop e8d4d7afe4 fix(package): migrator: uncomplite RAISE on sqlite 2025-12-17 03:28:03 +00:00
yukkop 849db431b7 feat(package): migrator: ! sqlite support 2025-12-17 03:24:59 +00:00
yukkop ef2bd26ead feat(package): migrator: mvp 2025-12-16 17:28:36 +00:00
yukkop b763257783 test(hemar): update parser test to match better paths 2025-12-09 14:21:03 +00:00
yukkop 2a824db6b1 fix(package): hemar: paths better handling 2025-12-09 14:13:34 +00:00
yukkop 6102f00de3 test(hemar): many tests but not pass 2025-12-09 13:49:43 +00:00
yukkop aa5cc832df feat(package): hemar: json_escape() 2025-12-09 03:47:27 +00:00
yukkop 2a846d0375 feat(package): hemar: interpolation 2025-12-08 23:47:33 +00:00
yukkop 361714cdba feat(package): accord: some cli things 2025-12-08 01:55:33 +00:00
yukkop 22a05fa8de feat(nixos): bgs: vless 2025-12-07 03:40:07 +00:00
yukkop 4f0e92c5e7 refactor(nixos): bfs: some secrets 2025-12-07 02:25:29 +00:00
yukkop d417e9f6d8 feate(package): accord: init 2025-12-06 23:49:14 +00:00
yukkop 788748433c Merge branch 'master' of github.com:hectic-lab/util.nix 2025-12-06 04:47:46 +00:00
yukkop f1337db536 fix(package): migrator: index_of subshell issue 2025-12-06 04:47:31 +00:00
snuff 5f122aaf6c fix: matrix works with self-hosted element 2025-12-06 00:36:01 +03:00
yukkop 517bab60e1 fix: bfs nixosConfiguration check 2025-12-05 21:01:53 +00:00
yukkop 7aacc14a58 feate(nixos) bfs: matrix, element, bla bla bla 2025-12-05 19:36:53 +00:00
yukkop d3450a334b feat(nixos): bfs: fix: element-rtc 2025-12-05 18:17:41 +00:00
yukkop 567f28d677 feat(nixos): bfs: +matrix, very unsave 2025-12-05 16:30:34 +00:00
yukkop cac1b65498 feat(nixos): netherlands xray 2025-12-05 00:56:08 +00:00
yukkop 6008da3af1 feat(nixos): xray 2025-12-04 21:03:01 +00:00
yukkop 659ede1043 feat(package): migrator: something 2025-12-04 21:02:38 +00:00
yukkop 758c431fb4 feat(package): hemar: more grammar parsing 2025-11-30 13:31:40 +00:00
yukkop ca7f8a14e9 fix(nixos): generic postgresql 2025-11-29 13:48:57 +00:00
yukkop b78feba410 feat(package): hemar: some parser work 2025-11-29 00:30:27 +00:00
yukkop 7976c073e4 feat(package): hemar: add indexes to tree-sitter 2025-11-28 10:29:59 +00:00
yukkop 260489fcdd feat(package): hemar: add tree sitter hightlights 2025-11-27 19:45:12 +00:00
yukkop 0213f98515 fix(package): hemar: treesitter language name 2025-11-27 19:15:35 +00:00
yukkop 75bb993d0a fix(package): hemar: treesitter language name 2025-11-27 19:13:07 +00:00
yukkop eccfbda649 feat(package): hemar: tree sitter grammar 2025-11-27 17:53:49 +00:00
yukkop 8a14cef524 feat(package): hemar: antlr grammar, but still does not work 2025-11-26 14:48:51 +00:00
yukkop 35b52733ab feat(package): hemar: I think I was wrong 2025-11-25 15:45:02 +00:00
yukkop 68d850655c feat(package): hemar: some stages of parsing 2025-11-25 13:19:45 +00:00
yukkop 0205b9782c feat(package): hemar: conected to nix config 2025-11-24 14:33:45 +00:00
yukkop 278a8386d7 fix(package): migrator: init migration 2025-11-22 11:50:42 +00:00
yukkop 8ac20396e1 feat(package): migrator: some migrate up works and init 2025-11-17 16:26:25 +00:00
yukkop 1c8e233109 test: fix: migrator: multifile test 2025-11-17 01:57:32 +00:00
yukkop 7211b7b590 feat(legacy): helpers: allow HECTIC_NAMESPACE be empty 2025-11-16 16:56:26 +00:00
yukkop b72e240a83 feat(legacy): helpers: update log, unefficient but works 2025-11-16 16:38:38 +00:00
yukkop dd943267f0 refactor: remove nix warnings 2025-11-14 21:13:32 +00:00
yukkop 36bcfa788e feat(package): nbt2json: pack into nix 2025-11-14 21:12:41 +00:00
yukkop 90067ebc04 fix(template): correct hectic utils input 2025-11-14 15:53:12 +00:00
yukkop 1ff27bd96c feat(package): voronoi: simple voronoi algoritm realization 2025-11-14 15:51:28 +00:00
yukkop e162de4471 feat(package): migrator: ! migration up 2025-11-14 15:50:55 +00:00
yukkop 6ca49bbdd1 feat(package): migrator: init migration table 2025-11-04 13:21:15 +00:00
yukkop 0156ad64e0 feat(package): migrator: create new migration 2025-11-04 12:15:29 +00:00
yukkop f363a91138 refactor(package): pull out all realization from default.nix 2025-11-03 20:08:20 +00:00
yukkop 4558ba4755 fix(package): in default.nix migratior -> migrator 2025-11-03 19:15:57 +00:00
yukkop 464c56e672 feat: +checks flake output 2025-11-03 19:02:50 +00:00
yukkop 4e2ca9cd1e feat(package): +migrator 2025-11-03 18:55:35 +00:00
yukkop ff95cb447a feat(legacy): +writeDash +helpers 2025-11-03 18:51:38 +00:00
yukkop 4ec1f7dc98 fix(nixos): hardwares 2025-10-30 09:38:58 +00:00
yukkop e00c2663e5 feat(nixos): cloudzy hardware 2025-10-28 16:16:01 +00:00
yukkop c3f1d34767 feat(template): +avarage 2025-10-28 11:41:26 +00:00
yukkop df138a3f22 feat(package): sentinella: sqlschema 2025-10-28 11:37:22 +00:00
yukkop f2a5152924 refactor(package): sentinella: +logging 2025-10-25 09:54:57 +00:00
yukkop e1c9c503c0 fix(nixos): module: wrong separator 2025-10-25 07:21:07 +00:00
yukkop d56b33e114 feat(package): sentinel: some changes 2025-10-25 06:48:48 +00:00
yukkop 5e2a2542e2 feat(package): deploy: rollback 2025-10-23 14:48:10 +00:00
yukkop 84f00a17fd refactor(nixos): modules: hetzner network config 2025-10-23 11:49:34 +00:00
yukkop 819d4df645 some fixes 2025-10-23 11:28:56 +00:00
yukkop 8879352a3a feat(package): +deploy 2025-10-23 10:22:48 +00:00
yukkop aa162d3f2a refactor(nixos): modules: hetzner hardware 2025-10-22 09:32:41 +00:00
yukkop a9de125131 feat(nixos): modules: +network configuration to hetzner hardware 2025-10-22 08:20:27 +00:00
yukkop 6c6b365c2b ruined every thing again 2025-10-21 17:27:09 +00:00
yukkop 4d85b5671e ruined every thing 2025-10-20 17:17:08 +00:00
yukkop 5b860d849c feat(package): sentinèlla: sentinel: loging 2025-10-19 15:16:38 +00:00
yukkop 908bc96aeb feat(nixos): lenovo hardware 2025-10-16 12:22:19 +00:00
yukkop 5d85a6ddd3 fix: lib systems 2025-10-16 09:52:49 +00:00
yukkop a531c3b581 fix(nixos): senttinèlla-probe: module args 2025-10-12 08:59:03 +00:00
yukkop f55dde1b65 fix: writers & overlays 2025-10-11 14:56:25 +00:00
yukkop a153298a2e refactor: legacyPackages 2025-10-10 13:13:03 +00:00
yukkop 1d0ed49431 feat(package): onlinepubs: init 2025-10-08 15:58:35 +00:00
yukkop 66045e61cc fix(package): `sentinèlla one more unset error 2025-10-07 10:55:37 +00:00
yukkop bb56405dbf fix(package): `sentinèlla unset error 2025-10-07 04:02:49 +00:00
yukkop 579d4546ca fix(package): sentinèlla: base64 logic 2025-10-03 19:02:33 +00:00
yukkop facf3cf0a2 feat(package): sentinèlla: add auth 2025-10-03 02:30:24 +00:00
yukkop 329f9e9877 feat(package): hemar: continios work 2025-10-01 11:09:09 +00:00
yukkop befdb1c9e4 refactor: hemar reboot 2025-09-28 04:44:46 +00:00
yukkop d20d34d829 feat: appropriate sops dream wrapper 2025-09-12 17:17:29 +00:00
yukkop bf7fb382aa fix 2025-09-02 10:33:04 +00:00
yukkop b80dd6d781 fix: stupid error 2025-08-31 16:01:00 +00:00
yukkop aa9f8f6b5b refactor(nixos): module: hectic services config path 2025-08-28 18:17:18 +00:00
yukkop 7896589fac feat(nixos): module: +support-bot 2025-08-28 15:02:58 +00:00
yukkop a777d186ea feat(package): shellplot: created 2025-08-23 16:37:08 +00:00
yukkop 13c783bcd2 feat(package): server-health: created 2025-08-23 15:16:15 +00:00
yukkop d24df7ad21 feat: add templates 2025-08-05 04:41:27 +00:00
yukkop a7fbd59144 fix(package): nix-derivation-hash: purify 2025-07-24 19:33:51 +00:00
yukkop 6433dfccff feat(package): nix-derivation-hash 2025-07-23 20:15:53 +00:00
yukkop 46763ff6da feat(lib): shell: local-dir 2025-07-22 21:15:42 +00:00
yukkop 0f279508f5 feat: service.postgresql module extension 2025-07-22 02:11:32 +00:00
yukkop 02a5b42ef2 fix(lib): shell: remove unneccessary ''$ 2025-07-18 03:23:25 +00:00
yukkop 04cdb31c25 fix(nixos): module: user 2025-07-17 16:53:48 +00:00
yukkop 52930f2bd6 feat(lib): shell: +check-tool 2025-07-17 16:22:31 +00:00
yukkop f32ff17cea fix(lib): logs: mkdir error work around 2025-07-17 16:17:43 +00:00
yukkop b9ba47174b feat(nixos): module: +user.yukkop 2025-07-17 16:10:04 +00:00
yukkop d0a45d313f refactor: consistence 2025-07-17 16:00:59 +00:00
yukkop b2e5bb7f60 refactor: pull out flake devshells 2025-07-17 15:48:20 +00:00
yukkop f2e7f57c5d refactor: pull out flake packages 2025-07-17 15:28:29 +00:00
yukkop 6d7217e8f9 refactor: pull out systems 2025-07-17 15:21:06 +00:00
yukkop f9d37f0bdd feat: modules 2025-07-17 13:37:33 +00:00
yukkop 79e7c489bf feat: shell modules 2025-07-16 06:36:27 +00:00
yukkop 87168a0e1f feat: +some python packages 2025-07-06 22:12:14 +00:00
yukkop 4f33d0d937 feat: remove vmw_pvscsi on aarch64-linux 2025-07-05 23:24:58 +00:00
yukkop 54cc7fa9f6 feat(package): slpt: jq dependency 2025-07-03 01:21:44 +00:00
yukkop d1ee6a6fe7 feat(package): update slpt 2025-07-02 16:08:30 +00:00
yukkop c10a12cdf8 feat(package) add slpt 2025-07-01 02:26:34 +00:00
yukkop 3aaf40a0da fix(package): bolt-unpack update nodejs_22 2025-06-30 23:48:14 +00:00
yukkop ff511679c8 Merge branch 'master' of github.com:hectic-lab/util.nix 2025-06-30 21:00:04 +00:00
yukkop c814cf72c7 refactor: move to nixpkgs 25.05 2025-06-30 20:43:38 +00:00
yukkop 24946e4800 fix: plsh 2025-06-30 16:31:04 +00:00
yukkop 234740ad03 fix: remove broken haskell 2025-06-30 16:24:56 +00:00
yukkop b1892f6280 feat: plsh 2025-06-30 16:06:43 +00:00
yukkop 6b5055d5cf feat: aliases to system preset 2025-06-11 22:38:54 +00:00
yukkop 276a5f2a80 chore: pager AnsiEsc 2025-06-11 22:25:34 +00:00
yukkop 54fc4408f5 feat: find a way separate pg extension from overlay 2025-05-22 11:57:09 +00:00
yukkop cf4327b3db done 2025-05-21 20:00:02 +00:00
yukkop 65884480f4 checkpoint 2025-05-21 19:50:47 +00:00
yukkop b8ef79df33 test: hemar: fix parse tests according code cahnges 2025-05-19 14:36:37 +00:00
yukkop 2ab1f4645b fix: hemar: EBUCHIE WHITESPACЫ 2025-05-19 13:59:26 +00:00
yukkop bec7864618 fix: hemar: section whitespaces checkpoint 2025-05-19 11:07:10 +00:00
yukkop e534edd448 fix: hemar: include object does not exists 2025-05-18 20:18:54 +00:00
yukkop 1a2af49a6d test: hemar: test all at once 2025-05-18 19:58:04 +00:00
yukkop 440b2d9663 feat: hemar: included tag 2025-05-18 18:51:21 +00:00
yukkop 3d69cf24b1 fix: hemar: section implimentation 2025-05-18 13:20:57 +00:00
yukkop 8d339555ca feat: hemar: section checkpoint 2025-05-18 10:27:38 +00:00
yukkop d8d7b707f1 fix: hemar: jsonb_value_by_path string copy 2025-05-17 17:59:10 +00:00
yukkop b613c42e9b chore: hemar: more logs for god of logs 2025-05-17 15:56:46 +00:00
yukkop 92f8752977 feat: hemar: separate jsonb_get_by_path form postgresql 2025-05-17 14:30:18 +00:00
yukkop 515019c053 chore: forgoten changes 2025-05-17 14:13:11 +00:00
yukkop c4c44b5064 test: exec 2025-05-17 13:39:40 +00:00
yukkop 2709bd6da8 test: hemar: more tests for god of tests 2025-05-16 01:18:01 +00:00
yukkop bcb3b7f680 feat: hemar: exec handle the {{ }} inside 2025-05-16 00:37:30 +00:00
yukkop 05a52d9a63 fix: hemar: parser 2025-05-16 00:30:11 +00:00
yukkop d64a850809 test: hemar: puh 2025-05-15 23:57:18 +00:00
yukkop e4acdba080 test: hemar: +20 tests for parsing 2025-05-15 18:54:56 +00:00
yukkop d6b824d034 test: hemar: +10 complex tests 2025-05-15 17:53:45 +00:00
yukkop e82793f82c feat: find element in jsonb by path is works!!! 2025-05-15 17:48:28 +00:00
yukkop e8b17bc04c feat!: vpizdu 2025-05-15 15:33:53 +00:00
yukkop e5173818be feat: hemar: handling [] in path 2025-05-14 21:52:01 +00:00
yukkop 896c20b8fd feat: something 2025-05-14 21:30:16 +00:00
yukkop 9c700bbf55 feat: array with objects 2025-05-14 18:56:42 +00:00
yukkop 52d0a93e60 fix: more parsing errors handling, section body fix 2025-05-14 14:09:48 +00:00
yukkop 43a412df9f feat!: to the moon 2025-05-14 12:15:45 +00:00
yukkop e0d101fd06 feat!: one more checkpoint 2025-05-13 21:56:27 +00:00
yukkop d52bec5025 feat!: better? 2025-05-13 21:41:37 +00:00
yukkop c9c982036f feat!: some section parse issue 2025-05-13 15:44:55 +00:00
yukkop d1c27f0540 refactor: gemar grand rework 2025-05-13 12:12:57 +00:00
yukkop 9e93f0b26a fix: hemar & hectic: nothing 2025-05-13 02:30:15 +00:00
yukkop 76dd4f26c3 fix: hemar: unbeliveble, without memmory error? not true 2025-05-13 01:15:43 +00:00
yukkop b46fdb6ad9 feat!: many work useles with hemar 2025-05-10 11:00:36 +00:00
yukkop b766f0d07a fix: hectic C: template parse sections 2025-05-04 19:33:52 +00:00
yukkop 5c4572d421 fix: hectic C: template parse 2025-05-04 13:49:29 +00:00
yukkop 5411b5605c refactor: hemar: logs 2025-04-24 00:37:03 +00:00
yukkop c2dbf72f14 feat: hel -> hemar--amend 2025-04-23 12:47:28 +00:00
yukkop e35d2fad05 feat: static page for fun 2025-04-23 12:37:32 +00:00
yukkop 63622960f9 feat hectic C: logger log in file 2025-04-23 01:04:49 +00:00
yukkop fef42c6e53 feat hectic C: no asan option 2025-04-22 15:09:47 +00:00
zerosummed 2d75500f15 feat(package): +support-bot 2025-04-22 10:00:33 +03:00
yukkop 214109854e feat: wrapper for pg_dump 2025-04-22 01:13:23 +00:00
yukkop ad6d25ea6e feat: wrapper for pg_dumpall 2025-04-22 00:44:52 +00:00
yukkop 8d3039c8e8 refactor: hectic C: static-asan 2025-04-20 13:38:16 +00:00
yukkop bfbcf5893d refactor: hectic C: perfavor 2025-04-20 13:32:22 +00:00
yukkop 2bd2e17053 refactor: hectic C: libhectic.so -> hectic.so 2025-04-20 09:57:32 +00:00
yukkop be71d59270 fix: hel: anus linking 2025-04-20 00:33:34 +00:00
yukkop 3d071e9a0c build: hectic C: shared library in build 2025-04-19 23:44:39 +00:00
yukkop aa506a5036 fix: hectic: lib building 2025-04-19 23:23:09 +00:00
yukkop a603d269d8 fix: hel: Makefile LIBS 2025-04-19 23:10:13 +00:00
yukkop 67d9c8fd44 feat: hel: c89... 2025-04-19 23:04:29 +00:00
yukkop 14f3804a54 feat: hel: "" -> <> 2025-04-19 22:38:58 +00:00
yukkop d58994db6e feat: hecitc C: add hectic-config 2025-04-19 22:05:56 +00:00
yukkop 951786c882 feat: hel: render without execute 2025-04-19 20:30:00 +00:00
yukkop 1bc487328d fix: overlay postgres extensions 2025-04-19 02:39:10 +00:00
zerosummed 02a09446c7 fix?(package): postgreact: ~stir the stupid shit till it works [3] 2025-04-18 21:37:23 +03:00
zerosummed b7ec06096f fix?(package): postgreact: ~stir the stupid shit till it works [2] 2025-04-17 07:06:34 +03:00
zerosummed 3fdb01d7bd fix?(package): postgreact: ~stir the stupid shit till it works [1] 2025-04-17 07:06:31 +03:00
zerosummed 067082f24a fix?(package): postgreact 2025-04-17 06:22:21 +03:00
zerosummed 8f5c89f6ce style: ~apply alejandra 2025-04-17 03:57:54 +03:00
zerosummed 6dc09c55f0 refactor: postgreact: ~file modes, control file 2025-04-17 03:46:53 +03:00
yukkop 2420e0b6d7 feat: postgreact: hello world 2025-04-15 20:05:53 +00:00
yukkop de2a06334b Merge branch 'master' of github.com:hectic-lab/util.nix 2025-04-15 17:04:12 +00:00
yukkop 151f8c1209 test: hectic C!: templater sectin 2025-04-15 17:03:15 +00:00
yukkop abbc480a61 test: hectic C: templater interpolation 2025-04-15 16:33:49 +00:00
yukkop e2882aac03 feat: hectic C: prettify debug strings 2025-04-15 15:53:39 +00:00
yukkop 87730d5023 feat: +plhaskell package 2025-04-15 05:51:37 +00:00
yukkop 42284eaa3d feat: hectic C: some sigfault 2025-04-15 04:54:31 +00:00
yukkop 9688160a8d feat: hectic C: debug to json parser 2025-04-14 19:43:57 +00:00
yukkop 78d17b72ab fix: hectic C: union debug string constructor 2025-04-14 17:27:32 +00:00
yukkop 318aee6381 feat: hectic C: union in debug string constructor 2025-04-14 16:28:16 +00:00
yukkop 58f68a2aee fix: hectic C: impruve cycle detection for debug strig constructor 2025-04-13 23:13:28 +00:00
yukkop a2f7886f8e refactor: hectic C: logger rules and results 2025-04-13 19:38:11 +00:00
yukkop 045aaf00a2 feat: hectic C: template node debug, but union issue in debug string constructor 2025-04-12 18:25:25 +00:00
yukkop fe59708f5e test: hectic C: fix logger test 2025-04-12 15:38:06 +00:00
yukkop f43bc7e666 feat: hectic C: generic result 2025-04-12 15:21:33 +00:00
yukkop 84d5eac1aa test: hectic C: debug constructor 2025-04-12 01:38:40 +00:00
yukkop 8aeff83e79 feat: hectic C: debug constructor impruve 2025-04-12 01:07:20 +00:00
yukkop d801d7641a feat: hectic C: debug constructor wroks!!! 2025-04-11 22:34:43 +00:00
yukkop 8a9813d97c feat: hectic C: debug string constructor init 2025-04-11 10:33:20 +00:00
yukkop 77dfa58f04 fix: hectic C: template parser config 2025-04-10 14:26:21 +00:00
yukkop cf1c77c548 feat: hectic C: template parser, template to json 2025-04-09 19:52:43 +00:00
yukkop 596ee3b9c9 docs: hectic C: macro for reduse boilerplane with code position 2025-04-07 22:55:37 +00:00
yukkop d96782384d docs: hectic C: types for template parser 2025-04-07 22:44:05 +00:00
yukkop 1beab62935 feat: hectic C: logging settings 2025-04-07 16:54:26 +00:00
yukkop 9d29c6e2a2 fix: hectic C: impruve logging guidelines 2025-04-07 05:26:30 +00:00
yukkop cd1e463eaa fix: hectic C: impruve logging 2025-04-06 19:18:14 +00:00
yukkop fb566113c4 fix: hmpl: section blocks 2025-04-06 15:51:21 +00:00
yukkop 42bb81c2a3 feat: hectic C: debug print 2025-04-06 15:17:26 +00:00
yukkop 1954b83c7d feat: hmpl: section test passeeed 2025-04-05 21:31:35 +00:00
yukkop 77f9c7acbe refactor: change packages names 2025-04-05 19:09:13 +00:00
yukkop 6454ae65ef feat: some python packages 2025-04-05 17:04:29 +00:00
yukkop 7d746f78ca fix: c: log colorized with terminal mode 2025-04-04 14:13:54 +00:00
yukkop da4c767974 feat: c: log colorized 2025-04-04 14:07:17 +00:00
yukkop 28de673dc0 fix: hectic C: func definitions 2025-04-04 02:26:55 +00:00
yukkop 95586d02c1 feat: watch: try pager mode.. 2025-04-04 02:09:43 +00:00
yukkop 1a7f230b7c feat: some nix shit 2025-04-04 01:59:57 +00:00
yukkop 3256cd97d6 feat: watch: paterns 2025-04-03 22:57:55 +00:00
yukkop e86ac0d142 feat: watch: init 2025-04-03 22:34:50 +00:00
yukkop 75b9035178 test(hectic C): slice 2025-03-27 21:07:00 +00:00
yukkop 267bd497df refactor(hmpl): fix warnings 2025-03-24 19:27:11 +00:00
yukkop b169105ddd refactor(hectic C): fix warnings 2025-03-24 14:21:00 +00:00
yukkop 5294ad32f0 refactor(hmpl,hectic): warnings 2025-03-24 13:36:31 +00:00
yukkop 79eaa8bef0 refactor(hmpl): eval 2025-03-22 19:45:17 +00:00
yukkop 0f4591813f test(hmpl): render interpolation tags with prefix 2025-03-22 15:08:48 +00:00
yukkop 02f60509b1 test(hmpl): render interpolation tags 2025-03-22 14:56:03 +00:00
yukkop 72113fe866 fix: json eval 2025-03-22 04:15:58 +00:00
yukkop c8889170d0 fix: segfalt in json_get_object_item 2025-03-22 03:18:34 +00:00
yukkop 12ea3e88f1 feat(hectic C): json api 2025-03-22 01:22:36 +00:00
yukkop 8cda8ec6c1 refactor(hmpl): rewrite via arenas 2025-03-21 15:29:00 +00:00
yukkop 37c3fd4831 fix(hmpl): memory leak 2025-03-21 13:33:01 +00:00
yukkop bbd987f3ec feat(hmpl): render simple placeholder with memory leak of course 2025-03-21 04:00:23 +00:00
yukkop a4b43f18f8 fix(libhectic): arena allocators 2025-03-21 02:54:47 +00:00
yukkop a1a1172d98 feat(libhectic): arenas 2025-03-21 01:53:02 +00:00
yukkop cb4382ca87 test: fix: test raise generic 2025-03-20 18:47:44 +00:00
yukkop 715d4064f0 Merge branch 'master' of github.com:hectic-lab/util.nix 2025-03-20 18:16:04 +00:00
yukkop ca871022e1 test: libhectic 2025-03-20 18:15:52 +00:00
zerosummed d69566fb8b feat!: makeEnvironment: +strip prefix, ~rename to readEnvironment 2025-03-20 14:00:10 +03:00
yukkop 8e023a2cc3 feat: min C writer 2025-03-19 01:44:19 +00:00
yukkop 446fc82a0f 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 999ff2e107 feat: C writer 2025-03-19 01:09:47 +00:00
yukkop bd96f363c5 feat(c): extrude lib c 2025-03-12 23:19:45 +01:00
yukkop 493900d9ea refactor: stir some shit around again 2025-03-07 00:40:00 +00:00
yukkop d46ce1cbf6 refector(pg-neo-migration): stir some shit around trust me it's fine 2025-03-06 16:07:53 +01:00
yukkop 093c10e1d9 feat(pg-neo-migration): init 2025-03-05 19:13:00 +00:00
yukkop 88997cf96d feat(pg-schema): init 2025-03-02 10:48:52 +00:00
yukkop d8b088998d fix(pg-migration): db_url fetch 2025-02-16 17:21:02 +00:00
yukkop dfc30f946c fix(pg-migration): postgres build inputs 2025-02-16 17:03:59 +00:00
yukkop d1bb606b8e feat(pg-migration): variable providing for migrations 2025-02-16 15:58:31 +00:00
yukkop f5df53082e feat: inheritance for migration table 2025-02-16 14:21:20 +00:00
yukkop bef5ea366e feat: postgres extension pg_smtp_client 2025-02-16 10:32:37 +00:00
zerosummed f73d43451d fix: nix check errors 2025-02-14 21:27:48 +03:00
zerosummed a48bf89168 style: format (alejandra) 2025-02-14 21:06:51 +03:00
473 changed files with 46674 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
+97 -237
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;
outputs = {
self,
nixpkgs,
rust-overlay,
...
}@inputs: let
flake = ./.;
self-lib = import ./lib { inherit flake self inputs; };
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 =
# 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);
baseOverlay // legacyPackages;
# -- Cargo.toml --
cargoToml = src: (builtins.fromTOML (builtins.readFile "${src}/Cargo.toml"));
ssh.keys = {
hetzner-test = {
yukkop = ''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ8scy1tv6zfXX6xyaukhO/fsZwif5rC89DvXNc6XxOf'';
};
};
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,47 @@
{
inputs,
flake,
self,
}: {
lib,
config,
...
}: let
userNames = [
"yukkop"
"liquiz"
"vismajor"
"lvgkcfjl"
"MrAlex0O"
"Антоша"
"snuff"
];
adminNames = [ "yukkop" ];
cfg = config.hectic.generic.matrix-cluster;
in {
config = lib.mkIf cfg.enable {
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,146 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
cfg = config.hectic.hardware.hetzner-cloud;
isNewer = cfg.generation == "newer";
in {
options.hectic.hardware.hetzner-cloud = {
enable = lib.mkEnableOption "Enable hetzner-cloud hardware configurations";
generation = lib.mkOption {
type = lib.types.enum [ "classic" "newer" ];
default = "classic";
description = ''
Hetzner server generation profile.
`classic` keeps the historical `/dev/sda` assumption.
`newer` is for ccx/NVMe-era servers and defaults the disk device to
`/dev/nvme0n1`.
'';
};
#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 = if isNewer then "/dev/nvme0n1" else "/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
/dev/nvme0n1 - default for newer Hetzner generations
!! But can changes on reboot if server have volumes
!! So use IDs
'';
};
networkMatchConfigName = lib.mkOption {
type = lib.types.strMatching "^(enp1s0|ens3|eth0)$";
example = "enp1s0";
description = ''
type of network conection,
on older hetzner servers may be `ens3` or else
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.zomro;
in {
options.hectic.hardware.zomro = {
enable = lib.mkEnableOption "Enable zomro 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 zomro 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 { }
+135
View File
@@ -0,0 +1,135 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
legacyCfg = config.hectic.services.matrix;
hasClusterCfg = config.hectic ? generic && config.hectic.generic ? matrix-cluster;
clusterCfg = if hasClusterCfg then config.hectic.generic.matrix-cluster else null;
clusterSynapseEnabled =
if hasClusterCfg
then clusterCfg.enable
&& (if clusterCfg.overrideEnableSynapse != null then clusterCfg.overrideEnableSynapse else clusterCfg.role == "primary")
else false;
enabled = legacyCfg.enable || clusterSynapseEnabled;
matrixDomain = if legacyCfg.enable then legacyCfg.matrixDomain else if hasClusterCfg then clusterCfg.matrixDomain else "";
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
];
};
});
}
+56
View File
@@ -0,0 +1,56 @@
{
inputs,
flake,
self,
}:
{
pkgs,
lib,
config,
...
}: let
legacyCfg = config.hectic.services.matrix;
hasClusterCfg = config.hectic ? generic && config.hectic.generic ? matrix-cluster;
clusterCfg = if hasClusterCfg then config.hectic.generic.matrix-cluster else null;
clusterSynapseEnabled =
if hasClusterCfg
then clusterCfg.enable
&& (if clusterCfg.overrideEnableSynapse != null then clusterCfg.overrideEnableSynapse else clusterCfg.role == "primary")
else false;
enabled = legacyCfg.enable || clusterSynapseEnabled;
matrixDomain = if legacyCfg.enable then legacyCfg.matrixDomain else if hasClusterCfg then clusterCfg.matrixDomain else "";
jitsiPreferredDomain =
if legacyCfg.enable && config.hectic.services.jitsi.enable
then config.hectic.services.jitsi.hostName
else if hasClusterCfg then clusterCfg.jitsi.preferredDomain else null;
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";
mailUserNames = [
"security"
"founders"
"lvgkcfjl"
"yukkop"
"daniil-perlyk"
"iana-perlyk"
"snuff"
"antoshka"
];
mkMailPasswordSecret = name: {
name = "mailserver/${name}/hashedPassword";
value = {};
};
mkMailLoginAccount = name: {
inherit name;
value = {
hashedPasswordFile = config.sops.secrets."mailserver/${name}/hashedPassword".path;
};
};
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";
secrets = builtins.listToAttrs (map mkMailPasswordSecret mailUserNames) // {
"init-postgresql" = {
key = "init-postgresql";
};
"ssl/porkbun/${domain}/domain.cert.pem" = {
group = "nginx";
mode = "0440";
};
"ssl/porkbun/${domain}/private.key.pem" = {
group = "nginx";
mode = "0440";
};
"ssl/porkbun/${domain}/public.key.pem" = {
group = "nginx";
mode = "0440";
};
"wg-bfs/private-key" = {};
};
};
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"
];
};
services.mailserver = {
enable = true;
domain = domain;
loginAccounts = builtins.listToAttrs (map mkMailLoginAccount mailUserNames);
};
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 -"
];
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;
SSH_DOMAIN = "hectic-lab.com";
};
database = {
createDatabase = true;
type = "postgres";
socket = "/run/postgresql";
user = "gitea";
name = "gitea";
};
};
};
# === WireGuard (disabled) ===
# 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