mirror of
https://github.com/oqyude/nixos.git
synced 2026-10-08 04:57:13 +03:00
Compare commits
1
Commits
master
..
856092cc76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
856092cc76 |
@@ -1,4 +1,2 @@
|
|||||||
.vscode
|
.vscode
|
||||||
.omo
|
.omo
|
||||||
__pycache__
|
|
||||||
scripts
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
NixOS-конфиг домашнего флота. 6 NixOS-хостов + Android (`nix-on-droid`).
|
|
||||||
|
|
||||||
Этот файл — то, что агент должен прочитать **до** первого изменения. Если задача
|
|
||||||
выглядит так, что требует сломать что-то из «Подтверждённых инвариантов» или
|
|
||||||
«Ловушек» ниже — остановиться и спросить.
|
|
||||||
|
|
||||||
## Архитектура (30 секунд)
|
|
||||||
|
|
||||||
```
|
|
||||||
flake.nix
|
|
||||||
├── configurations/ ← реестр хостов (1 запись = 1 машина)
|
|
||||||
│ ├── default.nix ← hosts + xlibLib + mkSystem
|
|
||||||
│ ├── <host>.nix ← модульное тело хоста
|
|
||||||
│ └── hardware/<host>.nix
|
|
||||||
├── home/ ← home-manager (per device-type)
|
|
||||||
├── modules/
|
|
||||||
│ ├── options.nix ← кросс-модульные опции
|
|
||||||
│ ├── default.nix ← defaultModule + strictModule (для nix-on-droid)
|
|
||||||
│ ├── essentials/ ← packages, services, settings, ssh, shell, systemd-routines
|
|
||||||
│ ├── desktop/, server/, server/├── vds/, wsl/, containers/, termux/, other/
|
|
||||||
├── lib/
|
|
||||||
│ ├── mkSystem.nix ← nixosSystem + specialArgs(xlib, inputs)
|
|
||||||
│ └── xlib/ ← чистые данные: devices, dirs, helpers
|
|
||||||
├── overlays/, pkgs/, deploy/, secrets/ (sops)
|
|
||||||
└── .sops.yaml ← один age-ключ на secrets/<name>.(yaml|json|env|ini)
|
|
||||||
```
|
|
||||||
|
|
||||||
`xlib` (в `lib/xlib/`) — чистые данные: identity (`device`), capability flags,
|
|
||||||
директории, helper'ы. Передаётся в каждый модуль через `specialArgs`. Конфиг не
|
|
||||||
может переопределить `xlib` — единственная точка изменения это `configurations/default.nix`.
|
|
||||||
|
|
||||||
## Хосты
|
|
||||||
|
|
||||||
| Attr / имя | device.type | Роль | Деплой | Примечание |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `default` (nixos) | minimal | Шаблон / минималка | — | hostname `"nixos"` |
|
|
||||||
| `atoridu` | primary | Основной десктоп | — | xanmod |
|
|
||||||
| `rydiwo` | secondary | Ноутбук Chuwi MiniBook (xanmod, NTFS) | deploy-rs | `stateVersion 26.05` |
|
|
||||||
| `otrecа` | vds | VPS, SSH только по Tailscale | deploy-rs | nftables, DHCP, no firewall в NixOS |
|
|
||||||
| `sapphira` | server | Домашний сервер (белый IP через роутер) | deploy-rs | `firewall.enable = false` намеренно |
|
|
||||||
| `wsl` | wsl | WSL NixOS на vetymae | — | nixos-wsl module |
|
|
||||||
| `epral` | termux | Android (`nix-on-droid`) | — | через `mobile.nix`, отдельный модульный путь |
|
|
||||||
|
|
||||||
`device.type` ∈ { minimal, primary, secondary, server, vds, wsl, termux }.
|
|
||||||
`modules/defaultModule` импортирует `modules/<type>/` через `lib.optional
|
|
||||||
(!isDesktop && type != "minimal") (./. + "/${type}")`.
|
|
||||||
|
|
||||||
## Подтверждённые инварианты
|
|
||||||
|
|
||||||
1. **Все `outputs` флейка должны вычисляться.** `configurations/mobile.nix:12`
|
|
||||||
импортировал несуществующий `lib/xlib.nix` — был сломан, `epral` не
|
|
||||||
собирался. Зафиксировать через `nix flake check`.
|
|
||||||
2. **Носитель данных (`/home/oqyude/External`) обязан быть смонтирован** до
|
|
||||||
старта `postgresql`, `n8n`, `samba`, `homebox`, `minecraft`, `3x-ui`,
|
|
||||||
`tape-rotation`. `mkServiceStorage` даёт `bind,x-systemd.automount,nofail`
|
|
||||||
— без guard'а сервис стартует на пустой БД. → todo B1.
|
|
||||||
3. **Сетевая граница sapphira — роутер.** `firewall.enable = false` намеренно.
|
|
||||||
Роутер пробрасывает ровно 5 портов: **443, 80, 22000 (syncthing), 8443
|
|
||||||
(xray), 22 (ssh)**. `nginx.nix:225` (`allowedTCPPorts = [80 443]`) мёртв.
|
|
||||||
`openFirewall`/`allowedTCPPorts` на sapphira не имеют эффекта.
|
|
||||||
5. **`100.64.0.0` = Tailscale-адрес sapphira**, назначен вручную. Не сеть, не
|
|
||||||
ошибка. Используется в `nginx.nix`, `nextcloud.nix` (`trusted_proxies`),
|
|
||||||
`vds/systemd.nix`, `vds/nginx.nix`. При смене — править 4 файла.
|
|
||||||
4. **3x-ui заморожен.** Панель на последней версии (образ `:latest` → запинить),
|
|
||||||
ядро Xray на 26.7.x. Миграция на 26.9.x провалена. Обходные скрипты (тimer,
|
|
||||||
migrateScript) отключены осознанно. **Не** обновлять ядро через панель без
|
|
||||||
записи в `docs/arch/notes/3x-ui-xray-26.9.md`.
|
|
||||||
6. **nftables на VDS требует явной финальной политики.** Текущий ruleset
|
|
||||||
(`vds.nix:73-91`) — без явного последнего правила и без `policy` → неявный
|
|
||||||
accept. На otreca одновременно `nftables.enable = true` и `firewall.*` —
|
|
||||||
проверить, кто реально владеет ruleset'ом, перед правкой.
|
|
||||||
|
|
||||||
## Ловушки (выглядит сломанным, намеренно)
|
|
||||||
|
|
||||||
| Где | Что выглядит ошибкой | На самом деле |
|
|
||||||
|---|---|---|
|
|
||||||
| `server.nix:130` | `firewall.enable = false` при 20 сервисах на `0.0.0.0` | Роутер фильтрует, см. §4 |
|
|
||||||
| `mobile.nix:95`, `wsl.nix:59` | `stateVersion` 24.05 / 24.11 vs 26.05 | Каждый хост зафиксирован на своей версии |
|
|
||||||
| `users.nix:66` | `uid = if hostname == "sapphira" then 1001 else …` | Костыль под 1000 = удалённый `yuyus`; удалять только после миграции ФС |
|
|
||||||
| `3x-ui.nix:54` | `image = …:latest` | Панель намеренно latest; ядро Xray — на 26.7.x |
|
|
||||||
| `3x-ui.nix:33-35` | `reality443Forwarding = true` на VDS | Следствие отката `c8d4a12`; смысл утрачен, см. todo C5 |
|
|
||||||
| `server/default.nix:33-47` | 15 закомментированных модулей | Отключены осознанно, см. todo E3 |
|
|
||||||
| `opencode.nix:339` | `systemd.user.services.opencode-web.Service` | `serviceConfig` рендерится в секцию `[serviceConfig]`, systemd молча игнорирует (`c73a698`) |
|
|
||||||
| `vds.nix:73-91` | nftables без финального правила | Известный пробел, см. todo A3 |
|
|
||||||
| `100.64.0.0` | Первый адрес CGNAT `/10` | Tassigned вручную, см. §5 |
|
|
||||||
| `server.nix:61-63` | `z /mnt/services 0777` | World-writable точка монтирования; см. todo B1 |
|
|
||||||
|
|
||||||
## Куда лезть по задаче
|
|
||||||
|
|
||||||
| Задача | Файл |
|
|
||||||
|---|---|
|
|
||||||
| Добавить хост | `configurations/default.nix` + `configurations/<host>.nix` + `configurations/{hardware,disko}/<host>.nix` |
|
|
||||||
| Добавить системный сервис | `modules/server/<name>.nix`, добавить в `modules/server/default.nix:imports` |
|
|
||||||
| Добавить home-пакет для пользователя | `home/<device_type>.nix` (через `lib.mkIf` или просто список) |
|
|
||||||
| Добавить опцию, читаемую несколькими модулями | `modules/options.nix` |
|
|
||||||
| Изменить mount/имя пользователя | `lib/xlib/dirs.nix`, `lib/xlib/device.nix` |
|
|
||||||
| Изменить домен / сертификат | `modules/server/coredns.nix` + `modules/server/nginx.nix` (или `vds/`) |
|
|
||||||
| Sops-секрет | положить в `secrets/<name>.<yaml|json|env|ini>`; `users.nix:99` уже подключает `secrets/default.yaml`; dotenv/json-секреты — через `mkUserSecret` |
|
|
||||||
|
|
||||||
## Проверки
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# все outputs вычисляются
|
|
||||||
nix flake check
|
|
||||||
|
|
||||||
# правки применились на целевой хост
|
|
||||||
nix build .#nixosConfigurations.<host>.config.system.build.toplevel
|
|
||||||
|
|
||||||
# nixOnDroid
|
|
||||||
nix build .#nixOnDroidConfigurations.epral.config.system.build.toplevel
|
|
||||||
|
|
||||||
# внешний диск смонтирован (до рестарта сервисов на нём)
|
|
||||||
findmnt /home/oqyude/External
|
|
||||||
findmnt /mnt/services
|
|
||||||
|
|
||||||
# state of guard-зависимостей (когда будет todo B1)
|
|
||||||
systemctl show postgresql -p Requires -p After | tr ' ' '\n' | grep -E 'mnt-|home-oqyude'
|
|
||||||
|
|
||||||
# sops
|
|
||||||
sops --version
|
|
||||||
```
|
|
||||||
|
|
||||||
## Где НЕ лезть без ответа владельца
|
|
||||||
|
|
||||||
- `secrets/` (sops-encrypted, расшифровываются `/etc/ssh/id_ed25519` → циклический bootstrap).
|
|
||||||
- `lдet deploy` без проверки deploy-rs нод: `rydiwo` (ноутбук, может быть выключен).
|
|
||||||
- Любая правка, противоречащая «Подтверждённым инвариантам» выше.
|
|
||||||
|
|
||||||
## Дальше читать
|
|
||||||
|
|
||||||
- `docs/arch/map.md` — полная карта: per-host детали, сетевая топология,
|
|
||||||
инвентарь сервисов, все известные open questions.
|
|
||||||
- `docs/arch/invariants.md` — слои 9–11 (home-manager, deploy, формат) +
|
|
||||||
полный список неотвеченных вопросов слоёв 1–8.
|
|
||||||
- `docs/arch/todo.md` — задачи A1–F (правки и документирование).
|
|
||||||
+28
-13
@@ -1,15 +1,30 @@
|
|||||||
# Host: "default" (device: minimal)
|
{ inputs, ... }@flakeContext:
|
||||||
#
|
let
|
||||||
# The host record lives in configurations/default.nix; this file is only the
|
nixosModule =
|
||||||
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument.
|
{
|
||||||
{
|
config,
|
||||||
inputs,
|
lib,
|
||||||
...
|
modulesPath,
|
||||||
}:
|
pkgs,
|
||||||
{
|
xlib,
|
||||||
imports = [
|
...
|
||||||
inputs.self.nixosModules.default
|
}:
|
||||||
];
|
{
|
||||||
|
imports = [
|
||||||
|
inputs.self.nixosModules.default
|
||||||
|
];
|
||||||
|
|
||||||
system.stateVersion = "26.05";
|
system = {
|
||||||
|
stateVersion = "26.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
inputs.nixpkgs.lib.nixosSystem {
|
||||||
|
modules = [
|
||||||
|
nixosModule
|
||||||
|
];
|
||||||
|
system = "x86_64-linux";
|
||||||
|
specialArgs = {
|
||||||
|
deviceType = "minimal";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,12 @@
|
|||||||
{ inputs, ... }@flakeContext:
|
{ inputs, ... }@flakeContext:
|
||||||
let
|
|
||||||
lib = inputs.nixpkgs.lib;
|
|
||||||
mkSystem = import ../lib/mkSystem.nix flakeContext;
|
|
||||||
xlibLib = import ../lib/xlib { inherit lib; };
|
|
||||||
|
|
||||||
# One record per host. The attribute name IS the hostname, so it is written
|
|
||||||
# exactly once; `hostname` is only needed where the attribute name is not
|
|
||||||
# the real hostname (the `default` entry).
|
|
||||||
#
|
|
||||||
# device device type, must be a key of `devices` in lib/xlib/device.nix
|
|
||||||
# modules module body for this host
|
|
||||||
hosts = {
|
|
||||||
default = {
|
|
||||||
hostname = "nixos";
|
|
||||||
device = "minimal";
|
|
||||||
modules = [ ./any.nix ];
|
|
||||||
};
|
|
||||||
atoridu = {
|
|
||||||
device = "primary";
|
|
||||||
modules = [ ./mini-pc.nix ];
|
|
||||||
};
|
|
||||||
rydiwo = {
|
|
||||||
device = "secondary";
|
|
||||||
modules = [ ./mini-laptop.nix ];
|
|
||||||
};
|
|
||||||
otreca = {
|
|
||||||
device = "vds";
|
|
||||||
modules = [ ./vds.nix ];
|
|
||||||
};
|
|
||||||
sapphira = {
|
|
||||||
device = "server";
|
|
||||||
modules = [ ./server.nix ];
|
|
||||||
};
|
|
||||||
wsl = {
|
|
||||||
device = "wsl";
|
|
||||||
modules = [ ./wsl.nix ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkHost =
|
|
||||||
name:
|
|
||||||
{
|
|
||||||
device,
|
|
||||||
modules,
|
|
||||||
hostname ? name,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
xlib = xlibLib.mkXlib {
|
|
||||||
inherit hostname;
|
|
||||||
type = device;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit xlib;
|
|
||||||
system = mkSystem { inherit xlib modules; };
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
nixosConfigurations = lib.mapAttrs' (
|
nixosConfigurations = {
|
||||||
name: spec: lib.nameValuePair name (mkHost name spec).system
|
default = import ./any.nix flakeContext; # default
|
||||||
) hosts;
|
atoridu = import ./mini-pc.nix flakeContext; # atoridu
|
||||||
|
rydiwo = import ./mini-laptop.nix flakeContext; # rydiwo
|
||||||
# Per-host xlib values, for code that lives outside the module system
|
otreca = import ./vds.nix flakeContext; # vds
|
||||||
# (deploy, overlays, pkgs).
|
otreca-new = import ./vds-new.nix flakeContext; # vds-new
|
||||||
xlib = lib.mapAttrs' (name: spec: lib.nameValuePair name (mkHost name spec).xlib) hosts;
|
sapphira = import ./server.nix flakeContext; # sapphira
|
||||||
|
wsl = import ./wsl.nix flakeContext; # wsl
|
||||||
nixOnDroidConfigurations = {
|
|
||||||
epral = import ./mobile.nix flakeContext; # epral (Android device via nix-on-droid)
|
|
||||||
# Alias so a plain `nix-on-droid switch` from a local clone
|
|
||||||
# (~/.config/nix-on-droid) picks up the device config without `#epral`.
|
|
||||||
default = import ./mobile.nix flakeContext;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
kernel = {
|
kernel = {
|
||||||
sysctl = {
|
sysctl = {
|
||||||
"fs.inotify.max_user_watches" = "204800";
|
"fs.inotify.max_user_watches" = "204800";
|
||||||
"net.ipv4.ip_forward" = 1;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
kernelModules = [
|
kernelModules = [
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
modulesPath,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
fileSystems = {
|
||||||
|
"/" = {
|
||||||
|
device = lib.mkForce "/dev/disk/by-partlabel/disk-main-root"; # "/dev/disk/by-partlabel/disk-main-root";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# swapDevices = [
|
||||||
|
# { device = "/dev/disk/by-partlabel/disk-main-swap"; }
|
||||||
|
# ];
|
||||||
|
|
||||||
|
networking.useDHCP = lib.mkDefault true;
|
||||||
|
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
|
||||||
|
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
|
||||||
|
}
|
||||||
+136
-30
@@ -1,37 +1,143 @@
|
|||||||
# Host: "rydiwo" (device: secondary)
|
|
||||||
#
|
|
||||||
# The host record lives in configurations/default.nix; this file is only the
|
|
||||||
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument.
|
|
||||||
{
|
{
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
xlib,
|
|
||||||
inputs,
|
inputs,
|
||||||
...
|
...
|
||||||
}:
|
}@flakeContext:
|
||||||
{
|
let
|
||||||
imports = with inputs; [
|
nixosModule =
|
||||||
nixos-hardware.nixosModules.chuwi-minibook-x
|
{
|
||||||
./hardware/mini-laptop.nix
|
config,
|
||||||
self.nixosModules.default
|
lib,
|
||||||
];
|
pkgs,
|
||||||
|
xlib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
xlib.device = {
|
||||||
|
type = "secondary";
|
||||||
|
hostname = "rydiwo";
|
||||||
|
};
|
||||||
|
|
||||||
boot = {
|
imports = with inputs; [
|
||||||
kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable;
|
nixos-hardware.nixosModules.chuwi-minibook-x
|
||||||
loader = {
|
./hardware/mini-laptop.nix
|
||||||
systemd-boot.enable = lib.mkDefault true;
|
self.nixosModules.default
|
||||||
efi.canTouchEfiVariables = lib.mkDefault true;
|
];
|
||||||
|
|
||||||
|
boot = {
|
||||||
|
kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable;
|
||||||
|
loader = {
|
||||||
|
systemd-boot.enable = lib.mkDefault true;
|
||||||
|
efi.canTouchEfiVariables = lib.mkDefault true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems."${xlib.dirs.lamet-drive}" = {
|
||||||
|
device = "/dev/disk/by-uuid/DC76BD3576BD116E";
|
||||||
|
fsType = "ntfs3";
|
||||||
|
options = [
|
||||||
|
"defaults"
|
||||||
|
"uid=1000"
|
||||||
|
"gid=1000"
|
||||||
|
"fmask=0000"
|
||||||
|
"dmask=0000"
|
||||||
|
"nofail"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
hardware = {
|
||||||
|
bluetooth.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
hostName = "${xlib.device.hostname}";
|
||||||
|
networkmanager.enable = true;
|
||||||
|
firewall.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n = {
|
||||||
|
extraLocaleSettings = {
|
||||||
|
LC_ADDRESS = "ru_RU.UTF-8";
|
||||||
|
LC_IDENTIFICATION = "ru_RU.UTF-8";
|
||||||
|
LC_MEASUREMENT = "ru_RU.UTF-8";
|
||||||
|
LC_MONETARY = "ru_RU.UTF-8";
|
||||||
|
LC_NAME = "ru_RU.UTF-8";
|
||||||
|
LC_NUMERIC = "ru_RU.UTF-8";
|
||||||
|
LC_PAPER = "ru_RU.UTF-8";
|
||||||
|
LC_TELEPHONE = "ru_RU.UTF-8";
|
||||||
|
LC_TIME = "ru_RU.UTF-8";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
# xserver = {
|
||||||
|
# videoDrivers = [
|
||||||
|
# "nomodeset"
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
|
syncthing = {
|
||||||
|
enable = true;
|
||||||
|
systemService = true;
|
||||||
|
configDir = "${xlib.dirs.user-storage}/Syncthing/${config.system.name}";
|
||||||
|
dataDir = "${xlib.dirs.user-home}";
|
||||||
|
group = "users";
|
||||||
|
user = "${xlib.device.username}";
|
||||||
|
};
|
||||||
|
# pipewire = {
|
||||||
|
# enable = lib.mkDefault true;
|
||||||
|
# systemWide = true;
|
||||||
|
# alsa.enable = false;
|
||||||
|
# alsa.support32Bit = true;
|
||||||
|
# pulse.enable = true;
|
||||||
|
# jack.enable = true;
|
||||||
|
# extraConfig.pipewire = {
|
||||||
|
# "99-default.conf" = {
|
||||||
|
# "context.properties" = {
|
||||||
|
# "default.clock.rate" = 96000;
|
||||||
|
# "default.clock.allowed-rates" = [
|
||||||
|
# 44100
|
||||||
|
# 48000
|
||||||
|
# 96000
|
||||||
|
# ];
|
||||||
|
# "default.clock.quantum" = 1024;
|
||||||
|
# "default.clock.min-quantum" = 256;
|
||||||
|
# "default.clock.max-quantum" = 2048;
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
thermald.enable = true;
|
||||||
|
earlyoom.enable = true;
|
||||||
|
openssh = {
|
||||||
|
enable = true;
|
||||||
|
allowSFTP = true;
|
||||||
|
hostKeys = [
|
||||||
|
{
|
||||||
|
path = "/etc/ssh/id_ed25519";
|
||||||
|
type = "ed25519";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
settings = {
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
PermitRootLogin = "yes";
|
||||||
|
UsePAM = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
security = {
|
||||||
|
rtkit.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
hardware.intel-gpu-tools.enable = true;
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
};
|
};
|
||||||
|
in
|
||||||
|
inputs.nixpkgs.lib.nixosSystem {
|
||||||
|
modules = with inputs; [
|
||||||
|
nixosModule
|
||||||
|
];
|
||||||
|
system = "x86_64-linux";
|
||||||
|
specialArgs = {
|
||||||
|
deviceType = "secondary";
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems = xlib.helpers.mkNtfsMount {
|
|
||||||
path = xlib.dirs.lamet-drive;
|
|
||||||
uuid = "DC76BD3576BD116E";
|
|
||||||
mask = "0000";
|
|
||||||
};
|
|
||||||
|
|
||||||
host.ssh.enable = true;
|
|
||||||
hardware.intel-gpu-tools.enable = true;
|
|
||||||
|
|
||||||
system.stateVersion = "26.05";
|
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-74
@@ -1,84 +1,163 @@
|
|||||||
# Host: "atoridu" (device: primary)
|
|
||||||
#
|
|
||||||
# The host record lives in configurations/default.nix; this file is only the
|
|
||||||
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument.
|
|
||||||
{
|
{
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
xlib,
|
|
||||||
inputs,
|
inputs,
|
||||||
...
|
...
|
||||||
}:
|
}@flakeContext:
|
||||||
{
|
let
|
||||||
imports = with inputs; [
|
nixosModule =
|
||||||
./hardware/mini-pc.nix
|
{
|
||||||
./disko/mini-pc.nix
|
config,
|
||||||
./hardware/logitech.nix
|
lib,
|
||||||
self.nixosModules.default
|
pkgs,
|
||||||
];
|
xlib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
xlib.device = {
|
||||||
|
type = "primary";
|
||||||
|
hostname = "atoridu";
|
||||||
|
};
|
||||||
|
|
||||||
# mkNtfsMount returns a `{ "<path>" = { ... }; }` attrset (the shape
|
imports = with inputs; [
|
||||||
# fileSystems itself wants), so several mounts are combined with
|
./hardware/mini-pc.nix
|
||||||
# mergeAttrsList — not listToAttrs, which would demand `name`/`value`.
|
./disko/mini-pc.nix
|
||||||
#
|
./hardware/logitech.nix
|
||||||
# These three ntfs3 drives are intentionally left unmounted. The entries
|
self.nixosModules.default
|
||||||
# are kept commented out rather than deleted, so restoring a drive is a
|
];
|
||||||
# matter of uncommenting its block. `enable = false` would declare a drive
|
|
||||||
# without mounting it; dropping the field mounts it.
|
|
||||||
fileSystems = lib.mergeAttrsList (
|
|
||||||
map (xlib.helpers.mkNtfsMount) [
|
|
||||||
# {
|
|
||||||
# path = xlib.dirs.therima-drive;
|
|
||||||
# uuid = "C0A2DDEFA2DDEA44";
|
|
||||||
# }
|
|
||||||
# {
|
|
||||||
# path = xlib.dirs.vetymae-drive;
|
|
||||||
# uuid = "6408433908430A0E";
|
|
||||||
# }
|
|
||||||
# {
|
|
||||||
# path = xlib.dirs.soptur-drive;
|
|
||||||
# uuid = "C00C56E40C56D54E";
|
|
||||||
# }
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
boot = {
|
fileSystems = {
|
||||||
kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable;
|
"${xlib.dirs.therima-drive}" = {
|
||||||
loader = {
|
enable = false;
|
||||||
systemd-boot.enable = lib.mkDefault true;
|
device = "/dev/disk/by-uuid/C0A2DDEFA2DDEA44";
|
||||||
efi.canTouchEfiVariables = lib.mkDefault true;
|
fsType = "ntfs3";
|
||||||
};
|
options = [
|
||||||
};
|
"defaults"
|
||||||
|
"uid=1000"
|
||||||
services.xserver = {
|
"gid=1000"
|
||||||
videoDrivers = [
|
"fmask=0007"
|
||||||
"amdgpu"
|
"dmask=0007"
|
||||||
];
|
"nofail"
|
||||||
};
|
];
|
||||||
services.pipewire = {
|
};
|
||||||
enable = lib.mkDefault true;
|
"${xlib.dirs.vetymae-drive}" = {
|
||||||
systemWide = true;
|
enable = false;
|
||||||
alsa.enable = false;
|
device = "/dev/disk/by-uuid/6408433908430A0E";
|
||||||
alsa.support32Bit = true;
|
fsType = "ntfs3";
|
||||||
pulse.enable = true;
|
options = [
|
||||||
jack.enable = true;
|
"defaults"
|
||||||
extraConfig.pipewire = {
|
"uid=1000"
|
||||||
"99-default.conf" = {
|
"gid=1000"
|
||||||
"context.properties" = {
|
"fmask=0007"
|
||||||
"default.clock.rate" = 96000;
|
"dmask=0007"
|
||||||
"default.clock.allowed-rates" = [
|
"nofail"
|
||||||
44100
|
];
|
||||||
48000
|
};
|
||||||
96000
|
"${xlib.dirs.soptur-drive}" = {
|
||||||
|
enable = false;
|
||||||
|
device = "/dev/disk/by-uuid/C00C56E40C56D54E";
|
||||||
|
fsType = "ntfs3";
|
||||||
|
options = [
|
||||||
|
"defaults"
|
||||||
|
"uid=1000"
|
||||||
|
"gid=1000"
|
||||||
|
"fmask=0007"
|
||||||
|
"dmask=0007"
|
||||||
|
"nofail"
|
||||||
];
|
];
|
||||||
"default.clock.quantum" = 1024;
|
|
||||||
"default.clock.min-quantum" = 256;
|
|
||||||
"default.clock.max-quantum" = 2048;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
};
|
|
||||||
nixpkgs.config.pulseaudio = true;
|
|
||||||
|
|
||||||
system.stateVersion = "26.05";
|
boot = {
|
||||||
|
kernelPackages = lib.mkDefault pkgs.linuxPackages_xanmod_stable;
|
||||||
|
#kernelParams = [ "usbcore.autosuspend=-1" ];
|
||||||
|
loader = {
|
||||||
|
systemd-boot.enable = lib.mkDefault true;
|
||||||
|
efi.canTouchEfiVariables = lib.mkDefault true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
hardware = {
|
||||||
|
bluetooth.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# networking.firewall.allowedTCPPorts = [ ... ];
|
||||||
|
# networking.firewall.allowedUDPPorts = [ ... ];
|
||||||
|
networking = {
|
||||||
|
hostName = "${xlib.device.hostname}";
|
||||||
|
networkmanager.enable = true;
|
||||||
|
firewall.enable = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n = {
|
||||||
|
extraLocaleSettings = {
|
||||||
|
LC_ADDRESS = "ru_RU.UTF-8";
|
||||||
|
LC_IDENTIFICATION = "ru_RU.UTF-8";
|
||||||
|
LC_MEASUREMENT = "ru_RU.UTF-8";
|
||||||
|
LC_MONETARY = "ru_RU.UTF-8";
|
||||||
|
LC_NAME = "ru_RU.UTF-8";
|
||||||
|
LC_NUMERIC = "ru_RU.UTF-8";
|
||||||
|
LC_PAPER = "ru_RU.UTF-8";
|
||||||
|
LC_TELEPHONE = "ru_RU.UTF-8";
|
||||||
|
LC_TIME = "ru_RU.UTF-8";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
#logrotate.checkConfig = false;
|
||||||
|
#power-profiles-daemon.enable = false;
|
||||||
|
xserver = {
|
||||||
|
videoDrivers = [
|
||||||
|
"amdgpu"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
syncthing = {
|
||||||
|
enable = true;
|
||||||
|
systemService = true;
|
||||||
|
configDir = "${xlib.dirs.user-storage}/Syncthing/${config.system.name}";
|
||||||
|
dataDir = "${xlib.dirs.user-home}";
|
||||||
|
group = "users";
|
||||||
|
user = "${xlib.device.username}";
|
||||||
|
};
|
||||||
|
pipewire = {
|
||||||
|
enable = lib.mkDefault true;
|
||||||
|
systemWide = true;
|
||||||
|
alsa.enable = false;
|
||||||
|
alsa.support32Bit = true;
|
||||||
|
pulse.enable = true;
|
||||||
|
jack.enable = true;
|
||||||
|
extraConfig.pipewire = {
|
||||||
|
"99-default.conf" = {
|
||||||
|
"context.properties" = {
|
||||||
|
"default.clock.rate" = 96000;
|
||||||
|
"default.clock.allowed-rates" = [
|
||||||
|
44100
|
||||||
|
48000
|
||||||
|
96000
|
||||||
|
];
|
||||||
|
"default.clock.quantum" = 1024;
|
||||||
|
"default.clock.min-quantum" = 256;
|
||||||
|
"default.clock.max-quantum" = 2048;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
thermald.enable = true;
|
||||||
|
earlyoom.enable = true;
|
||||||
|
};
|
||||||
|
nixpkgs.config.pulseaudio = true;
|
||||||
|
|
||||||
|
security = {
|
||||||
|
rtkit.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "26.05";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
inputs.nixpkgs.lib.nixosSystem {
|
||||||
|
modules = [
|
||||||
|
nixosModule
|
||||||
|
];
|
||||||
|
system = "x86_64-linux";
|
||||||
|
specialArgs = {
|
||||||
|
deviceType = "primary";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-131
@@ -1,139 +1,132 @@
|
|||||||
# Host: "sapphira" (device: server)
|
{ inputs, ... }@flakeContext:
|
||||||
#
|
let
|
||||||
# The host record lives in configurations/default.nix; this file is only the
|
nixosModule =
|
||||||
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument.
|
{
|
||||||
{
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
xlib,
|
xlib,
|
||||||
inputs,
|
...
|
||||||
...
|
}:
|
||||||
}:
|
{
|
||||||
{
|
xlib.device = {
|
||||||
imports = [
|
type = "server";
|
||||||
./hardware/server.nix
|
hostname = "sapphira";
|
||||||
inputs.self.nixosModules.default
|
};
|
||||||
];
|
|
||||||
|
|
||||||
boot = {
|
imports = [
|
||||||
# kernelPackages = pkgs.linuxPackages_xanmod_stable;
|
./hardware/server.nix
|
||||||
hardwareScan = true;
|
inputs.self.nixosModules.default
|
||||||
loader = {
|
|
||||||
systemd-boot.enable = lib.mkDefault true;
|
|
||||||
efi.canTouchEfiVariables = lib.mkDefault true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
hardware = {
|
|
||||||
bluetooth.enable = true;
|
|
||||||
graphics = {
|
|
||||||
enable = true;
|
|
||||||
extraPackages = with pkgs; [
|
|
||||||
intel-media-driver
|
|
||||||
intel-ocl
|
|
||||||
intel-vaapi-driver
|
|
||||||
];
|
];
|
||||||
};
|
|
||||||
intel-gpu-tools.enable = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
fileSystems =
|
boot = {
|
||||||
(xlib.helpers.mkExfatMount {
|
# kernelPackages = pkgs.linuxPackages_xanmod_stable;
|
||||||
path = xlib.dirs.archive-drive;
|
hardwareScan = true;
|
||||||
label = "archive";
|
loader = {
|
||||||
})
|
systemd-boot.enable = lib.mkDefault true;
|
||||||
// (xlib.helpers.mkExfatMount {
|
efi.canTouchEfiVariables = lib.mkDefault true;
|
||||||
path = xlib.dirs.mobile-drive;
|
};
|
||||||
uuid = "7EB1-DC99";
|
};
|
||||||
})
|
|
||||||
// (xlib.helpers.mkBindMount {
|
hardware = {
|
||||||
what = xlib.dirs.services-folder;
|
bluetooth.enable = true;
|
||||||
where = xlib.dirs.services-mnt-folder;
|
graphics = {
|
||||||
})
|
enable = true;
|
||||||
// {
|
extraPackages = with pkgs; [
|
||||||
# External drive
|
intel-media-driver
|
||||||
"${xlib.dirs.server-home}" = {
|
intel-ocl
|
||||||
device = "/dev/disk/by-uuid/37e53ebc-5343-a94d-9fe2-0ca39e13a8de";
|
intel-vaapi-driver
|
||||||
fsType = "ext4";
|
];
|
||||||
|
};
|
||||||
|
intel-gpu-tools.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
fileSystems = {
|
||||||
|
# External drive
|
||||||
|
"${xlib.dirs.server-home}" = {
|
||||||
|
device = "/dev/disk/by-uuid/37e53ebc-5343-a94d-9fe2-0ca39e13a8de";
|
||||||
|
fsType = "ext4";
|
||||||
|
};
|
||||||
|
# Archive drive
|
||||||
|
"${xlib.dirs.archive-drive}" = {
|
||||||
|
device = "/dev/disk/by-label/archive";
|
||||||
|
fsType = "exfat";
|
||||||
|
options = [
|
||||||
|
"nofail"
|
||||||
|
"uid=1000"
|
||||||
|
"gid=1000"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
# Mobile SD-Card
|
||||||
|
"${xlib.dirs.mobile-drive}" = {
|
||||||
|
device = "/dev/disk/by-uuid/7EB1-DC99";
|
||||||
|
fsType = "exfat";
|
||||||
|
options = [
|
||||||
|
"nofail"
|
||||||
|
"uid=1000"
|
||||||
|
"gid=1000"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
# Services in /mnt folder
|
||||||
|
"${xlib.dirs.services-mnt-folder}" = {
|
||||||
|
device = "${xlib.dirs.services-folder}";
|
||||||
|
fsType = "none";
|
||||||
|
options = [
|
||||||
|
"bind"
|
||||||
|
"nofail"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"z ${xlib.dirs.services-mnt-folder} 0777 root root -"
|
||||||
|
];
|
||||||
|
|
||||||
|
services = {
|
||||||
|
earlyoom.enable = true;
|
||||||
|
journald = {
|
||||||
|
extraConfig = ''
|
||||||
|
SystemMaxUse=512M
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
openssh = {
|
||||||
|
enable = true;
|
||||||
|
allowSFTP = true;
|
||||||
|
hostKeys = [
|
||||||
|
{
|
||||||
|
path = "/etc/ssh/id_ed25519";
|
||||||
|
type = "ed25519";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
settings = {
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
PermitRootLogin = "yes";
|
||||||
|
UsePAM = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
hostName = "${xlib.device.hostname}";
|
||||||
|
networkmanager.enable = true;
|
||||||
|
firewall.enable = false;
|
||||||
|
nameservers = [
|
||||||
|
"127.0.0.1"
|
||||||
|
"192.168.1.1"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
system = {
|
||||||
|
stateVersion = "25.05";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
in
|
||||||
systemd.tmpfiles.rules = [
|
inputs.nixpkgs.lib.nixosSystem {
|
||||||
"z ${xlib.dirs.services-mnt-folder} 0777 root root -"
|
modules = [
|
||||||
|
nixosModule
|
||||||
];
|
];
|
||||||
|
system = "x86_64-linux";
|
||||||
host.ssh.enable = true;
|
specialArgs = {
|
||||||
|
deviceType = "server";
|
||||||
# Offload Nix builds to the WSL2 NixOS instance running on vetymae
|
|
||||||
# (Windows 11 host). Sapphira only has 2 logical cores; the WSL exposes
|
|
||||||
# 24 cores + 14 GiB. The matchBlock with ProxyCommand is generated by
|
|
||||||
# modules/server/builder.nix, the other side of the same option lives in
|
|
||||||
# modules/wsl/builder.nix.
|
|
||||||
#
|
|
||||||
# `proxyCommand` is what marks this builder as needing the SSH matchBlock
|
|
||||||
# (see modules/server/builder.nix). A builder reachable directly would
|
|
||||||
# omit it.
|
|
||||||
#
|
|
||||||
# ---- DISABLED 2026-10-04 ----
|
|
||||||
# Remote building temporarily turned off coordinator-side. `host.builder.clients`
|
|
||||||
# falls back to its default `[]` (declared in modules/options.nix), so
|
|
||||||
# modules/server/builder.nix's `lib.mkIf (clients != [])` never fires and no
|
|
||||||
# buildMachines / SSH blocks / distributedBuilds override get generated.
|
|
||||||
# All builds run locally on sapphira's 2 cores. Re-enable by removing the
|
|
||||||
# Nix comments on the block below (and on `host.builder.enable = true;`
|
|
||||||
# in configurations/wsl.nix).
|
|
||||||
#
|
|
||||||
# host.builder.clients = [
|
|
||||||
# {
|
|
||||||
# hostName = "vetymae-nix";
|
|
||||||
# sshUser = "oqyude";
|
|
||||||
# sshKey = "/root/.ssh/id_ed25519";
|
|
||||||
# # NixOS calls this `systems` (plural), not `systemTypes`. The
|
|
||||||
# # default is empty — every derivation is rejected. The WSL NixOS
|
|
||||||
# # runs on x86_64-linux, matching sapphira.
|
|
||||||
# systems = [ "x86_64-linux" ];
|
|
||||||
# # vetymae-nix drops kvm + nixos-test from its advertised
|
|
||||||
# # system-features (see modules/wsl/builder.nix). Listing them here
|
|
||||||
# # would not break anything (Nix intersects), but listing the
|
|
||||||
# # features the WSL actually has is the documented contract.
|
|
||||||
# supportedFeatures = [
|
|
||||||
# "benchmark"
|
|
||||||
# "big-parallel"
|
|
||||||
# ];
|
|
||||||
# mandatoryFeatures = [ ];
|
|
||||||
# maxJobs = 24;
|
|
||||||
# speedFactor = 0.5;
|
|
||||||
# # Keep the SSH session alive across many small builds in one daemon
|
|
||||||
# # session — compile-heavy workloads spam the daemon with hundreds of
|
|
||||||
# # derivations and ControlMaster collapses those into one Windows hop.
|
|
||||||
# # NB: `nix.buildMachines` has no `sshOptions` attribute, so the
|
|
||||||
# # ControlMaster directive lives in the SSH matchBlock instead (see
|
|
||||||
# # modules/server/builder.nix).
|
|
||||||
# #
|
|
||||||
# # The OpenSSH alias for this host (matches the user's
|
|
||||||
# # ~/.ssh/config so known_hosts entries do not collide with the
|
|
||||||
# # Windows OpenSSH entry on 127.0.0.1/vetymae) is consumed only by
|
|
||||||
# # the SSH matchBlock below — not by `nix.buildMachines`, which has
|
|
||||||
# # no such attribute.
|
|
||||||
# hostKeyAlias = "wsl-nixos-on-vetymae";
|
|
||||||
# # Use the Windows host's IP directly so the nix-daemon (running as
|
|
||||||
# # root, without the user's ~/.ssh/config) does not need a separate
|
|
||||||
# # `vetymae` host alias. With StrictHostKeyChecking=accept-new the
|
|
||||||
# # first connection adds the Windows host key to /root/.ssh/known_hosts.
|
|
||||||
# proxyCommand = "ssh oqyude@192.168.1.100 'wsl -d NixOS -- nc 127.0.0.1 22'";
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
|
|
||||||
networking = {
|
|
||||||
networkmanager.enable = true;
|
|
||||||
firewall.enable = false;
|
|
||||||
# nameservers = [
|
|
||||||
# "192.168.1.1"
|
|
||||||
# "127.0.0.1"
|
|
||||||
# ];
|
|
||||||
};
|
|
||||||
|
|
||||||
system = {
|
|
||||||
stateVersion = "25.05";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
{ inputs, ... }@flakeContext:
|
||||||
|
let
|
||||||
|
nixosModule =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
modulesPath,
|
||||||
|
pkgs,
|
||||||
|
xlib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
xlib.device = {
|
||||||
|
type = "vds-new";
|
||||||
|
hostname = "otreca-new";
|
||||||
|
};
|
||||||
|
|
||||||
|
imports = [
|
||||||
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
(modulesPath + "/profiles/qemu-guest.nix")
|
||||||
|
|
||||||
|
./disko/vds.nix
|
||||||
|
./hardware/vds.nix
|
||||||
|
|
||||||
|
inputs.self.nixosModules.default
|
||||||
|
];
|
||||||
|
|
||||||
|
boot = {
|
||||||
|
kernelPackages = pkgs.linuxPackages_xanmod_stable;
|
||||||
|
hardwareScan = true;
|
||||||
|
loader = {
|
||||||
|
grub = {
|
||||||
|
enable = true;
|
||||||
|
device = "nodev";
|
||||||
|
useOSProber = false;
|
||||||
|
efiSupport = false;
|
||||||
|
};
|
||||||
|
systemd-boot.enable = lib.mkDefault false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
earlyoom.enable = true;
|
||||||
|
journald = {
|
||||||
|
extraConfig = ''
|
||||||
|
SystemMaxUse=512M
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
openssh = {
|
||||||
|
enable = true;
|
||||||
|
allowSFTP = true;
|
||||||
|
openFirewall = true;
|
||||||
|
hostKeys = [
|
||||||
|
{
|
||||||
|
path = "/etc/ssh/id_ed25519";
|
||||||
|
type = "ed25519";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
settings = {
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
PermitRootLogin = "yes";
|
||||||
|
UsePAM = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
tailscale = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
nameservers = [
|
||||||
|
"1.1.1.1"
|
||||||
|
"8.8.8.8"
|
||||||
|
"2001:4860:4860::8844"
|
||||||
|
"2001:4860:4860::8888"
|
||||||
|
"2606:4700:4700::1111"
|
||||||
|
"2606:4700:4700::1001"
|
||||||
|
];
|
||||||
|
hostName = "${xlib.device.hostname}";
|
||||||
|
networkmanager.enable = true;
|
||||||
|
tempAddresses = "disabled";
|
||||||
|
dhcpcd = {
|
||||||
|
enable = true;
|
||||||
|
IPv6rs = true;
|
||||||
|
};
|
||||||
|
firewall = {
|
||||||
|
enable = true;
|
||||||
|
allowPing = true;
|
||||||
|
};
|
||||||
|
enableIPv6 = true;
|
||||||
|
interfaces.ens3 = {
|
||||||
|
useDHCP = true;
|
||||||
|
# ipv4.addresses = [
|
||||||
|
# {
|
||||||
|
# address = "31.57.158.109";
|
||||||
|
# prefixLength = 24;
|
||||||
|
# }
|
||||||
|
# ];
|
||||||
|
# ipv6.addresses = [
|
||||||
|
# {
|
||||||
|
# address = "2a13:7c00:6:102:f816:3eff:fe91:6b9e";
|
||||||
|
# prefixLength = 64;
|
||||||
|
# }
|
||||||
|
# ];
|
||||||
|
};
|
||||||
|
# defaultGateway = {
|
||||||
|
# address = "31.57.158.1";
|
||||||
|
# interface = "ens3";
|
||||||
|
# };
|
||||||
|
# defaultGateway6 = {
|
||||||
|
# address = "2a13:7c00:6:102::1";
|
||||||
|
# interface = "ens3";
|
||||||
|
# };
|
||||||
|
};
|
||||||
|
|
||||||
|
system = {
|
||||||
|
stateVersion = "25.05";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
inputs.nixpkgs.lib.nixosSystem {
|
||||||
|
modules = [
|
||||||
|
nixosModule
|
||||||
|
];
|
||||||
|
system = "x86_64-linux";
|
||||||
|
specialArgs = {
|
||||||
|
deviceType = "vds-new";
|
||||||
|
};
|
||||||
|
}
|
||||||
+153
-118
@@ -1,122 +1,157 @@
|
|||||||
# Host: "otreca" (device: vds)
|
{ inputs, ... }@flakeContext:
|
||||||
#
|
let
|
||||||
# The host record lives in configurations/default.nix; this file is only the
|
nixosModule =
|
||||||
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument.
|
{
|
||||||
{
|
config,
|
||||||
lib,
|
lib,
|
||||||
modulesPath,
|
modulesPath,
|
||||||
pkgs,
|
pkgs,
|
||||||
xlib,
|
xlib,
|
||||||
inputs,
|
...
|
||||||
...
|
}:
|
||||||
}:
|
{
|
||||||
{
|
xlib.device = {
|
||||||
imports = [
|
type = "vds";
|
||||||
(modulesPath + "/installer/scan/not-detected.nix")
|
hostname = "otreca";
|
||||||
(modulesPath + "/profiles/qemu-guest.nix")
|
};
|
||||||
|
|
||||||
./disko/vds.nix
|
imports = [
|
||||||
./hardware/vds.nix
|
(modulesPath + "/installer/scan/not-detected.nix")
|
||||||
|
(modulesPath + "/profiles/qemu-guest.nix")
|
||||||
inputs.self.nixosModules.default
|
|
||||||
];
|
./disko/vds.nix
|
||||||
|
./hardware/vds.nix
|
||||||
boot = {
|
|
||||||
# kernelPackages = pkgs.linuxPackages_xanmod_stable;
|
inputs.self.nixosModules.default
|
||||||
hardwareScan = true;
|
];
|
||||||
loader = {
|
|
||||||
grub = {
|
boot = {
|
||||||
enable = true;
|
# kernelPackages = pkgs.linuxPackages_xanmod_stable;
|
||||||
device = "nodev";
|
hardwareScan = true;
|
||||||
useOSProber = false;
|
loader = {
|
||||||
efiSupport = false;
|
grub = {
|
||||||
|
enable = true;
|
||||||
|
device = "nodev";
|
||||||
|
useOSProber = false;
|
||||||
|
efiSupport = false;
|
||||||
|
};
|
||||||
|
systemd-boot.enable = lib.mkDefault false;
|
||||||
|
};
|
||||||
|
kernel.sysctl = {
|
||||||
|
"net.ipv4.tcp_syncookies" = 1;
|
||||||
|
"net.ipv4.tcp_max_syn_backlog" = 4096;
|
||||||
|
"net.ipv4.tcp_synack_retries" = 3;
|
||||||
|
"net.ipv4.tcp_syn_retries" = 3;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
earlyoom.enable = true;
|
||||||
|
journald = {
|
||||||
|
extraConfig = ''
|
||||||
|
SystemMaxUse=512M
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
openssh = {
|
||||||
|
enable = true;
|
||||||
|
allowSFTP = true;
|
||||||
|
openFirewall = true;
|
||||||
|
hostKeys = [
|
||||||
|
{
|
||||||
|
path = "/etc/ssh/id_ed25519";
|
||||||
|
type = "ed25519";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
settings = {
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
PermitRootLogin = "yes";
|
||||||
|
UsePAM = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
tailscale = {
|
||||||
|
enable = true;
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
networking = {
|
||||||
|
nameservers = [
|
||||||
|
"1.1.1.1"
|
||||||
|
"8.8.8.8"
|
||||||
|
# "2001:4860:4860::8844"
|
||||||
|
# "2001:4860:4860::8888"
|
||||||
|
# "2606:4700:4700::1111"
|
||||||
|
# "2606:4700:4700::1001"
|
||||||
|
];
|
||||||
|
hostName = "${xlib.device.hostname}";
|
||||||
|
networkmanager.enable = true;
|
||||||
|
tempAddresses = "disabled";
|
||||||
|
dhcpcd = {
|
||||||
|
enable = true;
|
||||||
|
IPv6rs = false;
|
||||||
|
};
|
||||||
|
firewall = {
|
||||||
|
enable = true;
|
||||||
|
allowPing = true;
|
||||||
|
};
|
||||||
|
nftables = {
|
||||||
|
enable = true;
|
||||||
|
ruleset = ''
|
||||||
|
table inet filter {
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority 0;
|
||||||
|
|
||||||
|
# loopback
|
||||||
|
iif lo accept
|
||||||
|
|
||||||
|
# уже установленные
|
||||||
|
ct state established,related accept
|
||||||
|
|
||||||
|
# РЕЖЕМ SYN СРАЗУ
|
||||||
|
tcp flags syn tcp dport {80,443} limit rate 20/second burst 40 packets accept
|
||||||
|
tcp flags syn tcp dport {80,443} drop
|
||||||
|
|
||||||
|
# остальное по необходимости
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
enableIPv6 = false;
|
||||||
|
interfaces.ens3 = {
|
||||||
|
useDHCP = true;
|
||||||
|
# ipv4.addresses = [
|
||||||
|
# {
|
||||||
|
# address = "31.57.158.109";
|
||||||
|
# prefixLength = 24;
|
||||||
|
# }
|
||||||
|
# ];
|
||||||
|
# ipv6.addresses = [
|
||||||
|
# {
|
||||||
|
# address = "2a13:7c00:6:102:f816:3eff:fe91:6b9e";
|
||||||
|
# prefixLength = 64;
|
||||||
|
# }
|
||||||
|
# ];
|
||||||
|
};
|
||||||
|
# defaultGateway = {
|
||||||
|
# address = "31.57.158.1";
|
||||||
|
# interface = "ens3";
|
||||||
|
# };
|
||||||
|
# defaultGateway6 = {
|
||||||
|
# address = "2a13:7c00:6:102::1";
|
||||||
|
# interface = "ens3";
|
||||||
|
# };
|
||||||
|
};
|
||||||
|
|
||||||
|
system = {
|
||||||
|
stateVersion = "25.05";
|
||||||
};
|
};
|
||||||
systemd-boot.enable = lib.mkDefault false;
|
|
||||||
};
|
};
|
||||||
kernel.sysctl = {
|
in
|
||||||
"net.ipv4.tcp_syncookies" = 1;
|
inputs.nixpkgs.lib.nixosSystem {
|
||||||
"net.ipv4.tcp_max_syn_backlog" = 4096;
|
modules = [
|
||||||
"net.ipv4.tcp_synack_retries" = 3;
|
nixosModule
|
||||||
"net.ipv4.tcp_syn_retries" = 3;
|
];
|
||||||
};
|
system = "x86_64-linux";
|
||||||
};
|
specialArgs = {
|
||||||
|
deviceType = "vds";
|
||||||
host.ssh.enable = true;
|
|
||||||
# SSH is reachable only over Tailscale (not on the public internet).
|
|
||||||
# This otreca VDS is reached by deploy-rs and by oqyude over the
|
|
||||||
# tailnet, so exposing 22 to ens3 is pure attack surface.
|
|
||||||
services.openssh.openFirewall = false;
|
|
||||||
|
|
||||||
services.tailscale = {
|
|
||||||
enable = true;
|
|
||||||
openFirewall = true;
|
|
||||||
};
|
|
||||||
# Open port 22 only on the tailscale interface.
|
|
||||||
networking.firewall.interfaces.tailscale0.allowedTCPPorts = [ 22 ];
|
|
||||||
networking = {
|
|
||||||
nameservers = [
|
|
||||||
"1.1.1.1"
|
|
||||||
"8.8.8.8"
|
|
||||||
];
|
|
||||||
networkmanager.enable = true;
|
|
||||||
tempAddresses = "disabled";
|
|
||||||
dhcpcd = {
|
|
||||||
enable = true;
|
|
||||||
IPv6rs = false;
|
|
||||||
};
|
|
||||||
firewall = {
|
|
||||||
enable = true;
|
|
||||||
allowPing = true;
|
|
||||||
};
|
|
||||||
nftables = {
|
|
||||||
enable = true;
|
|
||||||
ruleset = ''
|
|
||||||
table inet filter {
|
|
||||||
chain input {
|
|
||||||
type filter hook input priority 0;
|
|
||||||
|
|
||||||
# loopback
|
|
||||||
iif lo accept
|
|
||||||
|
|
||||||
# уже установленные
|
|
||||||
ct state established,related accept
|
|
||||||
|
|
||||||
# РЕЖЕМ SYN СРАЗУ
|
|
||||||
tcp flags syn tcp dport {80,443} limit rate 20/second burst 40 packets accept
|
|
||||||
tcp flags syn tcp dport {80,443} drop
|
|
||||||
|
|
||||||
# остальное по необходимости
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
enableIPv6 = false;
|
|
||||||
interfaces.ens3 = {
|
|
||||||
useDHCP = true;
|
|
||||||
# ipv4.addresses = [
|
|
||||||
# {
|
|
||||||
# address = "31.57.158.109";
|
|
||||||
# prefixLength = 24;
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
# ipv6.addresses = [
|
|
||||||
# {
|
|
||||||
# address = "2a13:7c00:6:102:f816:3eff:fe91:6b9e";
|
|
||||||
# prefixLength = 64;
|
|
||||||
# }
|
|
||||||
# ];
|
|
||||||
};
|
|
||||||
# defaultGateway = {
|
|
||||||
# address = "31.57.158.1";
|
|
||||||
# interface = "ens3";
|
|
||||||
# };
|
|
||||||
# defaultGateway6 = {
|
|
||||||
# address = "2a13:7c00:6:102::1";
|
|
||||||
# interface = "ens3";
|
|
||||||
# };
|
|
||||||
};
|
|
||||||
|
|
||||||
system = {
|
|
||||||
stateVersion = "25.05";
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-55
@@ -1,60 +1,103 @@
|
|||||||
# Host: "wsl" (device: wsl)
|
{ inputs, ... }@flakeContext:
|
||||||
#
|
let
|
||||||
# The host record lives in configurations/default.nix; this file is only the
|
nixosModule =
|
||||||
# module body. `xlib` (identity, dirs, helpers) arrives as a module argument.
|
{
|
||||||
{
|
config,
|
||||||
lib,
|
lib,
|
||||||
modulesPath,
|
pkgs,
|
||||||
pkgs,
|
modulesPath,
|
||||||
xlib,
|
xlib,
|
||||||
inputs,
|
...
|
||||||
...
|
}:
|
||||||
}:
|
{
|
||||||
{
|
xlib.device = {
|
||||||
imports = [
|
type = "wsl";
|
||||||
inputs.nixos-wsl.nixosModules.default
|
hostname = "wsl";
|
||||||
inputs.self.nixosModules.default
|
};
|
||||||
];
|
|
||||||
|
|
||||||
hardware = {
|
imports = [
|
||||||
graphics.enable = true;
|
inputs.nixos-wsl.nixosModules.default
|
||||||
};
|
inputs.self.nixosModules.default
|
||||||
|
];
|
||||||
|
|
||||||
networking = {
|
#zramSwap.enable = true;
|
||||||
firewall = {
|
services = {
|
||||||
enable = false;
|
journald = {
|
||||||
allowPing = true;
|
extraConfig = ''
|
||||||
|
SystemMaxUse=512M
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
earlyoom.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
hardware = {
|
||||||
|
graphics.enable = true;
|
||||||
|
# amdgpu.opencl.enable = true;
|
||||||
|
# amdgpu.amdvlk.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
networking = {
|
||||||
|
# nameservers = [
|
||||||
|
# "1.1.1.1"
|
||||||
|
# "8.8.8.8"
|
||||||
|
# "2001:4860:4860::8844"
|
||||||
|
# "2001:4860:4860::8888"
|
||||||
|
# "2606:4700:4700::1111"
|
||||||
|
# "2606:4700:4700::1001"
|
||||||
|
# ];
|
||||||
|
hostName = "${xlib.device.hostname}";
|
||||||
|
# networkmanager.enable = true;
|
||||||
|
# tempAddresses = "disabled";
|
||||||
|
# dhcpcd = {
|
||||||
|
# enable = true;
|
||||||
|
# IPv6rs = true;
|
||||||
|
# };
|
||||||
|
firewall = {
|
||||||
|
enable = false;
|
||||||
|
allowPing = true;
|
||||||
|
};
|
||||||
|
enableIPv6 = true;
|
||||||
|
# interfaces.ens3 = {
|
||||||
|
# useDHCP = true;
|
||||||
|
# # ipv4.addresses = [
|
||||||
|
# # {
|
||||||
|
# # address = "31.57.158.109";
|
||||||
|
# # prefixLength = 24;
|
||||||
|
# # }
|
||||||
|
# # ];
|
||||||
|
# ipv6.addresses = [
|
||||||
|
# {
|
||||||
|
# address = "2a13:7c00:10:6:f816:3eff:fe36:fe1b";
|
||||||
|
# prefixLength = 64;
|
||||||
|
# }
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
|
# # defaultGateway = {
|
||||||
|
# # address = "31.57.158.1";
|
||||||
|
# # interface = "ens3";
|
||||||
|
# # };
|
||||||
|
# defaultGateway6 = {
|
||||||
|
# address = "2a13:7c00:10:6::1";
|
||||||
|
# interface = "ens3";
|
||||||
|
# };
|
||||||
|
};
|
||||||
|
|
||||||
|
wsl = {
|
||||||
|
enable = true;
|
||||||
|
startMenuLaunchers = true;
|
||||||
|
useWindowsDriver = true;
|
||||||
|
defaultUser = config.xlib.device.username;
|
||||||
|
};
|
||||||
|
|
||||||
|
system.stateVersion = "24.11";
|
||||||
};
|
};
|
||||||
enableIPv6 = true;
|
in
|
||||||
|
inputs.nixpkgs.lib.nixosSystem {
|
||||||
|
modules = [
|
||||||
|
nixosModule
|
||||||
|
];
|
||||||
|
system = "x86_64-linux";
|
||||||
|
specialArgs = {
|
||||||
|
deviceType = "wsl";
|
||||||
};
|
};
|
||||||
|
|
||||||
wsl = {
|
|
||||||
enable = true;
|
|
||||||
startMenuLaunchers = true;
|
|
||||||
useWindowsDriver = true;
|
|
||||||
defaultUser = xlib.device.username;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Enable SSH server on WSL NixOS so sapphira can drive it directly via a
|
|
||||||
# ProxyCommand chain through the Windows OpenSSH layer. The shared
|
|
||||||
# essentials/ssh.nix module wires host keys, sops-managed user keys, and
|
|
||||||
# passwordless key auth — nothing to repeat here.
|
|
||||||
host.ssh.enable = true;
|
|
||||||
|
|
||||||
# Advertise this WSL instance as a remote Nix builder for sapphira (2
|
|
||||||
# cores, the bottleneck host). All builder wiring — fixing the
|
|
||||||
# `system-features` to drop the unsupported `kvm`, and adding the SSH
|
|
||||||
# user `oqyude` to trusted-users — lives in modules/wsl/builder.nix.
|
|
||||||
#
|
|
||||||
# ---- DISABLED 2026-10-04 ----
|
|
||||||
# Remote building temporarily turned off builder-side. The default of
|
|
||||||
# `host.builder.enable` is `false` (modules/options.nix), so
|
|
||||||
# modules/wsl/builder.nix's `lib.mkIf enable` block is skipped: WSL
|
|
||||||
# keeps its default system-features and trusted-users, and no SSH-side
|
|
||||||
# state changes. Re-enable by uncommenting the assignment below and
|
|
||||||
# removing the DISABLED banner in configurations/server.nix.
|
|
||||||
#
|
|
||||||
# host.builder.enable = true;
|
|
||||||
|
|
||||||
system.stateVersion = "24.11";
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -6,11 +6,10 @@ let
|
|||||||
path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos inputs.self.nixosConfigurations.${hostname};
|
path = inputs.deploy-rs.lib.x86_64-linux.activate.nixos inputs.self.nixosConfigurations.${hostname};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
# Login user for every deploy target. Read from the hoisted xlib instead of
|
user = "${inputs.self.nixosConfigurations.default.config.xlib.device.username}";
|
||||||
# digging through a built NixOS configuration.
|
|
||||||
user = "${inputs.self.xlib.default.device.username}";
|
|
||||||
server = "sapphira";
|
server = "sapphira";
|
||||||
vds = "otreca";
|
vds = "otreca";
|
||||||
|
vds-new = "otreca-new";
|
||||||
mini-laptop = "rydiwo";
|
mini-laptop = "rydiwo";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -20,6 +19,7 @@ in
|
|||||||
nodes = {
|
nodes = {
|
||||||
"${server}" = mkDeploy "${server}";
|
"${server}" = mkDeploy "${server}";
|
||||||
"${vds}" = mkDeploy "${vds}";
|
"${vds}" = mkDeploy "${vds}";
|
||||||
|
"${vds-new}" = mkDeploy "${vds-new}";
|
||||||
"${mini-laptop}" = mkDeploy "${mini-laptop}";
|
"${mini-laptop}" = mkDeploy "${mini-laptop}";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
# Инварианты: вопросы владельцу
|
|
||||||
|
|
||||||
Проход по репозиторию сверху вниз, 2026-10-05. 120 `.nix`, ~8.5k строк.
|
|
||||||
|
|
||||||
Структура:
|
|
||||||
- Сводный ответ, ядро и ловушки → **`AGENTS.md`** (корень репозитория).
|
|
||||||
- Подробная карта архитектуры с per-host деталями и инвентарём сервисов →
|
|
||||||
**`docs/arch/map.md`**.
|
|
||||||
- Этот файл → **открытые вопросы** (слои 0–8) + ещё непрочитанные **слои 9–11**
|
|
||||||
(home-manager, deploy, формат).
|
|
||||||
|
|
||||||
Пометки: `[!]` — найденный дефект, не вопрос. `[?]` — не смог определить по коду.
|
|
||||||
`[✓]` — отвечено владельцем 2026-10-05.
|
|
||||||
|
|
||||||
## Статус ответов (2026-10-05)
|
|
||||||
|
|
||||||
Отвечено: **2.1, 5.2, 6.1, 6.3, 6.5, 7.1, 7.2** (7 пунктов). Остальные ждут
|
|
||||||
ответа (таблица ниже).
|
|
||||||
|
|
||||||
Ключевое из ответов, что меняет картину:
|
|
||||||
|
|
||||||
- **6.5 — моя ошибка.** `100.64.0.0` не «сетевой адрес вместо интерфейса»:
|
|
||||||
это Tailscale-адрес sapphira, назначенный вручную.
|
|
||||||
- **6.3 — это не дыра, а осознанное решение.** Граница держится на роутере:
|
|
||||||
на сервер пробрасываются ровно 5 портов — **443, 80, 22000 (syncthing),
|
|
||||||
8443 (xray), 22 (ssh)**. `firewall.enable = false` на sapphira — следствие,
|
|
||||||
а не недосмотр. Проблема в другом: **список пробросов нигде не записан
|
|
||||||
в репозитории**, и именно его агент обязан уважать (D1 в `todo.md`).
|
|
||||||
- **7.2 — 3x-ui рабочий.** Откат сделан осознанно: панель последняя, ядро Xray
|
|
||||||
осталось на 26.7.28, миграция на 26.9 провалена, лишний код закомментирован.
|
|
||||||
Состояние — «заморожено», а не «сломано».
|
|
||||||
- **5.2 — подтверждённая дыра в защите данных.** Guard для несмонтированного
|
|
||||||
носителя не был продуман → задача B1.
|
|
||||||
|
|
||||||
## Сводка подтверждённых инвариантов
|
|
||||||
|
|
||||||
| # | Пункт | Краткая формулировка | См. |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1 | Все `outputs` флейка вычисляются | A1: правка `lib/xlib.nix` → `lib/xlib`; закрепить через `nix flake check` | todo A1 |
|
|
||||||
| 2 | External-диск монтируется до сервисов | mkServiceStorage + bind без guard'а → сервис стартует на пустой БД | todo B1 |
|
|
||||||
| 3 | Сетевая граница sapphira = роутер | 5 портов: 22, 80, 443, 8443, 22000; `firewall.enable = false` намеренно | todo D1 |
|
|
||||||
| 4 | `100.64.0.0` = Tailscale sapphira | Назначен вручную; в 4 файлах | AGENTS.md §5 |
|
|
||||||
| 5 | 3x-ui заморожен | Панель на latest; ядро Xray на 26.7.x; миграция 26.9 провалена | todo C1–C5 |
|
|
||||||
| 6 | nftables на VDS — явная финальная политика | Сейчас ruleset без финального правила + конфликт с `firewall.*` | todo A3 |
|
|
||||||
|
|
||||||
## Сводка по ловушкам
|
|
||||||
|
|
||||||
Полная таблица (10 пунктов) в **`AGENTS.md`** → раздел «Ловушки». Кратко:
|
|
||||||
`firewall.enable=false` намеренно · uid=1001 на sapphira · `image = …:latest`
|
|
||||||
намеренно для 3x-ui · `reality443Forwarding=true` — следствие отката ·
|
|
||||||
15 закомментированных модулей в server/default.nix · `serviceConfig` vs `Service`
|
|
||||||
в home-manager · nftables без финального правила · `100.64.0.0` не сеть ·
|
|
||||||
`/mnt/services` mode 0777 · `stateVersion` разный между хостами.
|
|
||||||
|
|
||||||
## Неотвеченные вопросы (слои 0–8)
|
|
||||||
|
|
||||||
Самые важные — выделены.
|
|
||||||
|
|
||||||
| ID | Вопрос | Что блокирует |
|
|
||||||
|---|---|---|
|
|
||||||
| 0.1 | Восстанавливать ли migration notes, удалённые в `22a19be`? | C1: реконструкция заметки 3x-ui |
|
|
||||||
| 0.2 | Комментарий-density 38/120 файлов без комментариев — нормально? | Стиль модулей |
|
|
||||||
| 0.3 | 15 закомментированных модулей: удалить или хранить как референс? | E3: чистота кода |
|
|
||||||
| 0.4 | README пустой, todo.md нет — норма? | E1: AGENTS.md/README |
|
|
||||||
| 1.3 | Лишние inputs в flake (`justray`, `nix-minecraft`, `proxy-suite`)? | Чистота flake |
|
|
||||||
| 1.5 | `nix-systems` через `follows` — оптимизация размера lock | Документация |
|
|
||||||
| **2.2** | **`vetymae` / `lamet` / `therima` / `soptur` — те же машины или хосты вне реестра?** | **DNS/nginx/identity** |
|
|
||||||
| 2.3 | sapphira uid=1001: блокер ли использование `/mnt/archive`/`/mnt/mobile`? | Миграция ФС |
|
|
||||||
| **2.5** | **stateVersion 24.05 / 24.11 / 25.05 / 26.05 — намеренный дрейф?** | **Миграции** |
|
|
||||||
| 2.6 | Есть ли escape hatch для per-host отличий в xlib? | Архитектура |
|
|
||||||
| 2.7 | `devices.termux` без NixOS-хоста — закрытый список | Документация |
|
|
||||||
| 3.2 | `any.nix` (minimal) нужны home-manager + sops + disko? | Минималка |
|
|
||||||
| **4.1** | **Как root получает доступ по SSH — authorizedKeys в коде нет** | **deploy, безопасность** |
|
|
||||||
| **4.2** | **Как разрешается цикл «ключ в секрете, а нужен для расшифровки»?** | **bootstrap, recovery** |
|
|
||||||
| 4.3 | Все файлы в `secrets/` покрыты `path_regex`? | sops |
|
|
||||||
| 4.4 | Как подключается вторая машина / второй человек при одном age-ключе? | sops, scale |
|
|
||||||
| 4.5 | `users.nix:87` — личный ключ или общий «ключ от деплоя»? | Безопасность |
|
|
||||||
| 5.1 | `/mnt/services` mode 0777 — осознанно? | Безопасность |
|
|
||||||
| 5.3 | NFS выключен, Samba работает — миграция? | Сетевые сервисы |
|
|
||||||
| 5.4 | NTFS-том `lamet-drive` `mask=0000` — что на нём? | Семантика |
|
|
||||||
| 5.5 | `therima` / `vetymae` / `soptur` в dirs.nix — реально смонтированы? | Семантика |
|
|
||||||
| 5.6 | Где бэкапы БД и 3x-ui? | B2 |
|
|
||||||
| 6.6 | `192.168.1.20` зашит в 30 мест — константа? | Рефакторинг |
|
|
||||||
| 6.7 | DNS ↔ сервисы — как ловим рассинхрон? | Документация, CI |
|
|
||||||
| **6.8** | **Публичные IP + SSH-алиасы в `home/termux.nix` — карта «хост → адреса» нужна?** | **Архитектура** |
|
|
||||||
| 6.9 | Какой путь REALITY правильный сейчас? | C5 |
|
|
||||||
| 7.4 | Почему не публиковать весь диапазон 14380-15379? | 3x-ui |
|
|
||||||
| 8.2 | `lamet.opencodes` → `:6061` (порт miniflux) — ошибка? | nginx |
|
|
||||||
| 8.4 | `onlyoffice` после трёх регрессов — работает? | Статус сервиса |
|
|
||||||
| 8.5 | Что слушает `:3002` (`/whiteboard` nextcloud)? | Карта сервисов |
|
|
||||||
| 8.6 | Бэкапы вне Nix — записать | Документация |
|
|
||||||
|
|
||||||
## Где это раньше лежало
|
|
||||||
|
|
||||||
До переноса в `AGENTS.md` / `map.md` здесь был подробный Q&A по слоям 0–8
|
|
||||||
с разделами «Вопрос», «Факт», «Риск», «Кандидат». Этот текст сохранён в
|
|
||||||
git-истории файла (последний коммит, где Q&A был полным). Восстановить:
|
|
||||||
`git log -p docs/arch/invariants.md | less`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Слой 9. home-manager
|
|
||||||
|
|
||||||
**9.1** `home/home.nix:52-57` — для пользователя импортируется
|
|
||||||
`home/${xlib.device.type}.nix`; для `root` — без профиля (строка 51).
|
|
||||||
**Вопрос:** почему у `root` нет home-профиля — сознательно?
|
|
||||||
**Кандидат:** `home/<type>.nix` = единственный источник «что есть на этом хосте»
|
|
||||||
для пользователя; добавление пакета в новый тип = правильный файл, а не
|
|
||||||
`home/default.nix`.
|
|
||||||
|
|
||||||
**9.2 [!]** `home/home.nix:28-43` для headless-хостов: `xdg.userDirs.* = null` и
|
|
||||||
`createDirectories = false`, при этом `lib/xlib/dirs.nix:26` обещает
|
|
||||||
`music-library = "${user-home}/Music"`.
|
|
||||||
**Вопрос:** кто создаёт `~/Music` и `~/Storage`? `createDirectories = false`
|
|
||||||
означает, что home-manager их не создаст, а `dirs.nix` на них ссылается.
|
|
||||||
**Риск:** на headless-хосте путь в конфиге есть, а каталога нет → тихий сбой
|
|
||||||
сервиса, который туда пишет.
|
|
||||||
|
|
||||||
**9.3 [!]** `home/modules/opencode.nix:339-350` (`c73a698`): в home-manager нельзя
|
|
||||||
писать `serviceConfig = { ... }` — рендерится литеральная секция `[serviceConfig]`,
|
|
||||||
которую systemd молча игнорирует («Unknown section 'serviceConfig'. Ignoring.»).
|
|
||||||
Правильно: `systemd.user.services.opencode-web.Service = { ... }`.
|
|
||||||
**Кандидат (готовый инвариант, стоит закрепить буквально в `AGENTS.md`):**
|
|
||||||
в home-manager cgroup-опции (`MemoryHigh`, `OOMScoreAdjust`, …) пишутся
|
|
||||||
в `systemd.user.services.<name>.Service`, **не** в `serviceConfig`. Ошибка
|
|
||||||
не диагностируется — она просто не применяется.
|
|
||||||
|
|
||||||
**9.4** `linger = true` добавлен ради `opencode-web` (`users.nix:71-75`) и включён
|
|
||||||
**для всех** хостов.
|
|
||||||
**Кандидат:** «user-сервисы переживают logout на всех хостах» — закрепить, потому
|
|
||||||
что это неочевидное поведение, влияющее на ресурсы и на безопасность.
|
|
||||||
|
|
||||||
**9.5** `home/modules/opencode.nix:286-303` — `opencode.web` слушает `0.0.0.0:4096`
|
|
||||||
(комментарий: nginx проксирует `127.0.0.1:4096`), и nginx на sapphira ходит туда
|
|
||||||
же по Tailscale у двух других хостов (см. 8.3).
|
|
||||||
**Кандидат:** `0.0.0.0` в `opencode.web` — обязательное условие для внешнего
|
|
||||||
доступа через `opencodes.*`; пароль приходит из sops-секрета `opencode_server`.
|
|
||||||
|
|
||||||
**9.6** Секреты opencode приходят в `~/.config/opencode/server.env` (dotenv),
|
|
||||||
`~/.local/share/opencode/auth.json` и `account.json` (json, `key = ""`).
|
|
||||||
**Кандидат:** эти три файла перезаписываются sops при каждой активации — ручные
|
|
||||||
правки в них теряются. Уже отражено в комментарии `users.nix:120-131`, стоит
|
|
||||||
закрепить как инвариант.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Слой 10. deploy и проверка
|
|
||||||
|
|
||||||
**10.1** `deploy/default.nix:20-24` — цели: `sapphira` (server), `otrecа` (vds),
|
|
||||||
`rydiwo` (ноутбук). **Нет** `atoridu` (основной десктоп), `wsl`, `epral`.
|
|
||||||
**Вопрос:** почему не деплоится десктоп? И безопасно ли пересобирать ноутбук
|
|
||||||
`rydiwo` по SSH (он может быть выключен/на другом Wi-Fi)?
|
|
||||||
**Кандидат:** `deploy-rs` = только серверы + ноутбук; десктоп и WSL обновляются
|
|
||||||
вручную. Инвариант: не добавлять в `deploy.nodes` хост, который нельзя
|
|
||||||
пересобрать в любой момент без риска потерять доступ.
|
|
||||||
|
|
||||||
**10.2** `deploy/default.nix:18-19` — `sshUser = "oqyude"`, `user = "root"`.
|
|
||||||
См. 4.1: root-доход по SSH не описан в конфигурации.
|
|
||||||
**Кандидат:** деплой требует ручной настройки root-доступа на каждом из 3 хостов —
|
|
||||||
это скрытая зависимость, которую агент не выведет.
|
|
||||||
|
|
||||||
**10.3** `deploy/default.nix:27-29` — `checks = builtins.mapAttrs (... deployChecks)`.
|
|
||||||
**Вопрос:** `nix flake check` реально проходит сейчас? Учитывая 2.1 (`lib/xlib.nix`)
|
|
||||||
он должен падать на `nixOnDroidConfigurations`. Падает или `checks` покрывают
|
|
||||||
не всё дерево outputs?
|
|
||||||
**Кандидат (первое, что стоит сделать):** добиться, чтобы
|
|
||||||
`nix flake check` был зелёным — это единственная автоматическая защита от
|
|
||||||
подобных breakage'ов.
|
|
||||||
|
|
||||||
**10.4** CI нет, `flake check` не запускается автоматически.
|
|
||||||
**Кандидат:** минимальный локальный набор перед коммитом:
|
|
||||||
`nix flake check && nix build .#nixosConfigurations.<хост>.config.system.build.toplevel --dry-run`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Слой 11. Формат (то, что я предлагаю зафиксировать как процесс)
|
|
||||||
|
|
||||||
**11.1** Где будет жить итог: `AGENTS.md` в корне (читается агентом всегда),
|
|
||||||
`docs/arch/map.md` (карта хостов/сервисов), `docs/arch/invariants.md` (этот файл).
|
|
||||||
**Кандидат:** этот файл после ответов превращается в `docs/arch/invariants.md`
|
|
||||||
с колонкой «ответ» и становится источником для `AGENTS.md`; `AGENTS.md` — краткая
|
|
||||||
выжимка, без подробностей.
|
|
||||||
|
|
||||||
**11.2** Какие инварианты можно превратить в автоматическую проверку (тогда они
|
|
||||||
перестанут «забываться»):
|
|
||||||
1. ни одного `:latest` в образах (grep по `image =`);
|
|
||||||
2. `nix flake check` зелёный;
|
|
||||||
3. каждый домен из `coredns.nix` имеет vhost в `nginx.nix` и наоборот;
|
|
||||||
4. каждый сервис в `mkServiceStorage` имеет каталог в `/mnt/services` на
|
|
||||||
`External`-диске;
|
|
||||||
5. в самописном nftables-ruleset последнее правило цепочки явное;
|
|
||||||
6. каждый `listen.addr` — реально назначенный адрес, а не сеть;
|
|
||||||
7. все файлы в `secrets/` матчат `path_regex` из `.sops.yaml`.
|
|
||||||
|
|
||||||
**Вопрос:** какие из этих проверок ты хочешь, а какие — лишний CI?
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
# Карта архитектуры
|
|
||||||
|
|
||||||
Полная карта репозитория: per-host детали, сетевая топология, инвентарь сервисов.
|
|
||||||
Слои 0–8 проработаны; слои 9–11 (home-manager, deploy, формат) см. в
|
|
||||||
`docs/arch/invariants.md`.
|
|
||||||
|
|
||||||
## Содержание
|
|
||||||
|
|
||||||
1. [Реестр хостов](#реестр-хостов)
|
|
||||||
2. [Идентичность и xlib](#идентичность-и-xlib)
|
|
||||||
3. [Диспетчеризация модулей](#диспетчеризация-модулей)
|
|
||||||
4. [Пользователь, SSH, секреты](#пользователь-ssh-секреты)
|
|
||||||
5. [Хранилище](#хранилище)
|
|
||||||
6. [Сеть и firewall](#сеть-и-firewall)
|
|
||||||
7. [Сервисы](#сервисы)
|
|
||||||
8. [Неотвеченные вопросы](#неотвеченные-вопросы)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Реестр хостов
|
|
||||||
|
|
||||||
Единственная точка добавления/изменения хоста — `configurations/default.nix:13-39`.
|
|
||||||
Имя атрибута **равно** hostname; отдельное `hostname = …` только у `default`
|
|
||||||
(где attr = `default`).
|
|
||||||
|
|
||||||
| Attr | hostname | device.type | description |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `default` | nixos | minimal | Шаблон, hostname `"nixos"`, `device = "minimal"` |
|
|
||||||
| `atoridu` | atoridu | primary | Основной десктоп, `xanmod` |
|
|
||||||
| `rydiwo` | rydiwo | secondary | Chuwi MiniBook, `xanmod`, NTFS-том `lamet-drive` |
|
|
||||||
| `otrecа` | otreca | vds | VPS, SSH только через Tailscale, `grub` без EFI |
|
|
||||||
| `sapphira` | sapphira | server | Домашний сервер, `firewall.enable = false` намеренно |
|
|
||||||
| `wsl` | wsl | wsl | WSL NixOS на Windows-хосте `vetymae` |
|
|
||||||
| `epral` | epral | termux | Android (`nix-on-droid`), отдельный путь конфигурации |
|
|
||||||
|
|
||||||
`device.type` ∈ { minimal, primary, secondary, server, vds, wsl, termux }.
|
|
||||||
Машина `vetymae` (Windows + WSL) фигурирует в `coredns`, `nginx`, `modules/server/systemd.nix`,
|
|
||||||
но **не** в реестре хостов — это внешний хост, через который заходят на WSL.
|
|
||||||
|
|
||||||
### Per-host summary
|
|
||||||
|
|
||||||
- **`atoridu`** (`primary/mini-pc`): без `nixos-hardware` (мини-ПК). Linux `xanmod_stable`,
|
|
||||||
`systemd-boot`, EFI. `stateVersion 26.05`. → `configurations/mini-pc.nix`.
|
|
||||||
- **`rydiwo`** (`secondary/mini-laptop`): `nixos-hardware: chuwi-minibook-x`, xanmod,
|
|
||||||
`systemd-boot`, EFI. **NTFS-том `xlib.dirs.lamet-drive`** с `mask = "0000"` —
|
|
||||||
world-readable/writable по дизайну [?]. `stateVersion 26.05`.
|
|
||||||
- **`otrecа`** (`vds`): qemu-guest, GRUB без EFI, `disko` + `hardware/vds.nix`.
|
|
||||||
`firewall.enable = true` + ручной `nftables.ruleset` без финального правила.
|
|
||||||
`firewall.interfaces.tailscale0.allowedTCPPorts = [22]`. `stateVersion 25.05`.
|
|
||||||
- **`sapphira`** (`server`): systemd-boot, EFI, ext4 на UUID `37e53ebc-…-a8de`.
|
|
||||||
bind-mount `/mnt/services` ← `/home/oqyude/External/Services`. `stateVersion 25.05`.
|
|
||||||
- **`wsl`**: `nixos-wsl` + NixOS-стек, IPv6 on, `firewall.enable = false`.
|
|
||||||
`stateVersion 24.11`. Реальный Windows-хост — `vetymae`, `192.168.1.100`.
|
|
||||||
- **`epral`** (`mobile.nix`): не NixOS, **nix-on-droid**. `stateVersion 24.05`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Идентичность и xlib
|
|
||||||
|
|
||||||
`lib/xlib/` собирает чистые данные (без модулей):
|
|
||||||
|
|
||||||
```
|
|
||||||
xlib = {
|
|
||||||
device = { hostname, type, username, uid, gid };
|
|
||||||
isDesktop, isHeadless; # ← от device.type через devices.<type>.{desktop,headless}
|
|
||||||
dirs = mkDirs username; # ← well-known пути, зависят только от username
|
|
||||||
helpers = { mkBindMount, mkSystemdBind, mkServiceStorage, mkNtfsMount,
|
|
||||||
mkExfatMount, mkTmpDirs, mkSymlinks };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- **`mkXlib`** (`lib/xlib/default.nix:38-77`) — единственная точка сборки; вызывается
|
|
||||||
в `configurations/default.nix:50`. Прокидывается в каждый модуль как
|
|
||||||
`xlib = …` через `lib/mkSystem.nix:specialArgs`.
|
|
||||||
- **`devices`** (`lib/xlib/device.nix:12-41`) — закрытое множество device.types.
|
|
||||||
Неизвестный тип → throw со списком валидных. Добавление типа = новая папка
|
|
||||||
`modules/<type>/` + `home/<type>.nix` + запись в `devices`.
|
|
||||||
- **`uid/gid`** зашиты как `?` `1000`/`1000` в `mkXlib`. Менять = инвентаризация
|
|
||||||
во всех хостах, иначе расходятся владельцы файлов на NTFS/exFAT.
|
|
||||||
- **`sapphira`** — исключение: `users.nix:66` ставит `uid = 1001` для сохранения
|
|
||||||
совместимости со старым `uid-map` (`yuyus` = 1000). `TODO: delete once
|
|
||||||
sapphira migrated to 1000`. Цена: exFAT на sapphira получает `uid=1000` от
|
|
||||||
`xlib.device.uid`, поэтому пользователь не может писать в `/mnt/archive` и
|
|
||||||
`/mnt/mobile` до миграции.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Диспетчеризация модулей
|
|
||||||
|
|
||||||
### `nixosModules.default` (`modules/default.nix:9-39`)
|
|
||||||
|
|
||||||
Импортирует на **каждый** NixOS-хост (включая `minimal`):
|
|
||||||
|
|
||||||
```
|
|
||||||
./essentials → packages, services, settings, ssh, shell, systemd-routines
|
|
||||||
./options.nix → host.builder.*, host."3x-ui".*
|
|
||||||
./users.nix → пользователь, sops-секреты
|
|
||||||
home-manager.nixosModules.home-manager
|
|
||||||
sops-nix.nixosModules.sops
|
|
||||||
justray.nixosModules.default
|
|
||||||
disko.nixosModules.disko
|
|
||||||
grub2-themes.nixosModules.default
|
|
||||||
self.homeConfigurations.default.nixosModule
|
|
||||||
```
|
|
||||||
|
|
||||||
Плюс `lib.optional xlib.isDesktop ./desktop` (primary, secondary).
|
|
||||||
Плюс `lib.optional (!isDesktop && type != "minimal") (./. + "/${type}")`
|
|
||||||
(server, vds, wsl, termux). **termux** попадает сюда только в path nix-on-droid,
|
|
||||||
не как NixOS-хост (см. `mobile.nix`).
|
|
||||||
|
|
||||||
### `nixosModules.strict` (`modules/default.nix:40-53`)
|
|
||||||
|
|
||||||
Используется только `mobile.nix:22`. Импортирует `options.nix` +
|
|
||||||
`./<device.type>`; **всё** остальное NixOS-специфичное (essentials, users,
|
|
||||||
home-manager, sops, disko, grub2-themes) **выключено**, потому что nix-on-droid
|
|
||||||
не имеет `services.*`, `users.*`, `sops.*`, `disko.*` в своей модульной системе.
|
|
||||||
|
|
||||||
### Правило для кросс-модульных опций
|
|
||||||
|
|
||||||
Опция живёт в `modules/options.nix`, если её **устанавливает** один модуль,
|
|
||||||
а **читает** другой. `host.reader.X.enable` живёт в `essentials/ssh.nix`, потому
|
|
||||||
что его объявляет и использует один модуль.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Пользователь, SSH, секреты
|
|
||||||
|
|
||||||
### Пользователь `oqyude`
|
|
||||||
|
|
||||||
- `uid` = `1000` на всех хостах, кроме `sapphira` (=`1001`, см. выше).
|
|
||||||
- `home = /home/oqyude`, `homeMode = "700"`.
|
|
||||||
- `linger = true` на всех хостах — user-services (opencode-web) переживают logout.
|
|
||||||
Следствие: user-сервисы стартуют и потребляют ресурсы без активной сессии.
|
|
||||||
- `extraGroups`: `audio disk gamemode networkmanager pipewire wheel libvirtd qemu-libvirtd`.
|
|
||||||
|
|
||||||
### SSH
|
|
||||||
|
|
||||||
- `essentials/ssh.nix`: `services.openssh` включается через `host.ssh.enable`,
|
|
||||||
`PermitRootLogin = "yes"` (намеренно для deploy), `PasswordAuthentication = false`,
|
|
||||||
hostKey = `/etc/ssh/id_ed25519`.
|
|
||||||
- `authorizedKeys` для `oqyude` зашит в `users.nix:87` (`ssh-ed25519 AAAA…`).
|
|
||||||
Чей — `[?]` (см. вопрос 4.1).
|
|
||||||
- `users.nix` определяет `root`-authorizedKeys **отсутствует** [?] — root как-то
|
|
||||||
попадает на хост; deploy-rs использует `sshUser = "oqyude", user = "root"`.
|
|
||||||
|
|
||||||
### Циклическая зависимость ключа
|
|
||||||
|
|
||||||
`/etc/ssh/id_ed25519` одновременно:
|
|
||||||
- `hostKeys` для sshd (`essentials/ssh.nix:22`)
|
|
||||||
- `sops.age.sshKeyPaths` для расшифровки (`users.nix:95-97`)
|
|
||||||
- цель `ssh_key_private_known` (`users.nix:147-152`)
|
|
||||||
- цель `ssh_key_public_host` (`users.nix:159`)
|
|
||||||
|
|
||||||
Как разворачивается на чистой машине — **одноразовый bootstrap** [?].
|
|
||||||
Должен быть задокументирован, иначе при переустановке хоста агент не выведет.
|
|
||||||
|
|
||||||
### `.sops.yaml`
|
|
||||||
|
|
||||||
- Один age-ключ (`*default`), `path_regex: secrets/[^/]+\.(yaml|json|env|ini)$`.
|
|
||||||
- Покрывает только плоские файлы в `secrets/` (без подкаталогов).
|
|
||||||
- Добавление секрета = `secrets/<имя>.<yaml|json|env|ini>` строго в корне.
|
|
||||||
- Дополнительные секреты dotenv/json — через `mkUserSecret` (`users.nix:33-41`).
|
|
||||||
|
|
||||||
### Инвентарь секретов (`users.nix:100-162`)
|
|
||||||
|
|
||||||
| Секрет | Формат | Назначение |
|
|
||||||
|---|---|---|
|
|
||||||
| `hashed_password` | yaml | Пароль пользователя |
|
|
||||||
| `age_key_private` | yaml | `~/.config/sops/age/keys.txt` |
|
|
||||||
| `opencode_server` | dotenv | `~/.config/opencode/server.env` |
|
|
||||||
| `opencode_auth` | json | `~/.local/share/opencode/auth.json` (`key=""`) |
|
|
||||||
| `opencode_account` | json | `~/.local/share/opencode/account.json` (`key=""`) |
|
|
||||||
| `ssh_key_private` | yaml | `~/.ssh/id_ed25519` |
|
|
||||||
| `ssh_key_public` | yaml | `~/.ssh/id_ed25519.pub` |
|
|
||||||
| `ssh_key_private_root` | yaml | `/root/.ssh/id_ed25519` |
|
|
||||||
| `ssh_key_public_root` | yaml | `/root/.ssh/id_ed25519.pub` |
|
|
||||||
| `ssh_key_public_host` | yaml | `/etc/ssh/id_ed25519.pub` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Хранилище
|
|
||||||
|
|
||||||
### `/home/oqyude/External` (ext4)
|
|
||||||
|
|
||||||
- `sapphira`: UUID `37e53ebc-5343-a94d-9fe2-0ca39e13a8de`, fsType `ext4`,
|
|
||||||
**без `nofail`**, **без automount** — обычный mount, без `x-systemd.automount`,
|
|
||||||
не помечен как `requiredBy local-fs.target` явно, но NixOS добавляет это для
|
|
||||||
всех `fileSystems` без `nofail` [?].
|
|
||||||
- `rydiwo`: не смонтирован (у ноутбука есть только NTFS `lamet-drive`).
|
|
||||||
- На других NixOS-хостах — не заявлен (нет внешнего диска).
|
|
||||||
|
|
||||||
### `/mnt/services` (bind)
|
|
||||||
|
|
||||||
- `server.nix:49-52`: `mkBindMount` от `xlib.dirs.services-folder`
|
|
||||||
(= `/home/oqyude/External/Services`) к `/mnt/services`, `bind,nofail`.
|
|
||||||
- `server.nix:61-63`: tmpfiles `z /mnt/services 0777 root root`.
|
|
||||||
- `vds/default.nix:23`: tmpfiles создаёт `/mnt/services` с правами `0755`.
|
|
||||||
- Используется сервисами на sapphira для bind-mount сервисных данных
|
|
||||||
(`mkServiceStorage`) и как прямой `stateDir` для gitea/memos/calibre-web/
|
|
||||||
immich/nextcloud/step-ca/trilium/uptime-kuma/3x-ui/tape-rotation.
|
|
||||||
|
|
||||||
### `/mnt/archive`, `/mnt/mobile`, `/mnt/lamet`, `/mnt/therima`, `/mnt/vetymae`, `/mnt/soptur`
|
|
||||||
|
|
||||||
- `archive` и `mobile` смонтированы на sapphira через `mkExfatMount`
|
|
||||||
(`nofail`+uid=1000).
|
|
||||||
- `lamet` — NTFS на rydiwo (`mask = "0000"`).
|
|
||||||
- `therima`, `vetymae`, `soptur` — **не** смонтированы нигде в репозитории
|
|
||||||
(см. вопрос 2.2).
|
|
||||||
- `dirs.nix` объявляет их все; `dirs.nix` **не** читать как список дисков этой
|
|
||||||
системы — там имена, часть из которых не существует.
|
|
||||||
|
|
||||||
### Потребители External-диска и порядок защиты
|
|
||||||
|
|
||||||
Включённые на sapphira сервисы с данными на `/mnt/services` или `/home/oqyude/External`:
|
|
||||||
|
|
||||||
- `postgresql`, `samba-smbd`, `homebox` (+setup), `gitea` (+dump),
|
|
||||||
`navidrome`, `syncthing`, `uptime-kuma`, `immich-server` (+ML),
|
|
||||||
`nextcloud`, `calibre-web`, `podman-3xui_app`, `podman-tape-rotation`
|
|
||||||
|
|
||||||
Все они обязаны иметь guard на `requiresMountsFor` (задача **B1** в `todo.md`).
|
|
||||||
Сейчас guard есть **только** у rsync-юнитов (`modules/server/systemd.nix:14,36`),
|
|
||||||
которые используют `--delete` и потенциально самые опасные при отсутствующем
|
|
||||||
диске.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Сеть и firewall
|
|
||||||
|
|
||||||
### Топология
|
|
||||||
|
|
||||||
```
|
|
||||||
Интернет (роутер, белый IP)
|
|
||||||
├── router NAT/proxy → sapphira: 443, 80, 22000, 8443, 22 (5 портов)
|
|
||||||
│
|
|
||||||
└── otreca (VPS): SSH только через Tailscale, не пробрасываем
|
|
||||||
|
|
||||||
LAN (192.168.1.0/24)
|
|
||||||
├── 192.168.1.20 = sapphira (домашний сервер)
|
|
||||||
├── 192.168.1.1 = роутер (gateway)
|
|
||||||
├── 192.168.1.100 = vetymae (Windows-хост; на нём — WSL NixOS = `wsl`)
|
|
||||||
├── 192.168.1.101, .102 = соседние машины (rsync/таблица в `termux.nix`)
|
|
||||||
└── ...
|
|
||||||
|
|
||||||
Tailscale (CGNAT 100.64.0.0/10)
|
|
||||||
├── 100.64.0.0 = sapphira (назначен вручную)
|
|
||||||
├── 100.64.1.0 = ещё один узел [?]
|
|
||||||
├── 100.86.62.4 = opencode на vetymae
|
|
||||||
└── 100.106.21.39 = miniflux на другом узле
|
|
||||||
```
|
|
||||||
|
|
||||||
`192.168.1.20` зашит в ~30 местах: `modules/server/{nginx,coredns,nfs,open-webui}.nix`,
|
|
||||||
`configurations/*`. `100.64.0.0` — в `nginx.nix`, `nextcloud.nix`,
|
|
||||||
`modules/vds/{nginx,systemd}.nix`.
|
|
||||||
|
|
||||||
### DNS (`modules/server/coredns.nix`)
|
|
||||||
|
|
||||||
Зоны `zeroq.su` (~17 записей) и `home.arpa` (~17) определены вручную.
|
|
||||||
Дублируют инвентарь сервисов: добавление сервиса = правка `coredns.nix` +
|
|
||||||
`nginx.nix` + самого модуля.
|
|
||||||
|
|
||||||
### Firewall
|
|
||||||
|
|
||||||
| Хост | `firewall.enable` | Фильтрация |
|
|
||||||
|---|---|---|
|
|
||||||
| sapphira | **false** (намеренно) | Роутер пробрасывает 5 портов: **443, 80, 22000, 8443, 22** |
|
|
||||||
| otreca | true | Самописанный nftables **без финального правила** → неявный accept; `firewall.interfaces.tailscale0.allowedTCPPorts = [22]` |
|
|
||||||
| wsl | false | WSL — не сетевой периметр |
|
|
||||||
| rydiwo, atoridu | default | `desktop` правила |
|
|
||||||
|
|
||||||
Следствия:
|
|
||||||
- На `sapphira` `openFirewall`/`allowedTCPPorts` не имеют эффекта.
|
|
||||||
- `nginx.nix:225` (`allowedTCPPorts = [80 443]`) — **мёртвое** правило.
|
|
||||||
- Допустимо `0.0.0.0` на любом сервисе sapphira — он не открывается в интернет
|
|
||||||
без проброса на роутере.
|
|
||||||
- На `otrecа` ruleset требует финальной политики (задача A3).
|
|
||||||
|
|
||||||
### SSH
|
|
||||||
|
|
||||||
`otreca` достижима только через Tailscale: `services.openssh.openFirewall = false`,
|
|
||||||
`firewall.interfaces.tailscale0.allowedTCPPorts = [22]`. Но при `nftables.enable`
|
|
||||||
с ручным ruleset это правило может не дойти до файрвола — проверить
|
|
||||||
`nft list ruleset` на otreca до правок (задача A3).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Сервисы
|
|
||||||
|
|
||||||
### Системные (sapphira, в `modules/server/default.nix:imports`)
|
|
||||||
|
|
||||||
Сервисы в `imports` + `state` + `roles`:
|
|
||||||
|
|
||||||
| Сервис | Файл | Порт | Данные | Guard? |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| acme (Let's Encrypt) | `modules/server/acme.nix` | — | `/var/lib/acme` | — |
|
|
||||||
| bentopdf | `bentopdf.nix` | — | — | — |
|
|
||||||
| builder (remote) | `builder.nix` | — | — | — (опция выключена) |
|
|
||||||
| calibre-web | `calibre-web.nix` | 8083 | `services-mnt-folder/calibre-web(-library)` | нужен B1 |
|
|
||||||
| chrony | `chrony.nix` | — | — | — |
|
|
||||||
| coredns | `coredns.nix` | 53 | inline zone | — |
|
|
||||||
| gitea | `gitea.nix` | 3000 | `services-mnt-folder/gitea` | нужен B1 |
|
|
||||||
| glances | `glances.nix` | — | — | — |
|
|
||||||
| homebox | `homebox.nix` | 7745 | `mkServiceStorage` | нужен B1 |
|
|
||||||
| immich | `immich.nix` | 2283 | `services-mnt-folder/immich` | нужен B1 |
|
|
||||||
| miniflux | `miniflux.nix` | 6061 | — | — |
|
|
||||||
| navidrome | `navidrome.nix` | 4533 | `server-home/Music` | нужен B1 |
|
|
||||||
| nextcloud | `nextcloud.nix` | 10000 | `services-mnt-folder/nextcloud` | нужен B1 |
|
|
||||||
| nginx | `nginx.nix` | 80/443 | proxy-only | — |
|
|
||||||
| nix-serve | `nix-serve.nix` | 5000 | — | — |
|
|
||||||
| onlyoffice | `onlyoffice.nix` | (через nginx) | — | — |
|
|
||||||
| postgresql | `postgresql.nix` | (local) | `mkServiceStorage` | нужен B1 |
|
|
||||||
| power | `power.nix` | — | — | — |
|
|
||||||
| samba | `samba.nix` | ? | `mkServiceStorage` | нужен B1 |
|
|
||||||
| syncthing | `syncthing.nix` | 8384 (gui), 22000 (data) | `server-home`, `storage/persist/...` | нужен B1 |
|
|
||||||
| systemd (rsync oneshots) | `systemd.nix` | — | источник/приёмник — оба на External | **уже есть guard** |
|
|
||||||
| uptime-kuma | `uptime-kuma.nix` | 4001 | `services-mnt-folder/uptime-kuma` | нужен B1 |
|
|
||||||
|
|
||||||
Закомментированы в `imports` (всё ещё живой код, потенциальный шум):
|
|
||||||
`remnawave, coturn, mealie, memos, minecraft, n8n, netdata, nfs, open-webui,
|
|
||||||
rsync, step-ca, stirling-pdf, transmission, trilium, zerotier` — см. задачу **E3**.
|
|
||||||
|
|
||||||
### Контейнеры (`modules/containers/`)
|
|
||||||
|
|
||||||
| Контейнер | Файл | Данные | Примечание |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 3x-ui | `3x-ui.nix` | `services-nodes-folder/<host>/3x-ui/{db,cert}` | **Заморожен**, см. ниже |
|
|
||||||
| tape-rotation | `tape-rotation.nix` | `services-nodes-folder/<host>/tape-rotation` | — |
|
|
||||||
| remnawave | `remnawave.nix` | `/mnt/services/containers/remnawave` | **закомментирован** в `server/default.nix` |
|
|
||||||
| remnanode | `remnanode.nix` | `/mnt/services/containers/remnanode` | — |
|
|
||||||
| kokoro-tts | `kokoro-tts.nix` | — | — |
|
|
||||||
| openhands | `openhands.nix` | — | — |
|
|
||||||
| remnawave-examples | `remnawave-examples/*.nix` | docker-compose | шаблоны |
|
|
||||||
|
|
||||||
### 3x-ui — замороженное состояние
|
|
||||||
|
|
||||||
Образ: `ghcr.io/mhsanaei/3x-ui:latest` (**не запинен**). Ядро Xray — на 26.7.x,
|
|
||||||
миграция на 26.9.x провалена. Панель может обновиться из upstream — поэтому:
|
|
||||||
- `podman-update-3xui_app` (`3x-ui.nix:80-90`) с `podman pull …:latest`
|
|
||||||
+ `systemctl restart` — **таймер закомментирован**.
|
|
||||||
- `podman.autoPrune.flags = ["--all"]` (`3x-ui.nix:45-47`) — потенциальный риск:
|
|
||||||
авто-prune может смести панель без коммита в репозиторий.
|
|
||||||
|
|
||||||
`reality443Forwarding = true` (`modules/vds/default.nix:19`) — следствие отката
|
|
||||||
`c8d4a12`; смысл утрачен, см. задачу **C5**.
|
|
||||||
|
|
||||||
### Nginx (`modules/server/nginx.nix`)
|
|
||||||
|
|
||||||
~12 vhost'ов через `mkProxy` для обратного проксирования сервисов на 192.168.1.20.
|
|
||||||
Плюс несколько hand-written:
|
|
||||||
|
|
||||||
- `nextcloud.private` — слушает на `100.64.0.0:10000` (= Tailscale sapphira),
|
|
||||||
`192.168.1.20:10000`, `127.0.0.1:10000`.
|
|
||||||
- `office.zeroq.su` — проксирует на nextcloud onlyoffice.
|
|
||||||
- `pdf.private` — слушает `0.0.0.0:80`, `100.64.0.0:8446`, `192.168.1.20:8446`,
|
|
||||||
`127.0.0.1:8446` (для Nextcloud PDF).
|
|
||||||
- `x.zeroq.su` — 3x-ui controller panel + `/subs/`, `/subsjs/`, `/clash/`.
|
|
||||||
- `zeroq.su` — корневой, заглушка + `/guest/` → LAN `:80`.
|
|
||||||
- `vetymae.opencodes.zeroq.su` → `100.86.62.4:4096`.
|
|
||||||
- `lamet.opencodes.zeroq.su` → `100.106.21.39:6061` — **порт miniflux**; либо
|
|
||||||
ошибка, либо так задумано [?] (см. вопрос 8.2).
|
|
||||||
- `opencode.zeroq.su` → `127.0.0.1:4096` (opencode-web на самом sapпира).
|
|
||||||
- `nextcloud.zeroq.su` → `192.168.1.20:10000`, `/whiteboard` → `:3002`.
|
|
||||||
|
|
||||||
`networking.firewall.allowedTCPPorts = [80 443]` (строка 225) — **мёртвое** правило
|
|
||||||
при `firewall.enable = false`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Неотвеченные вопросы
|
|
||||||
|
|
||||||
Слои 9–11 (home-manager, deploy, формат) оставлены для прочтения в
|
|
||||||
`docs/arch/invariants.md`. Неотвеченные вопросы слоёв 1–8:
|
|
||||||
|
|
||||||
| ID | Вопрос |
|
|
||||||
|---|---|
|
|
||||||
| 2.2 | `vetymae` / `lamet` / `therima` / `soptur` — те же машины или хосты вне репозитория? |
|
|
||||||
| 2.5 | `stateVersion` дрейфует 24.05 / 24.11 / 25.05 / 26.05 — намеренно? |
|
|
||||||
| 2.6 | Есть ли escape hatch для per-host отличий в `xlib`? |
|
|
||||||
| 3.2 | `any.nix` (minimal) действительно нуждается в home-manager + sops + disko? |
|
|
||||||
| 4.1 | Как root получает доступ по SSH — `authorizedKeys` для root в коде нет |
|
|
||||||
| 4.2 | Как разрешается цикл «ключ в секрете, а нужен для расшифровки»? |
|
|
||||||
| 4.3 | Все файлы в `secrets/` покрыты `path_regex`? |
|
|
||||||
| 4.4 | Как подключается вторая машина / второй человек при одном age-ключе? |
|
|
||||||
| 4.5 | `users.nix:87` — личный ключ или общий «ключ от деплоя»? |
|
|
||||||
| 5.1 | `/mnt/services` в режиме 0777 — осознанно? |
|
|
||||||
| 5.3 | NFS выключен, Samba работает — миграция? |
|
|
||||||
| 5.4 | NTFS-том `lamet-drive` с `mask = "0000"` — что на нём лежит? |
|
|
||||||
| 5.5 | `therima` / `vetymae` / `soptur` — несуществующие остатки или сетевые шары? |
|
|
||||||
| 5.6 | Где бэкапы БД и 3x-ui? |
|
|
||||||
| 6.6 | `192.168.1.20` зашит в 30 мест — считаем константой? |
|
|
||||||
| 6.7 | DNS дублирует инвентарь сервисов — как проверяем рассинхрон? |
|
|
||||||
| 6.8 | Публичные IP и SSH-алиасы в `home/termux.nix` — карта «хост → адреса» нужна? |
|
|
||||||
| 6.9 | Какой путь REALITY считается правильным? (→ C5) |
|
|
||||||
| 7.4 | Почему не публиковать весь диапазон 14380-15379? |
|
|
||||||
| 8.2 | `lamet.opencodes` → `:6061` — ошибка или так задумано? |
|
|
||||||
| 8.4 | `onlyoffice` — работает после трёх регрессов? |
|
|
||||||
| 8.5 | Что слушает `:3002` (`/whiteboard` в nextcloud)? |
|
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
# TODO: правки и инварианты
|
|
||||||
|
|
||||||
Источник: `docs/arch/invariants.md`. Ответы владельца от 2026-10-05 учтены.
|
|
||||||
Подтверждённые факты зафиксированы в `AGENTS.md` (корень) и `docs/arch/map.md`;
|
|
||||||
этот файл — только **незакрытые правки и неотвеченные вопросы**.
|
|
||||||
Порядок: A → B → C → D, потом E (документация для агента).
|
|
||||||
|
|
||||||
Обозначения: `[ ]` не начато, `[x]` сделано, `[!]` блокирует остальное.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Подтверждено (зафиксировано в `AGENTS.md` / `map.md`)
|
|
||||||
|
|
||||||
Эти инварианты уже учтены в ядре и карте — при правке кода опираться на
|
|
||||||
зафиксированные формулировки.
|
|
||||||
|
|
||||||
- **6.1** Явная финальная политика nftables на VDS → `todo A3` ещё открыто,
|
|
||||||
но сам «надо запилить» закреплён.
|
|
||||||
- **6.3** Firewall на sapphira выключен намеренно (граница — роутер, 5 портов:
|
|
||||||
22, 80, 443, 8443, 22000) → формулировка в `AGENTS.md §3`, `todo D1`.
|
|
||||||
- **6.5** `100.64.0.0` = Tailscale-адрес sapphira (назначен вручную) → `AGENTS.md §5`,
|
|
||||||
`map.md §Сеть и firewall`.
|
|
||||||
- **7.1 / 7.2** 3x-ui заморожен: панель на latest, ядро Xray на 26.7.x,
|
|
||||||
миграция 26.9 провалена → `AGENTS.md §4`, `todo C1–C5`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ловушки для агента: выглядит сломанным, но это намеренно
|
|
||||||
|
|
||||||
Прежде чем чинить — проверить этот список. Здесь лежат решения, которые
|
|
||||||
иначе «поправляются» обратно и ломают рабочую систему.
|
|
||||||
|
|
||||||
| Где | Что выглядит ошибкой | На самом деле |
|
|
||||||
|---|---|---|
|
|
||||||
| `configurations/server.nix:130` | `networking.firewall.enable = false` на сервере с 20 сервисами на `0.0.0.0` | Намеренно: фильтр на роутере, он пробрасывает 5 портов (см. D1) |
|
|
||||||
| `configurations/mobile.nix:95`, `wsl.nix:59` | `stateVersion` 24.05 / 24.11 против 26.05 у остальных | Каждый хост зафиксирован на своей версии; не «подровнять» |
|
|
||||||
| `modules/users.nix:66` | `uid = if hostname == "sapphira" then 1001 else …` с пометкой TODO | Осознанный костыль под старый uid 1000 = `yuyus`; удалять только после миграции ФС |
|
|
||||||
| `modules/containers/3x-ui.nix:54` | `image = …:latest` | Панель намеренно на последней версии; **ядро** Xray — на 26.7.x, миграция на 26.9 провалена |
|
|
||||||
| `modules/containers/3x-ui.nix:33-35` | `reality443Forwarding = true` на VDS при откате nginx-stream | Следствие отката `c8d4a12`; смысл утрачен, но опция объявлена — см. C5 |
|
|
||||||
| `modules/server/default.nix:33-47` | 15 закомментированных модулей с живым кодом | Отключены осознанно; см. E3 |
|
|
||||||
| `modules/server/{mealie,memos,n8n,netdata,nfs,open-webui,rsync,step-ca,transmission,trilium,zerotier}.nix` | Агент насчитает лишние порты и каталоги | Модули вне `imports` = мёртвый код |
|
|
||||||
| `home/modules/opencode.nix:339` | `systemd.user.services.opencode-web.Service` вместо привычного `serviceConfig` | `serviceConfig` рендерится в секцию `[serviceConfig]`, которую systemd **молча игнорирует** (`c73a698`) |
|
|
||||||
| `configurations/vds.nix:73-91` | nftables без финального правила | Известный пробел,см. A3 — **не** «случайно потерялось» |
|
|
||||||
| `100.64.0.0` в `nginx.nix`, `nextcloud.nix`, `vds/*` | Первый адрес CGNAT `/10`, похож на сетевой | Tailscale-адрес sapphira, назначен вручную |
|
|
||||||
| `server.nix:61-63` | `z /mnt/services 0777` | World-writable точка монтирования; см. B1 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## A. Блокеры: сломано или не защищено
|
|
||||||
|
|
||||||
### [ ] A1. `mobile.nix` импортирует несуществующий файл
|
|
||||||
|
|
||||||
**Где:** `configurations/mobile.nix:12`
|
|
||||||
```nix
|
|
||||||
xlib = import ../lib/xlib.nix { lib = inputs.nixpkgs.lib; };
|
|
||||||
```
|
|
||||||
Файла `lib/xlib.nix` нет — есть каталог `lib/xlib/` с `default.nix`.
|
|
||||||
**Правка** (как в `configurations/default.nix:5`):
|
|
||||||
```nix
|
|
||||||
xlib = import ../lib/xlib { inherit lib; };
|
|
||||||
```
|
|
||||||
**Следствие:** до правки `nixOnDroidConfigurations.epral` и `.default`
|
|
||||||
не вычисляются. Устройство `epral` мертво.
|
|
||||||
**Проверка:**
|
|
||||||
```
|
|
||||||
nix eval --raw .#nixOnDroidConfigurations.epral.config.environment.etcBackupExtension # ожидается .bak
|
|
||||||
```
|
|
||||||
|
|
||||||
### [ ] A2. Убедиться, что `nix flake check` вообще запускается
|
|
||||||
|
|
||||||
**Где:** нет CI; `checks` в `deploy/default.nix:27-29` покрывают только deploy.
|
|
||||||
**Сначала проверить**, ловит ли текущий `nix flake check` поломку из A1:
|
|
||||||
```
|
|
||||||
nix flake check
|
|
||||||
```
|
|
||||||
Ожидание, которое надо подтвердить: он **уже падает** на `epral`, то есть
|
|
||||||
проверка существует, но её не запускали. Если падает — A1 и был бы замечен.
|
|
||||||
**Проверка после A1:** та же команда должна стать зелёной.
|
|
||||||
**Затем** (E2) — превратить в привычку: прогонять перед каждым коммитом.
|
|
||||||
|
|
||||||
### [ ] A3. Явная финальная политика nftables на VDS
|
|
||||||
|
|
||||||
**Где:** `configurations/vds.nix:73-91`
|
|
||||||
**Сначала диагностика на otreca** (без неё править опасно — можно отрезать SSH):
|
|
||||||
```
|
|
||||||
nft list ruleset
|
|
||||||
systemctl status nftables firewall-nftables
|
|
||||||
```
|
|
||||||
Нужно понять, кто реально владеет набором правил: `nftables.enable = true` с
|
|
||||||
собственным ruleset **и** `networking.firewall.*` включены одновременно
|
|
||||||
(инвариант 6.2). Затем — править **один** механизм, не оба.
|
|
||||||
**Что должно получиться** (политика — на выбор владельца, два варианта):
|
|
||||||
```
|
|
||||||
# Вариант «белый список» (предпочтительно):
|
|
||||||
chain input {
|
|
||||||
type filter hook input priority 0; policy drop;
|
|
||||||
iif lo accept
|
|
||||||
ct state established,related accept
|
|
||||||
iif "tailscale0" accept
|
|
||||||
tcp dport { 80, 443 } ct state new limit rate 20/second burst 40 packets accept
|
|
||||||
tcp dport { 22 } ct state new accept # только если 22 нужен на ens3
|
|
||||||
}
|
|
||||||
# Вариант «мягкий» (минимум изменений, фиксирует текущее поведение):
|
|
||||||
chain input {
|
|
||||||
type filter hook input priority 0;
|
|
||||||
iif lo accept
|
|
||||||
ct state established,related accept
|
|
||||||
tcp dport { 80, 443 } ct state new limit rate 20/second burst 40 packets accept
|
|
||||||
tcp dport { 80, 443 } ct state new drop
|
|
||||||
# финал accept — но ТОЛЬКО как явно помеченное «разрешено всё остальное»:
|
|
||||||
iif "ens3" accept comment "PROVISIONAL: explicit allow-all, см. A3"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Инвариант к записи:** последнее правило самописной цепочки всегда явное.
|
|
||||||
**Проверка:** `nft list chain inet filter input` + `ssh` с внешнего адреса.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## B. Защита данных
|
|
||||||
|
|
||||||
### [ ] B1. Guard на несмонтированный носитель `/mnt/services`
|
|
||||||
|
|
||||||
**Где:** `lib/xlib/helpers.nix` (`mkServiceStorage`), потребители —
|
|
||||||
`modules/server/{postgresql,n8n,samba,homebox,minecraft}.nix` + `modules/containers/3x-ui.nix`
|
|
||||||
**Проблема (подтверждена владельцем как не продуманная):** `mkServiceStorage`
|
|
||||||
даёт `bind,x-systemd.automount,nofail`. Если диск `External` (`xlib.dirs.server-home`,
|
|
||||||
ext4 по UUID, `configurations/server.nix:55-58`) не смонтирован, то `/mnt/services`
|
|
||||||
— обычный каталог, `/var/lib/<service>` пуст, и сервис **молча** стартует на чистой
|
|
||||||
базе. Пользователь увидит «потерялись данные».
|
|
||||||
**Решение (рекомендую):** добавить в `xlib/helpers.nix`
|
|
||||||
```nix
|
|
||||||
mkStorageGuard =
|
|
||||||
{ dir }:
|
|
||||||
{
|
|
||||||
# сервис не стартует, пока /mnt/services не смонтирован:
|
|
||||||
# Requires+After на mnt-services.mount, который упадёт, если нет источника
|
|
||||||
requiresMountsFor = [ dir ];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
и в каждом потребителе:
|
|
||||||
```nix
|
|
||||||
systemd.services.postgresql = xlib.helpers.mkStorageGuard { dir = xlib.dirs.services-mnt-folder; };
|
|
||||||
```
|
|
||||||
**Важно — не проверять `ConditionPathIsMountPoint=/mnt/services`:** bind-mount
|
|
||||||
внутри одной ФС не меняет `st_dev`, условие вернёт false даже при корректном
|
|
||||||
монтировании. Надёжны `requiresMountsFor` или `ConditionPathIsMountPoint` на
|
|
||||||
`xlib.dirs.server-home` (там `st_dev` действительно другой).
|
|
||||||
**Плюс операционная строка в `AGENTS.md`:** перед рестартом этих сервисов —
|
|
||||||
`findmnt /mnt/services`.
|
|
||||||
**Проверка (имитация отказа):**
|
|
||||||
```
|
|
||||||
systemctl stop postgresql
|
|
||||||
sudo umount /mnt/services # или остановить automount
|
|
||||||
systemctl start postgresql # ожидается FAIL, а не пустая база
|
|
||||||
```
|
|
||||||
|
|
||||||
### [ ] B2. Зафиксировать, что бэкапов в конфигурации нет
|
|
||||||
|
|
||||||
**Где:** `modules/server/postgresql.nix:23` (`postgresqlBackup.enable` закомментирован),
|
|
||||||
бэкап-сервиса в репозитории нет вообще; БД 3x-ui — sqlite на том же диске.
|
|
||||||
**Задача — не код, а запись:** в `AGENTS.md` и `invariants.md` явно сказать,
|
|
||||||
что бэкапы ведутся вне Nix. Иначе агент считает конфиг самодостаточным.
|
|
||||||
**Ждёт ответа:** где бэкапы и как их проверять (инвариант 5.6).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## C. 3x-ui: заморозить рабочее состояние
|
|
||||||
|
|
||||||
### [ ] C1. Вернуть расследование, потерянное при откате
|
|
||||||
|
|
||||||
**Где:** 200 строк удалены коммитом `22a19be`.
|
|
||||||
**Восстановить и дополнить выводом:**
|
|
||||||
```
|
|
||||||
git show 9974784:modules/containers/3x-ui-migration-notes.md > docs/arch/notes/3x-ui-xray-26.9.md
|
|
||||||
```
|
|
||||||
Дописать в конец: вердикт — миграция ядра 26.7 → 26.9 **провалена**, откат на
|
|
||||||
рабочее состояние (панель последняя, ядро 26.7.x), обходные скрипты отключены
|
|
||||||
осознанно; причина отказа — обязательный постквантовый обмен X25519MLKEM768,
|
|
||||||
ломающий старых клиентов.
|
|
||||||
**Инвариант:** откат кода не удаляет расследование; заметка живёт в
|
|
||||||
`docs/arch/notes/`, а не рядом с откатываемым файлом.
|
|
||||||
|
|
||||||
### [ ] C2. Зафиксировать фактические версии панели и ядра
|
|
||||||
|
|
||||||
**Где:** `modules/containers/3x-ui.nix:54`
|
|
||||||
**Сначала узнать, что реально работает** (на sapphira и на otreca):
|
|
||||||
```
|
|
||||||
podman images --format '{{.Repository}}:{{.Tag}} {{.Id}} {{.Created}}' | grep 3x-ui
|
|
||||||
podman inspect ghcr.io/mhsanaei/3x-ui --format '{{index .RepoDigests 0}}'
|
|
||||||
podman exec 3xui_app /app/bin/xray-linux-amd64 version
|
|
||||||
```
|
|
||||||
**Потом** заменить `:latest` на найденный тег (или digest) в коде.
|
|
||||||
**Инвариант:** образы контейнеров запинены; `latest` запрещён — обновление
|
|
||||||
образа это правка в коде, а не `podman pull` на хосте.
|
|
||||||
**Почему срочно:** `podman.autoPrune.flags = ["--all"]` (`3x-ui.nix:45-47`) +
|
|
||||||
`:latest` = рабочее состояние может смениться без единого коммита.
|
|
||||||
|
|
||||||
### [ ] C3. Убрать сервис автообновления 3x-ui
|
|
||||||
|
|
||||||
**Где:** `modules/containers/3x-ui.nix:80-90` (`podman-update-3xui_app` с
|
|
||||||
`podman pull … :latest`) и закомментированный таймер (строка 97-103).
|
|
||||||
**Предложение:** удалить сервис целиком, оставив комментарий-предупреждение.
|
|
||||||
Обновление панели через `pull` — ровно тот путь, которым в 2026-10-04
|
|
||||||
декларация разошлась с рантаймом; автоматизировать его нельзя.
|
|
||||||
**Инвариант:** ни один контейнер в этом репозитории не обновляется сам.
|
|
||||||
|
|
||||||
### [ ] C4. Записать в AGENTS.md, что ядро Xray — состояние панели, а не Nix
|
|
||||||
|
|
||||||
Версия ядра выбирается в UI панели и лежит в её sqlite-БД, то есть **вне** Nix.
|
|
||||||
Репозиторий не может её гарантировать.
|
|
||||||
**Операционное правило:** перед деплоем/рестартом 3x-ui проверять версию ядра
|
|
||||||
в панели; обновление ядра = отдельная задача с записью в
|
|
||||||
`docs/arch/notes/`, а не молчаливый `podman pull`.
|
|
||||||
|
|
||||||
### [ ] C5. Решить судьбу `reality443Forwarding`
|
|
||||||
|
|
||||||
**Где:** `modules/vds/default.nix:19` (`= true`), `modules/options.nix:66-75`,
|
|
||||||
`modules/containers/3x-ui.nix:33-35`.
|
|
||||||
Состояние после отката `c8d4a12`: опция включена, поэтому на otreca
|
|
||||||
пробрасывается `127.0.0.1:15380:443`, тогда как единственный Reality-инбаунд
|
|
||||||
контейнера слушает 8443, а публичный 8443 проброшен напрямую (`0.0.0.0:8443`).
|
|
||||||
Потребителя потока (nginx-stream) откат убрал.
|
|
||||||
**Варианты:** (а) оставить как есть и описать в инвариантах; (б) погасить опцию
|
|
||||||
в `vds/default.nix` и убрать её из `options.nix`; (в) довести до рабочего
|
|
||||||
состояния. **Ждёт решения** — связано с 6.9.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## D. Сетевая граница: записать то, чего нет в репозитории
|
|
||||||
|
|
||||||
### [ ] D1. Пробросы роутера — главный недостающий инвариант
|
|
||||||
|
|
||||||
Ответ владельца: на сервер пробрасываются **443, 80, 22000 (syncthing),
|
|
||||||
8443 (xray), 22 (ssh)**. Это **настоящая граница доверия**, и она живёт
|
|
||||||
в конфиге роутера, то есть вне репозитория.
|
|
||||||
**Записать в двух местах:** `docs/arch/invariants.md` (слой 6) и `AGENTS.md`.
|
|
||||||
Формулировка инварианта:
|
|
||||||
> Экспозиция наружу определяется пробросами на роутере, не `openFirewall`.
|
|
||||||
> На `sapphira` `networking.firewall.enable = false` намеренно.
|
|
||||||
> Список пробросов: 22, 80, 443, 8443 (3x-ui/Xray REALITY), 22000 (syncthing).
|
|
||||||
> Новый сервис не становится доступен из интернета, пока не добавлен проброс.
|
|
||||||
> `networking.firewall.*` на `sapphira` не имеет эффекта.
|
|
||||||
|
|
||||||
### [ ] D2. Зафиксировать `100.64.0.0` как Tailscale-адрес sapphira
|
|
||||||
|
|
||||||
Моё прежнее замечание («сеть вместо адреса») было неверным — адрес назначен
|
|
||||||
вручную. Записать как факт + список из 4 мест, которые придётся править при
|
|
||||||
смене: `modules/server/nginx.nix`, `modules/server/nextcloud.nix`,
|
|
||||||
`modules/vds/systemd.nix`, `modules/vds/nginx.nix`.
|
|
||||||
**Опционально (отложено):** вынести `192.168.1.20` в `xlib.dirs` — сейчас
|
|
||||||
зашит в ~30 местах в 6 файлах. Не срочно, это рефакторинг.
|
|
||||||
|
|
||||||
### [ ] D3. Убрать мёртвое правило firewall
|
|
||||||
|
|
||||||
**Где:** `modules/server/nginx.nix:225-228` — `allowedTCPPorts = [80 443]`
|
|
||||||
не действует при `firewall.enable = false` (`server.nix:130`).
|
|
||||||
Удалить или пометить комментарием «депенит от D1».
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## E. Документация для агента (после прохода по invariants.md)
|
|
||||||
|
|
||||||
### [ ] E1. Написать `AGENTS.md` в корне
|
|
||||||
Собирается из подтверждённых инвариантов. Структура: карта хостов →
|
|
||||||
что где лежит → инварианты (нарушишь = сломает) → ловушки из таблицы выше →
|
|
||||||
команды проверки. Ожидаемый бюджет — до 150 строк.
|
|
||||||
|
|
||||||
### [ ] E2. Выбрать проверки, которые заменят половину инвариантов
|
|
||||||
Кандидаты из инварианта 11.2:
|
|
||||||
1. ни одного `:latest` в образах (grep по `image =`);
|
|
||||||
2. `nix flake check` зелёный — уже ловит A1;
|
|
||||||
3. домены в `coredns.nix` ↔ vhost'ы в `nginx.nix` совпадают в обе стороны;
|
|
||||||
4. для каждого потребителя `mkServiceStorage` каталог существует на `External`;
|
|
||||||
5. последнее правило самописной nftables-цепочки явное;
|
|
||||||
6. `listen.addr` — адрес интерфейса, а не сеть;
|
|
||||||
7. все файлы в `secrets/` матчат `path_regex` из `.sops.yaml`.
|
|
||||||
**Ждёт ответа:** какие из них делать, какие — избыточны.
|
|
||||||
|
|
||||||
### [ ] E3. Судьба 15 закомментированных модулей
|
|
||||||
`modules/server/default.nix:33-47` — `remnawave, coturn, mealie, memos,
|
|
||||||
minecraft, n8n, netdata, nfs, open-webui, rsync, step-ca, stirling-pdf,
|
|
||||||
transmission, trilium, zerotier`. Удалить или оставить как референс?
|
|
||||||
Они мешают агенту насчитывать порты и каталоги, которых нет.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## F. Ждут ответа (блокируют E1)
|
|
||||||
|
|
||||||
Индексы в `docs/arch/invariants.md`:
|
|
||||||
|
|
||||||
| № | Вопрос, который блокирует запись инварианта |
|
|
||||||
|---|---|
|
|
||||||
| 2.2 | `vetymae` / `lamet` / `therima` / `soptur` — это те же машины или хосты вне репозитория? |
|
|
||||||
| 2.5 | `stateVersion` дрейфует 24.05 / 24.11 / 25.05 / 26.05 — намеренно? |
|
|
||||||
| 2.6 | Есть ли escape hatch для per-host отличий в `xlib`, или «у всех хостов одно» — закон? |
|
|
||||||
| 3.2 | `any.nix` (minimal) действительно нуждается в home-manager + sops + disko? |
|
|
||||||
| 4.1 | Как root получает доступ по SSH — `authorizedKeys` для root в коде нет |
|
|
||||||
| 4.2 | Как разрешается цикл «ключ `/etc/ssh/id_ed25519` лежит внутри секрета, а нужен для расшифровки» |
|
|
||||||
| 4.3 | Что лежит в `secrets/`, все ли файлы покрыты `path_regex` |
|
|
||||||
| 4.4 | Как подключается вторая машина / второй человек при одном age-ключе |
|
|
||||||
| 4.5 | `users.nix:87` — личный ключ или общий «ключ от деплоя» |
|
|
||||||
| 5.1 | `/mnt/services` в режиме 0777 — осознанно? |
|
|
||||||
| 5.3 | NFS выключен, Samba работает — миграция? |
|
|
||||||
| 5.4 | NTFS-том `lamet-drive` с `mask = "0000"` — что на нём лежит |
|
|
||||||
| 5.5 | `therima` / `vetymae` / `soptur` — несуществующие остатки или сетевые шары |
|
|
||||||
| 5.6 | Где бэкапы БД и 3x-ui (→ B2) |
|
|
||||||
| 6.6 | `192.168.1.20` зашит в 30 мест — считаем константой? |
|
|
||||||
| 6.7 | DNS дублирует инвентарь сервисов — как проверяем рассинхрон |
|
|
||||||
| 6.8 | Публичные IP и SSH-алиасы в `home/termux.nix` — карта «хост → адреса» нужна? |
|
|
||||||
| 6.9 | Какой путь REALITY считается правильным (→ C5) |
|
|
||||||
| 7.4 | Почему не публиковать весь диапазон 14380-15379 |
|
|
||||||
| 8.2 | `lamet.opencodes` → `:6061` — это miniflux; ошибка или так задумано |
|
|
||||||
| 8.4 | `onlyoffice` — работает после трёх регрессов? |
|
|
||||||
| 8.5 | Что слушает `:3002` (`/whiteboard` в nextcloud) |
|
|
||||||
| 9.2 | Кто создаёт `~/Music` и `~/Storage` при `createDirectories = false` |
|
|
||||||
| 10.1 | Почему `deploy-rs` не деплоит `atoridu`, `wsl`, `epral` |
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{ inputs, ... }@flakeContext:
|
||||||
|
{
|
||||||
|
nixOnDroidConfigurations = {
|
||||||
|
epral = import ./epral.nix flakeContext; # epral (Android via nix-on-droid)
|
||||||
|
# Alias so a plain `nix-on-droid switch` from a local clone
|
||||||
|
# (~/.config/nix-on-droid) picks up the device config without `#epral`.
|
||||||
|
default = import ./epral.nix flakeContext;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ let
|
|||||||
# (essentials, users.nix, home-manager, sops-nix, disko, grub2-themes)
|
# (essentials, users.nix, home-manager, sops-nix, disko, grub2-themes)
|
||||||
# and nixpkgs.overlays are skipped so it evaluates under nix-on-droid's
|
# and nixpkgs.overlays are skipped so it evaluates under nix-on-droid's
|
||||||
# module system (class = "nixOnDroid").
|
# module system (class = "nixOnDroid").
|
||||||
xlib = import ../lib/xlib.nix { lib = inputs.nixpkgs.lib; };
|
|
||||||
nixOnDroidModule =
|
nixOnDroidModule =
|
||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
@@ -19,19 +18,19 @@ let
|
|||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
inputs.self.nixosModules.strict
|
inputs.self.nixosModules.default # base defaultModule (termux-trimmed)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
xlib.device = {
|
||||||
|
type = "termux";
|
||||||
|
hostname = "epral";
|
||||||
|
};
|
||||||
|
|
||||||
# Login shell. nix-on-droid writes /etc/passwd from user.shell on every
|
# Login shell. nix-on-droid writes /etc/passwd from user.shell on every
|
||||||
# activation, so `chsh` is useless here — set it in nix instead.
|
# activation, so `chsh` is useless here — set it in nix instead.
|
||||||
# (default is bashInteractive)
|
# (default is bashInteractive)
|
||||||
user.shell = "${pkgs.zsh}/bin/zsh";
|
user.shell = "${pkgs.zsh}/bin/zsh";
|
||||||
|
|
||||||
# SSH user (matches the `User` entries in the client's ~/.ssh/config).
|
|
||||||
# Default is "nix-on-droid"; home stays at the read-only
|
|
||||||
# /data/data/com.termux.nix/files/home either way.
|
|
||||||
user.userName = xlib.device.username;
|
|
||||||
|
|
||||||
# Minimal termux settings (nix-on-droid options only:
|
# Minimal termux settings (nix-on-droid options only:
|
||||||
# environment.*, nix.*, time.*, user.*, system.*, android-integration.*)
|
# environment.*, nix.*, time.*, user.*, system.*, android-integration.*)
|
||||||
|
|
||||||
@@ -45,26 +44,26 @@ let
|
|||||||
nano
|
nano
|
||||||
|
|
||||||
# Some common stuff that people expect to have
|
# Some common stuff that people expect to have
|
||||||
bzip2
|
openssh
|
||||||
|
treefmt
|
||||||
|
git
|
||||||
|
procps
|
||||||
|
psmisc # provides killall (attr `killall` was removed from nixpkgs)
|
||||||
diffutils
|
diffutils
|
||||||
findutils
|
findutils
|
||||||
git
|
util-linux # renamed from utillinux
|
||||||
|
tzdata
|
||||||
|
hostname
|
||||||
|
man
|
||||||
gnugrep
|
gnugrep
|
||||||
gnupg
|
gnupg
|
||||||
gnused
|
gnused
|
||||||
gnutar
|
gnutar
|
||||||
|
bzip2
|
||||||
gzip
|
gzip
|
||||||
hostname
|
|
||||||
man
|
|
||||||
ncurses
|
|
||||||
openssh
|
|
||||||
procps
|
|
||||||
psmisc # provides killall (attr `killall` was removed from nixpkgs)
|
|
||||||
treefmt
|
|
||||||
tzdata
|
|
||||||
unzip
|
|
||||||
util-linux # renamed from utillinux
|
|
||||||
zip
|
zip
|
||||||
|
unzip
|
||||||
|
ncurses
|
||||||
];
|
];
|
||||||
|
|
||||||
# Backup etc files instead of failing to activate generation if a file already exists in /etc
|
# Backup etc files instead of failing to activate generation if a file already exists in /etc
|
||||||
@@ -73,22 +72,15 @@ let
|
|||||||
# Shared userspace home-manager config (same cozy shell as on NixOS hosts).
|
# Shared userspace home-manager config (same cozy shell as on NixOS hosts).
|
||||||
# nix-on-droid forces home.username / home.homeDirectory from user.*,
|
# nix-on-droid forces home.username / home.homeDirectory from user.*,
|
||||||
# so the strict module must not set them.
|
# so the strict module must not set them.
|
||||||
# xlib is injected via home-manager.extraSpecialArgs (the HM submodule
|
|
||||||
# does not inherit the nix-on-droid module args).
|
|
||||||
home-manager = {
|
home-manager = {
|
||||||
useGlobalPkgs = true;
|
useGlobalPkgs = true;
|
||||||
backupFileExtension = "hm-bak";
|
backupFileExtension = "hm-bak";
|
||||||
extraSpecialArgs = {
|
config = { ... }: {
|
||||||
inherit xlib;
|
imports = [
|
||||||
|
../home/shared/strict.nix
|
||||||
|
];
|
||||||
|
home.stateVersion = "24.05";
|
||||||
};
|
};
|
||||||
config =
|
|
||||||
{ ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../home/termux.nix
|
|
||||||
];
|
|
||||||
home.stateVersion = "24.05";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# Read the changelog before changing this value
|
# Read the changelog before changing this value
|
||||||
@@ -103,9 +95,6 @@ let
|
|||||||
time.timeZone = "Europe/Moscow";
|
time.timeZone = "Europe/Moscow";
|
||||||
|
|
||||||
android-integration.termux-setup-storage.enable = true;
|
android-integration.termux-setup-storage.enable = true;
|
||||||
|
|
||||||
# Provides `am` (termux-am) — required by termux-api's broadcast backend.
|
|
||||||
android-integration.am.enable = true;
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
inputs.nix-on-droid.lib.nixOnDroidConfiguration {
|
inputs.nix-on-droid.lib.nixOnDroidConfiguration {
|
||||||
@@ -116,13 +105,6 @@ inputs.nix-on-droid.lib.nixOnDroidConfiguration {
|
|||||||
nixOnDroidModule
|
nixOnDroidModule
|
||||||
];
|
];
|
||||||
extraSpecialArgs = {
|
extraSpecialArgs = {
|
||||||
# `xlib` is the same value shape NixOS hosts get (lib/mkSystem.nix);
|
deviceType = "termux";
|
||||||
# the hostname lives here because nixOnDroidConfigurations is keyed by
|
|
||||||
# both "epral" and the "default" alias, so it cannot come from the
|
|
||||||
# attribute name.
|
|
||||||
xlib = xlib.mkXlib {
|
|
||||||
hostname = "epral";
|
|
||||||
type = "termux";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Generated
+182
-199
@@ -13,11 +13,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1789404474,
|
"lastModified": 1785518313,
|
||||||
"narHash": "sha256-UXFQ7tFiwn8sPz0EV4CBB2PCf/ZiGIHWn/6MXk81Lxs=",
|
"narHash": "sha256-anlq3YQDCsNrkNlu3HTg4dEIpRugwnyAVUxoPcBmA/U=",
|
||||||
"owner": "serokell",
|
"owner": "serokell",
|
||||||
"repo": "deploy-rs",
|
"repo": "deploy-rs",
|
||||||
"rev": "e760371d631165e7d8de5b0dcf148e21ec4c16f0",
|
"rev": "b974715a27b49fadbf3bf6d85e26bcb3109daa6d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1789770686,
|
"lastModified": 1781152676,
|
||||||
"narHash": "sha256-uZkBR7yHdIKUFB5SZdfgh1qkGfI3XmYmI/lTiquxbck=",
|
"narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "725ea35e410ad83be4931d1bff7e090eacaf3563",
|
"rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -61,24 +61,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-parts": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs-lib": "nixpkgs-lib"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1788450739,
|
|
||||||
"narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=",
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "flake-parts",
|
|
||||||
"rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "flake-parts",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"grub2-themes": {
|
"grub2-themes": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -86,11 +68,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788271742,
|
"lastModified": 1757136219,
|
||||||
"narHash": "sha256-H4IIqM+fmq9t3ZPHGs9kiuoQzau9AhCGQBSfClqQ/44=",
|
"narHash": "sha256-tKU+vq34KHu/A2wD7WdgP5A4/RCmSD8hB0TyQAUlixA=",
|
||||||
"owner": "vinceliuice",
|
"owner": "vinceliuice",
|
||||||
"repo": "grub2-themes",
|
"repo": "grub2-themes",
|
||||||
"rev": "4c5a77125b93f833edc9bf7b14a899faa8ac79c6",
|
"rev": "80dd04ddf3ba7b284a7b1a5df2b1e95ee2aad606",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -106,11 +88,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1790128814,
|
"lastModified": 1785531816,
|
||||||
"narHash": "sha256-6Gm9q+wW3E4Ey4F6wEbJAwaMsEK6hvCYfTW7yY64ZfA=",
|
"narHash": "sha256-vkMnV0JIyw+g/NmcfoajlGaAO+9a0ezia+FZohQJrik=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "0b2f1129177f70c5f0f5d88bb53c49ca47d0bfc0",
|
"rev": "bf9ce9fec78f95f374e8dd3b503863a3ec128ebe",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -119,45 +101,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"justray": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-parts": [
|
|
||||||
"flake-parts"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1790165840,
|
|
||||||
"narHash": "sha256-nQ3uCXiUGTLD/UtuChc2XlStYg9a33PYrkDitmmqxW8=",
|
|
||||||
"owner": "luynrs",
|
|
||||||
"repo": "justray",
|
|
||||||
"rev": "4073f3fb223613e4d780dafad6f93b5c266061a2",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "luynrs",
|
|
||||||
"repo": "justray",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nfqws2-keenetic": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1789144514,
|
|
||||||
"narHash": "sha256-G+LvvXDqzYm8SOXNc3N+HIzvD9DR3J+WT+HHDdmjB0Y=",
|
|
||||||
"owner": "nfqws",
|
|
||||||
"repo": "nfqws2-keenetic",
|
|
||||||
"rev": "fa22c177b340e73d8b8a94b295af20e0228c4c22",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nfqws",
|
|
||||||
"repo": "nfqws2-keenetic",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nix-formatter-pack": {
|
"nix-formatter-pack": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -184,32 +127,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nix-minecraft": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-compat": [
|
|
||||||
"flake-compat"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"systems": [
|
|
||||||
"nix-systems"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1790137578,
|
|
||||||
"narHash": "sha256-luzO2Bo/RxUHqTPxBttSB1QVzM9Y5s+Iwpip6SjWdWo=",
|
|
||||||
"owner": "Infinidoge",
|
|
||||||
"repo": "nix-minecraft",
|
|
||||||
"rev": "2e6a1d1ceb4da6b6ffb3980bc7993b3d057cd4db",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "Infinidoge",
|
|
||||||
"repo": "nix-minecraft",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nix-on-droid": {
|
"nix-on-droid": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"home-manager": [
|
"home-manager": [
|
||||||
@@ -238,33 +155,16 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nix-systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixos-hardware": {
|
"nixos-hardware": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": "nixpkgs"
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1789978172,
|
"lastModified": 1785232496,
|
||||||
"narHash": "sha256-FIRXajv1pPZ+l6On6ekkmcQ6le0Zi0ncRUoR3nB0vKA=",
|
"narHash": "sha256-65EQYIRRpTdpH8lUiB6Mvo5uBkG60aBIzAJuALfx+O0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixos-hardware",
|
"repo": "nixos-hardware",
|
||||||
"rev": "9ebcb7766700d7e006d9505247bd7ce0426f4232",
|
"rev": "2e790b0a6be8ec2b76174ac0931b8ff11919ec98",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -284,11 +184,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1789164534,
|
"lastModified": 1784642409,
|
||||||
"narHash": "sha256-DoYGPM6QpnYBLWj9gGw6ZwAzIX+HrAVov1BoT+8Jixo=",
|
"narHash": "sha256-hcbDqFuySAJawljt5r0sKBCJKYnbtGD0T/ZIozH1Dq0=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "NixOS-WSL",
|
"repo": "NixOS-WSL",
|
||||||
"rev": "72c92b11bb8289e6651c7fef29cc0a885fd6a255",
|
"rev": "eaeb18da90024448a60eb1ec7132eafa4003404e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -300,17 +200,46 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1790046670,
|
"lastModified": 1767892417,
|
||||||
"narHash": "sha256-MYiI+CzL0tuWgRPjGsKCDHqYs2T3OzMlMQWOYWG0qso=",
|
"narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
|
||||||
|
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-beets": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1774610258,
|
||||||
|
"narHash": "sha256-HaThtroVD9wRdx7KQk0B75JmFcXlMUoEdDFNOMOlsOs=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "6774f7bc253789b113a4f39285dc0fa100abeacc",
|
"rev": "832efc09b4caf6b4569fbf9dc01bec3082a00611",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
|
"rev": "832efc09b4caf6b4569fbf9dc01bec3082a00611",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-calibre": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776255774,
|
||||||
|
"narHash": "sha256-psVTpH6PK3q1htMJpmdz1hLF5pQgEshu7gQWgKO6t6Y=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "566acc07c54dc807f91625bb286cb9b321b5f42a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "566acc07c54dc807f91625bb286cb9b321b5f42a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -346,18 +275,83 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-lib": {
|
"nixpkgs-master": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788057806,
|
"lastModified": 1785744216,
|
||||||
"narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=",
|
"narHash": "sha256-DoNu3f4opSpQOTSaC/SYzSDDmcej4GBsT5s7rum7TYk=",
|
||||||
"owner": "nix-community",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs.lib",
|
"repo": "nixpkgs",
|
||||||
"rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c",
|
"rev": "34dca5401eac106b575afb642997094472f0d449",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-community",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs.lib",
|
"ref": "master",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-previous": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767892417,
|
||||||
|
"narHash": "sha256-dhhvQY67aboBk8b0/u0XB6vwHdgbROZT3fJAjyNh5Ww=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-stable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1782847189,
|
||||||
|
"narHash": "sha256-twXPFqFsrrY5r28Zh7Homgcp2gUMBgQ6WDS98Q/3xFI=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "b6018f87da91d19d0ab4cf979885689b469cdd41",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-25.11",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-unstable": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1785602060,
|
||||||
|
"narHash": "sha256-z7D96eESRM4CPV/XtwpwFn8IDdfLAmxz6lVWrGYXvR4=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "a5cbcfe954791221bfffe2307f7d1a1bf61a871e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1785571196,
|
||||||
|
"narHash": "sha256-KoTsyMQqnXQZq8deCEnu4QkyldkwH/bpMMhUcfMdGIw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "148bab9c1c3c53136ecb44a6ea356a0ed5b39b06",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -409,11 +403,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1785762349,
|
"lastModified": 1783574839,
|
||||||
"narHash": "sha256-jZhZkzAwc7f3exzcTDJWP2WCAchCv0iNC3UF/QsahdQ=",
|
"narHash": "sha256-ICof1tV4/9XheBLBGf7dhMhfMq4dOs1zwTHrr7zGFlA=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "plasma-manager",
|
"repo": "plasma-manager",
|
||||||
"rev": "a19a2a029fa180911bd89c554dca1616e10f4c1d",
|
"rev": "c551f0687658e4bb699cfff6015436ad7ee57a0d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -424,24 +418,21 @@
|
|||||||
},
|
},
|
||||||
"proxy-suite": {
|
"proxy-suite": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nfqws2-keenetic": "nfqws2-keenetic",
|
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
"nixpkgs"
|
"nixpkgs"
|
||||||
],
|
]
|
||||||
"z2k": "z2k",
|
|
||||||
"zapret": "zapret"
|
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1790130850,
|
"lastModified": 1783174389,
|
||||||
"narHash": "sha256-k7c+KGZmNLP0ZB9jnKKJUXUTvEJC8A6kHQmZlcN8kNQ=",
|
"narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=",
|
||||||
"owner": "FUFSoB",
|
"owner": "Mic92",
|
||||||
"repo": "proxy-suite-flake",
|
"repo": "sops-nix",
|
||||||
"rev": "8f65fa9c255e9350fb09f3d3cc9054034918bf49",
|
"rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "FUFSoB",
|
"owner": "Mic92",
|
||||||
"repo": "proxy-suite-flake",
|
"repo": "sops-nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -450,20 +441,24 @@
|
|||||||
"deploy-rs": "deploy-rs",
|
"deploy-rs": "deploy-rs",
|
||||||
"disko": "disko",
|
"disko": "disko",
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
"flake-parts": "flake-parts",
|
|
||||||
"grub2-themes": "grub2-themes",
|
"grub2-themes": "grub2-themes",
|
||||||
"home-manager": "home-manager",
|
"home-manager": "home-manager",
|
||||||
"justray": "justray",
|
|
||||||
"nix-minecraft": "nix-minecraft",
|
|
||||||
"nix-on-droid": "nix-on-droid",
|
"nix-on-droid": "nix-on-droid",
|
||||||
"nix-systems": "nix-systems",
|
|
||||||
"nixos-hardware": "nixos-hardware",
|
"nixos-hardware": "nixos-hardware",
|
||||||
"nixos-wsl": "nixos-wsl",
|
"nixos-wsl": "nixos-wsl",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs_2",
|
||||||
|
"nixpkgs-beets": "nixpkgs-beets",
|
||||||
|
"nixpkgs-calibre": "nixpkgs-calibre",
|
||||||
|
"nixpkgs-master": "nixpkgs-master",
|
||||||
|
"nixpkgs-previous": "nixpkgs-previous",
|
||||||
|
"nixpkgs-stable": "nixpkgs-stable",
|
||||||
|
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||||
"plasma-manager": "plasma-manager",
|
"plasma-manager": "plasma-manager",
|
||||||
"proxy-suite": "proxy-suite",
|
"proxy-suite": "proxy-suite",
|
||||||
"sops-nix": "sops-nix",
|
"sops-nix": "sops-nix",
|
||||||
"utils": "utils"
|
"utils": "utils",
|
||||||
|
"zapret": "zapret",
|
||||||
|
"zeroq-credentials": "zeroq-credentials"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scss-reset": {
|
"scss-reset": {
|
||||||
@@ -489,11 +484,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1789890976,
|
"lastModified": 1783174389,
|
||||||
"narHash": "sha256-GKwH3zpy7tartuJMG0Rv/xUsdetG1QLmTVv8UKgJLmA=",
|
"narHash": "sha256-aCWC8ngycU7OdJrU2+Je3qf+1a2ykuBvpPhZT/9tXMc=",
|
||||||
"owner": "Mic92",
|
"owner": "Mic92",
|
||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"rev": "7214124c20c1542c90deb54af50e2f53ae02711f",
|
"rev": "f1406619a3884cd5c47992a70b8b35c9c0fcb4c9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -502,11 +497,24 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"utils": {
|
"utils": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"systems": [
|
"systems": "systems"
|
||||||
"nix-systems"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1731533236,
|
"lastModified": 1731533236,
|
||||||
@@ -522,59 +530,34 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"z2k": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1789186266,
|
|
||||||
"narHash": "sha256-d9gg7s66P3pkN7d4l72ryaGC9Ayoqg4tbHaoDNbDTT4=",
|
|
||||||
"owner": "necronicle",
|
|
||||||
"repo": "z2k",
|
|
||||||
"rev": "7beb9754d65a2cafd9c1d26d382bcb61d453d5b3",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "necronicle",
|
|
||||||
"ref": "z2k-enhanced",
|
|
||||||
"repo": "z2k",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"zapret": {
|
"zapret": {
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"proxy-suite",
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"zapret-flowseal": "zapret-flowseal"
|
|
||||||
},
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788726932,
|
"lastModified": 1767430655,
|
||||||
"narHash": "sha256-MehJgJpN7BGMaDES9I0aj/YG6JkRlSj5RiZDZfclNpc=",
|
"narHash": "sha256-f9PricXeNm3lG1tk2TepPPY+wxM5y0ezo1HSzNn4BQ8=",
|
||||||
"owner": "kartavkun",
|
"owner": "oqyude",
|
||||||
"repo": "zapret-discord-youtube",
|
"repo": "zapret-easyflake",
|
||||||
"rev": "64a8ee76f4f2e4a8d2751cb732f13448ee1e4fcf",
|
"rev": "302e77aae5fc6030a9c3bcc781d6514d87b19d11",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "kartavkun",
|
"owner": "oqyude",
|
||||||
"repo": "zapret-discord-youtube",
|
"repo": "zapret-easyflake",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zapret-flowseal": {
|
"zeroq-credentials": {
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788121958,
|
"lastModified": 1772104025,
|
||||||
"narHash": "sha256-WMpxbtA2OH340e4uuXR0tcUW0D6V9Kzs0KI1iKqkXBM=",
|
"narHash": "sha256-tX5I2lkwbB1leoib6Ao/Et0B1GYrn3vxw4DkFYX8uyM=",
|
||||||
"owner": "Flowseal",
|
"ref": "refs/heads/master",
|
||||||
"repo": "zapret-discord-youtube",
|
"rev": "511fc5446b502ff111020bda6d57261648d62333",
|
||||||
"rev": "6cec828910d0809863205702182a3557d9d0e8c3",
|
"revCount": 75,
|
||||||
"type": "github"
|
"type": "git",
|
||||||
|
"url": "ssh://git@github.com/oqyude/zeroq-credentials.git"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "Flowseal",
|
"type": "git",
|
||||||
"repo": "zapret-discord-youtube",
|
"url": "ssh://git@github.com/oqyude/zeroq-credentials.git"
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
{
|
{
|
||||||
description = "oqyude flake";
|
description = "oqyude flake";
|
||||||
inputs = {
|
inputs = {
|
||||||
|
# My
|
||||||
|
zeroq-credentials.url = "git+ssh://git@github.com/oqyude/zeroq-credentials.git"; # flake of creds
|
||||||
|
zapret.url = "github:oqyude/zapret-easyflake"; # stupid flake of zapret
|
||||||
|
|
||||||
# nixpkgs
|
# nixpkgs
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
# nixpkgs-master.url = "github:NixOS/nixpkgs/master";
|
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||||
# nixpkgs-last-unstable.url = "github:NixOS/nixpkgs/3497aa5c9457a9d88d71fa93a4a8368816fbeeba";
|
# nixpkgs-last-unstable.url = "github:NixOS/nixpkgs/6b4955211758ba47fac850c040a27f23b9b4008f";
|
||||||
# nixpkgs-fingerprint.url = "github:NixOS/nixpkgs/nixos-24.11";
|
nixpkgs-calibre.url = "github:NixOS/nixpkgs/566acc07c54dc807f91625bb286cb9b321b5f42a";
|
||||||
|
nixpkgs-previous.url = "github:NixOS/nixpkgs/3497aa5c9457a9d88d71fa93a4a8368816fbeeba";
|
||||||
|
nixpkgs-master.url = "github:NixOS/nixpkgs/master";
|
||||||
|
nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||||
|
nixpkgs-beets.url = "github:NixOS/nixpkgs/832efc09b4caf6b4569fbf9dc01bec3082a00611"; # 2343bbb58f99267223bc2aac4fc9ea301a155a16
|
||||||
|
#nixpkgs-fingerprint.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||||
|
|
||||||
# nix-community
|
# nix-community
|
||||||
nixos-wsl = {
|
nixos-wsl = {
|
||||||
@@ -30,29 +39,13 @@
|
|||||||
utils.follows = "utils";
|
utils.follows = "utils";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
justray = {
|
|
||||||
url = "github:luynrs/justray";
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.follows = "nixpkgs";
|
|
||||||
flake-parts.follows = "flake-parts";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
utils = {
|
utils.url = "github:numtide/flake-utils";
|
||||||
url = "github:numtide/flake-utils";
|
|
||||||
# flake-utils тянет systems (nix-systems/default) сам -> наследуем корневой, чтобы не плодить дубль-узел в flake.lock
|
|
||||||
inputs.systems.follows = "nix-systems";
|
|
||||||
};
|
|
||||||
flake-compat.url = "github:edolstra/flake-compat";
|
flake-compat.url = "github:edolstra/flake-compat";
|
||||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
|
||||||
nixos-hardware = {
|
|
||||||
url = "github:NixOS/nixos-hardware/master";
|
|
||||||
# без follows nixos-hardware лочит свой собственный nixpkgs (две копии в lock/store)
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
nix-systems.url = "github:nix-systems/default";
|
|
||||||
# nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
|
# nixos-facter-modules.url = "github:numtide/nixos-facter-modules";
|
||||||
# flake-utils.url = "github:numtide/flake-utils";
|
# flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
# flake-parts.url = "github:hercules-ci/flake-parts";
|
||||||
# noctalia = {
|
# noctalia = {
|
||||||
# url = "github:noctalia-dev/noctalia-shell";
|
# url = "github:noctalia-dev/noctalia-shell";
|
||||||
# inputs.nixpkgs.follows = "nixpkgs";
|
# inputs.nixpkgs.follows = "nixpkgs";
|
||||||
@@ -74,7 +67,7 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
proxy-suite = {
|
proxy-suite = {
|
||||||
url = "github:FUFSoB/proxy-suite-flake";
|
url = "github:Mic92/sops-nix";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
sops-nix = {
|
sops-nix = {
|
||||||
@@ -85,14 +78,6 @@
|
|||||||
url = "github:vinceliuice/grub2-themes";
|
url = "github:vinceliuice/grub2-themes";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
nix-minecraft = {
|
|
||||||
url = "github:Infinidoge/nix-minecraft";
|
|
||||||
inputs = {
|
|
||||||
flake-compat.follows = "flake-compat";
|
|
||||||
nixpkgs.follows = "nixpkgs";
|
|
||||||
systems.follows = "nix-systems";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
# nix-index-database = {
|
# nix-index-database = {
|
||||||
# url = "github:nix-community/nix-index-database";
|
# url = "github:nix-community/nix-index-database";
|
||||||
# inputs.nixpkgs.follows = "nixpkgs";
|
# inputs.nixpkgs.follows = "nixpkgs";
|
||||||
@@ -135,6 +120,7 @@
|
|||||||
}
|
}
|
||||||
// (import ./configurations flakeContext)
|
// (import ./configurations flakeContext)
|
||||||
// (import ./deploy flakeContext)
|
// (import ./deploy flakeContext)
|
||||||
|
// (import ./droid flakeContext)
|
||||||
// (import ./home flakeContext)
|
// (import ./home flakeContext)
|
||||||
// (import ./modules flakeContext)
|
// (import ./modules flakeContext)
|
||||||
// (import ./overlays flakeContext)
|
// (import ./overlays flakeContext)
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
streamripPath = "${xlib.dirs.wsl-storage}/streamrip";
|
||||||
|
in
|
||||||
{
|
{
|
||||||
|
# xdg = {
|
||||||
|
# configFile = {
|
||||||
|
# "streamrip" = {
|
||||||
|
# source = config.lib.file.mkOutOfStoreSymlink streamripPath;
|
||||||
|
# target = "streamrip";
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# };
|
||||||
home.packages = [
|
home.packages = [
|
||||||
pkgs.yt-dlp-light
|
pkgs.yt-dlp-light
|
||||||
];
|
];
|
||||||
|
|||||||
+45
-40
@@ -9,53 +9,58 @@ let
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
mkUser =
|
mkHomeModule = username: {
|
||||||
username:
|
imports = [
|
||||||
{
|
(./. + "/${xlib.device.type}.nix")
|
||||||
imports ? [ ],
|
];
|
||||||
headless ? false,
|
home = {
|
||||||
}:
|
username = username;
|
||||||
{
|
stateVersion = lib.mkDefault "26.05";
|
||||||
inherit imports;
|
homeDirectory =
|
||||||
home = {
|
if username == "root" then lib.mkDefault "/${username}" else lib.mkDefault "/home/${username}";
|
||||||
username = username;
|
enableNixpkgsReleaseCheck = false;
|
||||||
stateVersion = lib.mkDefault "26.05";
|
|
||||||
homeDirectory =
|
|
||||||
if username == "root" then lib.mkDefault "/${username}" else lib.mkDefault "/home/${username}";
|
|
||||||
enableNixpkgsReleaseCheck = false;
|
|
||||||
};
|
|
||||||
# Headless hosts: no GUI user dirs
|
|
||||||
xdg = lib.mkIf headless {
|
|
||||||
enable = true;
|
|
||||||
autostart.enable = true;
|
|
||||||
userDirs = {
|
|
||||||
enable = true;
|
|
||||||
createDirectories = false;
|
|
||||||
desktop = null;
|
|
||||||
documents = null;
|
|
||||||
download = null;
|
|
||||||
music = null;
|
|
||||||
pictures = null;
|
|
||||||
publicShare = null;
|
|
||||||
templates = null;
|
|
||||||
videos = null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
mkRootModule = username: {
|
||||||
|
home = {
|
||||||
|
username = username;
|
||||||
|
stateVersion = lib.mkDefault "26.05";
|
||||||
|
homeDirectory =
|
||||||
|
if username == "root" then lib.mkDefault "/${username}" else lib.mkDefault "/home/${username}";
|
||||||
|
enableNixpkgsReleaseCheck = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
mkOthersModule = username: {
|
||||||
|
imports = [
|
||||||
|
(./. + "/others/${xlib.device.type}.nix")
|
||||||
|
];
|
||||||
|
home = {
|
||||||
|
username = username;
|
||||||
|
stateVersion = lib.mkDefault "26.05";
|
||||||
|
homeDirectory =
|
||||||
|
if username == "root" then lib.mkDefault "/${username}" else lib.mkDefault "/home/${username}";
|
||||||
|
enableNixpkgsReleaseCheck = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
home-manager = {
|
home-manager = {
|
||||||
useGlobalPkgs = true;
|
useGlobalPkgs = true;
|
||||||
useUserPackages = true;
|
useUserPackages = true;
|
||||||
users = {
|
users = {
|
||||||
root = mkUser "root" { };
|
root = mkRootModule "root";
|
||||||
"${xlib.device.username}" = mkUser xlib.device.username {
|
"${xlib.device.username}" = mkHomeModule xlib.device.username;
|
||||||
imports = [
|
}
|
||||||
(./. + "/${xlib.device.type}.nix")
|
//
|
||||||
];
|
lib.optionalAttrs
|
||||||
headless = xlib.isHeadless;
|
(builtins.elem xlib.device.type [
|
||||||
};
|
"test"
|
||||||
};
|
#"secondary"
|
||||||
|
#"primary"
|
||||||
|
])
|
||||||
|
{
|
||||||
|
snity = mkOthersModule "snity";
|
||||||
|
};
|
||||||
sharedModules = [
|
sharedModules = [
|
||||||
inputs.plasma-manager.homeModules.plasma-manager
|
inputs.plasma-manager.homeModules.plasma-manager
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,386 +0,0 @@
|
|||||||
# Declarative OpenCode + oh-my-openagent (oh-my-opencode) plugin setup.
|
|
||||||
#
|
|
||||||
# Mirrors ~/.config/opencode/ on the current workstation.
|
|
||||||
# Imported by home/server.nix (sapphira). Auto-enables programs.opencode.
|
|
||||||
#
|
|
||||||
# Three files this module owns on disk (via xdg.configFile):
|
|
||||||
# ~/.config/opencode/opencode.json <- programs.opencode.settings
|
|
||||||
# ~/.config/opencode/tui.json <- programs.opencode.tui
|
|
||||||
# ~/.config/opencode/oh-my-openagent.json <- oh-my-openagent plugin config
|
|
||||||
#
|
|
||||||
# Override any field in the importing module if needed.
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
xlib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
# Body of ~/.config/opencode/oh-my-openagent.json.
|
|
||||||
# Loaded by the oh-my-openagent opencode plugin on startup.
|
|
||||||
ohMyOpenagentConfig = {
|
|
||||||
"$schema" = "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/oh-my-opencode.schema.json";
|
|
||||||
|
|
||||||
agents = {
|
|
||||||
sisyphus = {
|
|
||||||
model = "opencode/claude-opus-5";
|
|
||||||
variant = "max";
|
|
||||||
fallback_models = [
|
|
||||||
{ model = "opencode/kimi-k3"; }
|
|
||||||
{
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "medium";
|
|
||||||
}
|
|
||||||
{ model = "opencode/glm-5"; }
|
|
||||||
{ model = "opencode/big-pickle"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
hephaestus = {
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "medium";
|
|
||||||
};
|
|
||||||
oracle = {
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "xhigh";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/gemini-3.1-pro";
|
|
||||||
variant = "high";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
model = "opencode/claude-opus-5";
|
|
||||||
variant = "max";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
librarian = {
|
|
||||||
model = "minimax-coding-plan/MiniMax-M3";
|
|
||||||
};
|
|
||||||
explore = {
|
|
||||||
model = "opencode/gpt-5-nano";
|
|
||||||
fallback_models = [
|
|
||||||
{ model = "minimax-coding-plan/MiniMax-M3"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
"multimodal-looker" = {
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "low";
|
|
||||||
fallback_models = [
|
|
||||||
{ model = "opencode/gpt-5-nano"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
prometheus = {
|
|
||||||
model = "opencode/claude-fable-5";
|
|
||||||
variant = "high";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/kimi-k3";
|
|
||||||
variant = "high";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
metis = {
|
|
||||||
model = "opencode/claude-opus-5";
|
|
||||||
variant = "high";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/kimi-k3";
|
|
||||||
variant = "low";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
momus = {
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "xhigh";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/claude-opus-5";
|
|
||||||
variant = "max";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
model = "opencode/gemini-3.1-pro";
|
|
||||||
variant = "high";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
atlas = {
|
|
||||||
model = "opencode/claude-sonnet-4-6";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "medium";
|
|
||||||
}
|
|
||||||
{ model = "minimax-coding-plan/MiniMax-M3"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
"sisyphus-junior" = {
|
|
||||||
model = "opencode/claude-sonnet-4-6";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "medium";
|
|
||||||
}
|
|
||||||
{ model = "minimax-coding-plan/MiniMax-M3"; }
|
|
||||||
{ model = "opencode/big-pickle"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
categories = {
|
|
||||||
"visual-engineering" = {
|
|
||||||
model = "opencode/gemini-3.1-pro";
|
|
||||||
variant = "high";
|
|
||||||
fallback_models = [
|
|
||||||
{ model = "opencode/glm-5"; }
|
|
||||||
{
|
|
||||||
model = "opencode/claude-opus-5";
|
|
||||||
variant = "max";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
ultrabrain = {
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "xhigh";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/gemini-3.1-pro";
|
|
||||||
variant = "high";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
model = "opencode/claude-opus-5";
|
|
||||||
variant = "max";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
deep = {
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "medium";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/claude-opus-5";
|
|
||||||
variant = "max";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
model = "opencode/gemini-3.1-pro";
|
|
||||||
variant = "high";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
artistry = {
|
|
||||||
model = "opencode/gemini-3.1-pro";
|
|
||||||
variant = "high";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/claude-opus-5";
|
|
||||||
variant = "max";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "high";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
quick = {
|
|
||||||
model = "opencode/gpt-5.4-mini";
|
|
||||||
fallback_models = [
|
|
||||||
{ model = "opencode/gemini-3-flash"; }
|
|
||||||
{ model = "minimax-coding-plan/MiniMax-M3"; }
|
|
||||||
{ model = "opencode/gpt-5-nano"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
"unspecified-low" = {
|
|
||||||
model = "opencode/claude-sonnet-4-6";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "medium";
|
|
||||||
}
|
|
||||||
{ model = "opencode/gemini-3-flash"; }
|
|
||||||
{ model = "minimax-coding-plan/MiniMax-M3"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
"unspecified-high" = {
|
|
||||||
model = "opencode/claude-sonnet-4-6";
|
|
||||||
fallback_models = [
|
|
||||||
{
|
|
||||||
model = "opencode/gpt-5.6-sol";
|
|
||||||
variant = "medium";
|
|
||||||
}
|
|
||||||
{ model = "opencode/gemini-3-flash"; }
|
|
||||||
{ model = "minimax-coding-plan/MiniMax-M3"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
writing = {
|
|
||||||
model = "opencode/gemini-3-flash";
|
|
||||||
fallback_models = [
|
|
||||||
{ model = "opencode/claude-sonnet-4-6"; }
|
|
||||||
{ model = "minimax-coding-plan/MiniMax-M3"; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
let
|
|
||||||
# nixpkgs ast-grep only ships binary `ast-grep`; omo's ast-grep skill probes
|
|
||||||
# for `sg` (or ast-grep). Provide both via a symlink wrapper.
|
|
||||||
astGrepWithSg = pkgs.runCommandLocal "ast-grep-with-sg" { } ''
|
|
||||||
mkdir -p $out/bin
|
|
||||||
ln -s ${pkgs.ast-grep}/bin/ast-grep $out/bin/ast-grep
|
|
||||||
ln -s ${pkgs.ast-grep}/bin/ast-grep $out/bin/sg
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
|
||||||
programs.opencode = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
# Extras available to opencode-wrapped (via --suffix PATH on the wrapper):
|
|
||||||
# pkgs.nodejs_22 — npx/npm for MCP servers (webpage-mcp) and omo's plugin loader
|
|
||||||
# pkgs.ast-grep — `sg` CLI; omo's ast-grep skill requires it (omo doctor)
|
|
||||||
# pkgs.bun — omo prefers bun; with bun on PATH, `omo doctor` skips node fallback
|
|
||||||
# pkgs.gh — GitHub CLI; omo's GitHub automation features require it
|
|
||||||
extraPackages = [
|
|
||||||
pkgs.nodejs_22
|
|
||||||
astGrepWithSg
|
|
||||||
pkgs.bun
|
|
||||||
pkgs.gh
|
|
||||||
];
|
|
||||||
|
|
||||||
# ~/.config/opencode/opencode.json
|
|
||||||
settings = {
|
|
||||||
plugin = [ "oh-my-openagent@latest" ];
|
|
||||||
mcp = {
|
|
||||||
webpage = {
|
|
||||||
type = "local";
|
|
||||||
command = [
|
|
||||||
"npx"
|
|
||||||
"-y"
|
|
||||||
"-p"
|
|
||||||
"webpage-mcp@latest"
|
|
||||||
"webpage-mcp-stdio"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# ~/.config/opencode/tui.json
|
|
||||||
# Mirrors workstation: oh-my-openagent also registered for the TUI.
|
|
||||||
tui = {
|
|
||||||
plugin = [ "oh-my-openagent@latest" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# ~/.config/opencode/oh-my-openagent.json — read by the plugin on startup.
|
|
||||||
#
|
|
||||||
# NOTE: the oh-my-openagent plugin runs a `2026-07-opencode-config-unification`
|
|
||||||
# migration on every startup that backs up this file and tries to write its
|
|
||||||
# consolidated form to ~/.omo/omo.jsonc. The backup directory name embeds the
|
|
||||||
# source's content-hashed store path; because HM does not delete the previous
|
|
||||||
# generation's store path until garbage collection, the same path is reused on
|
|
||||||
# every retry and omo logs "Migration backup path already exists" forever.
|
|
||||||
# Recovery: `rm -rf ~/.omo/migration-backup-*` and let omo retry; if the
|
|
||||||
# migration keeps failing on the same backup path, the plugin/omo version
|
|
||||||
# probably expects a new schema and this config needs updating.
|
|
||||||
xdg.configFile."opencode/oh-my-openagent.json".text = builtins.toJSON ohMyOpenagentConfig;
|
|
||||||
|
|
||||||
# Same extras on the user's PATH too, so `omo doctor` and standalone invocations
|
|
||||||
# of `sg`, `gh`, `bun`, `npm`, `npx` work in the user's shell — not only inside
|
|
||||||
# the opencode-wrapped binary.
|
|
||||||
home.packages = [
|
|
||||||
pkgs.nodejs_22
|
|
||||||
astGrepWithSg
|
|
||||||
pkgs.bun
|
|
||||||
pkgs.gh
|
|
||||||
];
|
|
||||||
|
|
||||||
# Expose `opencode web` as a systemd user service. nginx on sapphira
|
|
||||||
# proxies https://opencode.zeroq.su -> 127.0.0.1:4096.
|
|
||||||
#
|
|
||||||
# --hostname 0.0.0.0 binds the listener to every interface (matches the
|
|
||||||
# "0.0.0.0" intent; nginx then reverse-proxies 127.0.0.1:4096 internally).
|
|
||||||
# --cors https://opencode.zeroq.su lets the browser session reach the
|
|
||||||
# server from that origin without CORS rejection.
|
|
||||||
#
|
|
||||||
# SECURITY: with no password, anyone reaching the upstream socket gets full
|
|
||||||
# opencode. Bind 0.0.0.0 + listener == bridge == shell. The password is
|
|
||||||
# supplied via sops-managed EnvironmentFile, declared in modules/users.nix
|
|
||||||
# and decrypted to a path hardcoded here (home-manager modules cannot read
|
|
||||||
# `config.sops.*` — sops-nix options are NixOS-only).
|
|
||||||
programs.opencode.web = {
|
|
||||||
enable = true;
|
|
||||||
environmentFile = xlib.dirs.opencode-server-env;
|
|
||||||
extraArgs = [
|
|
||||||
"--hostname"
|
|
||||||
"0.0.0.0"
|
|
||||||
"--cors"
|
|
||||||
"https://opencode.zeroq.su"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# RAM constraints for the opencode-web user service.
|
|
||||||
#
|
|
||||||
# Sapphira has 5.6 GiB RAM with a ~1 GiB baseline (syncthing + immich + gitea
|
|
||||||
# + x-ui + nextcloud php-fpm). When something else spikes (immich-ml jobs,
|
|
||||||
# syncthing indexer, etc.) the system OOM killer activates and picks the
|
|
||||||
# largest cgroup — opencode at ~260 MiB – 1.4 GiB peak was being chosen and
|
|
||||||
# systemd then restarted it every few seconds (`RestartSec=5`), masking the
|
|
||||||
# real cause as a "service crash". The 2026-10-04 incident was exactly this.
|
|
||||||
#
|
|
||||||
# Three knobs together make opencode stop being an OOM victim AND stop being
|
|
||||||
# the source of an OOM:
|
|
||||||
#
|
|
||||||
# MemoryHigh soft pressure threshold: kernel reclaims aggressively
|
|
||||||
# once the cgroup hits this. Process keeps running.
|
|
||||||
# MemoryMax hard cap: cgroup-local OOM kills Node if exceeded. The
|
|
||||||
# HOST survives — only this process dies, no restart storm.
|
|
||||||
# OOMScoreAdjust negative bias for the system-wide OOM killer: opencode
|
|
||||||
# is killed last, after syncthing/immich/etc.
|
|
||||||
# OOMPolicy "continue" — systemd does NOT auto-restart on cgroup
|
|
||||||
# OOM-kill. Without this, a spike triggers the same
|
|
||||||
# restart-loop the host saw today.
|
|
||||||
#
|
|
||||||
# Sizes are derived from observed peak (1.4 GiB at 16:36, 1.1 GiB at 16:59).
|
|
||||||
# MemoryHigh = 1G gives headroom for normal runs; MemoryMax = 2G caps
|
|
||||||
# pathological growth. Tweak both together if a workload legitimately
|
|
||||||
# needs more.
|
|
||||||
#
|
|
||||||
# Refs:
|
|
||||||
# https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html
|
|
||||||
# https://www.freedesktop.org/software/systemd/man/systemd.exec.html#OOMScoreAdjust=
|
|
||||||
# cgroup/OOM knobs added on top of the [Service] section emitted by
|
|
||||||
# `programs.opencode.web`. home-manager unions multiple definitions of the
|
|
||||||
# same systemd unit attrset, so ExecStart/Restart/EnvironmentFile from
|
|
||||||
# upstream and MemoryHigh/MemoryMax/OOMScoreAdjust/OOMPolicy from here
|
|
||||||
# land in the same [Service] block systemd actually reads.
|
|
||||||
#
|
|
||||||
# NOTE: do NOT use `serviceConfig = { ... }` — home-manager renders that
|
|
||||||
# as a literal `[serviceConfig]` section, which systemd silently ignores
|
|
||||||
# (`Unknown section 'serviceConfig'. Ignoring.`). The cgroup protection
|
|
||||||
# above would never take effect (verified on sapphira, c73a698).
|
|
||||||
systemd.user.services.opencode-web.Service = {
|
|
||||||
MemoryHigh = "1G";
|
|
||||||
MemoryMax = "2G";
|
|
||||||
OOMScoreAdjust = -900;
|
|
||||||
OOMPolicy = "continue";
|
|
||||||
};
|
|
||||||
|
|
||||||
# Workaround: home-manager activation updates the GC root `current-home`
|
|
||||||
# only at the very end (line 358 of the generated activate script), AFTER all
|
|
||||||
# `home.activation.*` dag entries have run. So we cannot read current-home
|
|
||||||
# from a dag entry — it still points to the OLD generation at the time our
|
|
||||||
# script executes. Instead, read `new-home`, which the activator writes
|
|
||||||
# BEFORE any dag entry runs and which already points at the new generation.
|
|
||||||
#
|
|
||||||
# The versioned symlink (`home-manager-NN-link`) is found by following
|
|
||||||
# `home-manager` one hop rather than hardcoding `home-manager-24-link`,
|
|
||||||
# so this keeps working across HM major-version bumps.
|
|
||||||
home.activation.relinkHomeManager = lib.hm.dag.entryAfter [] ''
|
|
||||||
hmVersioned="$(readlink "$HOME/.local/state/nix/profiles/home-manager" 2>/dev/null || true)"
|
|
||||||
target="$HOME/.local/state/nix/profiles/$hmVersioned"
|
|
||||||
newGen="$(readlink -e "''${XDG_STATE_HOME:-$HOME/.local/state}/home-manager/gcroots/new-home" 2>/dev/null || true)"
|
|
||||||
if [[ -n "$hmVersioned" && -n "$newGen" && "$(readlink -f "$target")" != "$newGen" ]]; then
|
|
||||||
echo "home-manager: relinking $target -> $newGen"
|
|
||||||
ln -sfn "$newGen" "$target"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,25 @@
|
|||||||
programs = {
|
programs = {
|
||||||
mangohud.enable = true;
|
mangohud.enable = true;
|
||||||
keepassxc.enable = true;
|
keepassxc.enable = true;
|
||||||
|
zed-editor = {
|
||||||
|
enable = false;
|
||||||
|
extensions = [
|
||||||
|
"nix"
|
||||||
|
];
|
||||||
|
userSettings = {
|
||||||
|
"telemetry" = {
|
||||||
|
"diagnostics" = false;
|
||||||
|
"metrics" = false;
|
||||||
|
};
|
||||||
|
"ui_font_size" = 20;
|
||||||
|
"buffer_font_size" = 26;
|
||||||
|
"theme" = {
|
||||||
|
"mode" = "system";
|
||||||
|
"light" = "Ayu Light";
|
||||||
|
"dark" = "Ayu Dark";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
services = {
|
services = {
|
||||||
kdeconnect.enable = true;
|
kdeconnect.enable = true;
|
||||||
@@ -15,6 +34,12 @@
|
|||||||
};
|
};
|
||||||
home = {
|
home = {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
|
# Surfing
|
||||||
|
# (brave.override {
|
||||||
|
# commandLineArgs = [
|
||||||
|
# "--password-store=basic" # on purpose to make it break "--password-store=gnome-libsecret"
|
||||||
|
# ];
|
||||||
|
# })
|
||||||
brave
|
brave
|
||||||
v2rayn
|
v2rayn
|
||||||
|
|
||||||
@@ -23,6 +48,8 @@
|
|||||||
# amdgpu_top
|
# amdgpu_top
|
||||||
vscodium
|
vscodium
|
||||||
ayugram-desktop
|
ayugram-desktop
|
||||||
|
# vesktop
|
||||||
|
# discord
|
||||||
gramps
|
gramps
|
||||||
kdePackages.filelight
|
kdePackages.filelight
|
||||||
localsend
|
localsend
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
xlib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
symlinksPaths = {
|
||||||
|
"/home/oqyude/Games/PrismLaunchers" = "${config.home.homeDirectory}/Games/PrismLaunchers";
|
||||||
|
"${config.home.homeDirectory}/Games/PrismLaunchers/${config.home.username}" =
|
||||||
|
".local/share/PrismLauncher";
|
||||||
|
};
|
||||||
|
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
|
||||||
|
name = targetPath;
|
||||||
|
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
|
||||||
|
}) symlinksPaths;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../minimal.nix
|
||||||
|
../modules/packages.nix
|
||||||
|
../modules/plasma-manager.nix
|
||||||
|
];
|
||||||
|
xdg = {
|
||||||
|
enable = true;
|
||||||
|
autostart.enable = true;
|
||||||
|
userDirs = {
|
||||||
|
enable = true;
|
||||||
|
createDirectories = true;
|
||||||
|
desktop = "${config.xdg.dataHome}/desktop";
|
||||||
|
documents = null;
|
||||||
|
download = "${config.home.homeDirectory}/Downloads";
|
||||||
|
music = "${config.home.homeDirectory}/Music";
|
||||||
|
pictures = "${config.home.homeDirectory}/Pictures";
|
||||||
|
publicShare = "${config.home.homeDirectory}/Misc/Public";
|
||||||
|
templates = null;
|
||||||
|
videos = "${config.home.homeDirectory}/Pictures/Videos";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
home = {
|
||||||
|
file = mkLinks;
|
||||||
|
pointerCursor = {
|
||||||
|
enable = true;
|
||||||
|
x11.enable = true;
|
||||||
|
gtk.enable = true;
|
||||||
|
size = 24;
|
||||||
|
name = "Qogir";
|
||||||
|
package = pkgs.qogir-icon-theme;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
xlib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
symlinksPaths = {
|
||||||
|
"/home/oqyude/Games/PrismLaunchers" = "${config.home.homeDirectory}/Games/PrismLaunchers";
|
||||||
|
"${config.home.homeDirectory}/Games/PrismLaunchers/${config.home.username}" =
|
||||||
|
".local/share/PrismLauncher";
|
||||||
|
};
|
||||||
|
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
|
||||||
|
name = targetPath;
|
||||||
|
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
|
||||||
|
}) symlinksPaths;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../minimal.nix
|
||||||
|
../modules/packages.nix
|
||||||
|
../modules/plasma-manager.nix
|
||||||
|
];
|
||||||
|
xdg = {
|
||||||
|
enable = true;
|
||||||
|
autostart.enable = true;
|
||||||
|
userDirs = {
|
||||||
|
enable = true;
|
||||||
|
createDirectories = true;
|
||||||
|
desktop = "${config.xdg.dataHome}/desktop";
|
||||||
|
documents = null;
|
||||||
|
download = "${config.home.homeDirectory}/Downloads";
|
||||||
|
music = "${config.home.homeDirectory}/Music";
|
||||||
|
pictures = "${config.home.homeDirectory}/Pictures";
|
||||||
|
publicShare = "${config.home.homeDirectory}/Misc/Public";
|
||||||
|
templates = null;
|
||||||
|
videos = "${config.home.homeDirectory}/Pictures/Videos";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
home = {
|
||||||
|
file = mkLinks;
|
||||||
|
pointerCursor = {
|
||||||
|
enable = true;
|
||||||
|
x11.enable = true;
|
||||||
|
gtk.enable = true;
|
||||||
|
size = 24;
|
||||||
|
name = "Qogir";
|
||||||
|
package = pkgs.qogir-icon-theme;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
+1
-1
@@ -32,7 +32,7 @@ in
|
|||||||
./modules/dconf.nix
|
./modules/dconf.nix
|
||||||
./modules/packages.nix
|
./modules/packages.nix
|
||||||
./modules/plasma-manager.nix
|
./modules/plasma-manager.nix
|
||||||
# ./modules/noctalia.nix
|
./modules/noctalia.nix
|
||||||
];
|
];
|
||||||
xdg = {
|
xdg = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ let
|
|||||||
|
|
||||||
"${config.home.homeDirectory}/Games/PrismLaunchers/${config.home.username}" =
|
"${config.home.homeDirectory}/Games/PrismLaunchers/${config.home.username}" =
|
||||||
".local/share/PrismLauncher";
|
".local/share/PrismLauncher";
|
||||||
"${xlib.dirs.lamet-drive}/Users/${xlib.device.username}/Music" = "Music";
|
"${xlib.dirs.lamet-drive}/Users/oqyude/Music" = "Music";
|
||||||
};
|
};
|
||||||
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
|
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
|
||||||
name = targetPath;
|
name = targetPath;
|
||||||
@@ -27,7 +27,7 @@ in
|
|||||||
./modules/dconf.nix
|
./modules/dconf.nix
|
||||||
./modules/packages.nix
|
./modules/packages.nix
|
||||||
./modules/plasma-manager.nix
|
./modules/plasma-manager.nix
|
||||||
# ./modules/noctalia.nix
|
./modules/noctalia.nix
|
||||||
];
|
];
|
||||||
xdg = {
|
xdg = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|||||||
+25
-3
@@ -5,13 +5,35 @@
|
|||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
symlinksPaths = {
|
||||||
|
"${config.home.homeDirectory}/External/Music" = "Music";
|
||||||
|
};
|
||||||
|
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
|
||||||
|
name = targetPath;
|
||||||
|
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
|
||||||
|
}) symlinksPaths;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./minimal.nix
|
./minimal.nix
|
||||||
./modules/opencode.nix
|
|
||||||
];
|
];
|
||||||
home.file = xlib.helpers.mkSymlinks config {
|
home.file = mkLinks;
|
||||||
"${config.home.homeDirectory}/External/Music" = "Music";
|
xdg = {
|
||||||
|
enable = true;
|
||||||
|
autostart.enable = true;
|
||||||
|
userDirs = {
|
||||||
|
enable = true;
|
||||||
|
createDirectories = false;
|
||||||
|
desktop = null;
|
||||||
|
documents = null;
|
||||||
|
download = null;
|
||||||
|
music = null;
|
||||||
|
pictures = null;
|
||||||
|
publicShare = null;
|
||||||
|
templates = null;
|
||||||
|
videos = null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
home.activation = {
|
home.activation = {
|
||||||
yaziSync = ''
|
yaziSync = ''
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
xlib,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Shared "strict" home-manager module.
|
# Shared "strict" home-manager module.
|
||||||
@@ -18,52 +17,46 @@
|
|||||||
# (which stay NixOS-only for now). When this module is wired into NixOS hosts
|
# (which stay NixOS-only for now). When this module is wired into NixOS hosts
|
||||||
# via sharedModules, deduplicate those files.
|
# via sharedModules, deduplicate those files.
|
||||||
{
|
{
|
||||||
home = {
|
home.packages = with pkgs; [
|
||||||
packages = with pkgs; [
|
# Lazy (alias lc)
|
||||||
# Lazy (alias lc)
|
lazycli
|
||||||
lazycli
|
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
fresh-editor # EDITOR
|
fresh-editor # EDITOR
|
||||||
|
|
||||||
# Base utils
|
# Base utils
|
||||||
curl
|
curl
|
||||||
wget
|
wget
|
||||||
fd
|
fd
|
||||||
tree
|
tree
|
||||||
dust
|
dust
|
||||||
gdu
|
gdu
|
||||||
mc
|
mc
|
||||||
rsync
|
rsync
|
||||||
jq
|
jq
|
||||||
unzip
|
unzip
|
||||||
zip
|
zip
|
||||||
zstd
|
zstd
|
||||||
|
|
||||||
# Net diagnostic
|
# Net diagnostic
|
||||||
mtr
|
mtr
|
||||||
dnsutils
|
dnsutils
|
||||||
|
|
||||||
# Monitoring
|
# Monitoring
|
||||||
htop
|
htop
|
||||||
];
|
];
|
||||||
sessionVariables = {
|
|
||||||
TUCKR_HOME = "$HOME/Storage/dotfiles";
|
home.sessionVariables = {
|
||||||
EDITOR = "fresh";
|
TUCKR_HOME = "$HOME/Storage/dotfiles";
|
||||||
};
|
EDITOR = "fresh";
|
||||||
file = {
|
|
||||||
".nanorc".text = ''
|
|
||||||
set nowrap
|
|
||||||
set tabstospaces
|
|
||||||
set tabsize 2
|
|
||||||
'';
|
|
||||||
# Authorized keys for sshd (see modules/termux/default.nix).
|
|
||||||
# Declarative for now — the Store/.ssh symlink scheme is postponed.
|
|
||||||
".ssh/authorized_keys".text = ''
|
|
||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKduJia+unaQQdN6X5syaHvnpIutO+yZwvfiCP4qKQ/P
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
home.file.".nanorc".text = ''
|
||||||
|
set nowrap
|
||||||
|
set tabstospaces
|
||||||
|
set tabsize 2
|
||||||
|
'';
|
||||||
|
|
||||||
programs = {
|
programs = {
|
||||||
# ---- Shell: zsh ----
|
# ---- Shell: zsh ----
|
||||||
zsh = {
|
zsh = {
|
||||||
@@ -75,12 +68,7 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
theme = "robbyrussell";
|
theme = "robbyrussell";
|
||||||
};
|
};
|
||||||
loginExtra = "clear && fastfetch && cd ~/.config/nix-on-droid";
|
loginExtra = "clear && fastfetch";
|
||||||
# .zshenv — sourced by zsh in ALL sessions incl. non-login ssh commands.
|
|
||||||
# runit from nixpkgs defaults to /var/service as SVDIR, but our tree
|
|
||||||
# lives at ~/service (symlinked as /etc/service). Export it so that
|
|
||||||
# `sv status sshd` works without qualifying the path.
|
|
||||||
envExtra = "export SVDIR=/etc/service";
|
|
||||||
initContent = ''
|
initContent = ''
|
||||||
beet-p() {
|
beet-p() {
|
||||||
local base="${config.home.homeDirectory}/.config/beets/My"
|
local base="${config.home.homeDirectory}/.config/beets/My"
|
||||||
@@ -102,7 +90,6 @@
|
|||||||
lg = "lazygit";
|
lg = "lazygit";
|
||||||
lc = "lazycli";
|
lc = "lazycli";
|
||||||
gp = "git pull";
|
gp = "git pull";
|
||||||
ns = "nix-on-droid switch --flake ~/.config/nix-on-droid#${xlib.device.hostname}";
|
|
||||||
gp-ns = "gp && ns";
|
gp-ns = "gp && ns";
|
||||||
gc = "git add . && git commit -m 'dev: автокоммит $(date +'%Y-%m-%d %H:%M:%S')'";
|
gc = "git add . && git commit -m 'dev: автокоммит $(date +'%Y-%m-%d %H:%M:%S')'";
|
||||||
y = "yazi";
|
y = "yazi";
|
||||||
@@ -110,14 +97,15 @@
|
|||||||
beet-path-library = "realpath --relative-to='${config.home.homeDirectory}/.config/beets/My' .";
|
beet-path-library = "realpath --relative-to='${config.home.homeDirectory}/.config/beets/My' .";
|
||||||
z-proxy = "export ALL_PROXY=socks5://localhost:10808";
|
z-proxy = "export ALL_PROXY=socks5://localhost:10808";
|
||||||
zh-proxy = "export HTTPS_PROXY=http://localhost:10808 && export HTTP_PROXY=http://localhost:10808";
|
zh-proxy = "export HTTPS_PROXY=http://localhost:10808 && export HTTP_PROXY=http://localhost:10808";
|
||||||
nix-dir = "cd ~/.config/nix-on-droid";
|
|
||||||
q-ssh = "sv-start"; # start all supervised services (sshd, ...); manage with `sv status sshd` etc
|
|
||||||
|
|
||||||
# beets
|
# beets
|
||||||
beet-ima = "beet im ./ -A";
|
beet-ima = "beet im ./ -A";
|
||||||
|
|
||||||
# ssh (hosts live in programs.ssh.settings below)
|
# ssh
|
||||||
# NOTE: lamet / pubray-1 have no Host entry yet — kept as aliases.
|
z-s = "ssh sapphira";
|
||||||
|
z-st = "ssh sapphira-tailscale";
|
||||||
|
z-o = "ssh otreca";
|
||||||
|
z-ot = "ssh otreca-tailscale";
|
||||||
z-l = "ssh lamet";
|
z-l = "ssh lamet";
|
||||||
z-lt = "ssh lamet-tailscale";
|
z-lt = "ssh lamet-tailscale";
|
||||||
z-p-1 = "ssh pubray-1";
|
z-p-1 = "ssh pubray-1";
|
||||||
@@ -128,6 +116,7 @@
|
|||||||
pip2nix = "nix run github:nix-community/pip2nix --"; # https://github.com/nix-community/pip2nix
|
pip2nix = "nix run github:nix-community/pip2nix --"; # https://github.com/nix-community/pip2nix
|
||||||
pip2nix-g = "nix run github:nix-community/pip2nix -- generate -r";
|
pip2nix-g = "nix run github:nix-community/pip2nix -- generate -r";
|
||||||
json2nix = "nix run github:sempruijs/json2nix";
|
json2nix = "nix run github:sempruijs/json2nix";
|
||||||
|
reb = "nix-on-droid switch --flake ~/.config/nix-on-droid";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -218,7 +207,7 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
settings = {
|
settings = {
|
||||||
user = {
|
user = {
|
||||||
name = xlib.device.username;
|
name = "oqyude";
|
||||||
email = "oqyude@gmail.com";
|
email = "oqyude@gmail.com";
|
||||||
};
|
};
|
||||||
pull = {
|
pull = {
|
||||||
@@ -226,58 +215,5 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# ---- SSH ----
|
|
||||||
# Declarative ~/.ssh/config, same as the one previously copied by hand.
|
|
||||||
# matchBlocks is deprecated in current home-manager; use `settings`
|
|
||||||
# (bare attr names become `Host` headers, keys are upstream directives).
|
|
||||||
ssh = {
|
|
||||||
enable = true;
|
|
||||||
# Reproduce the old enableDefaultConfig values explicitly.
|
|
||||||
enableDefaultConfig = false;
|
|
||||||
settings = {
|
|
||||||
"*" = {
|
|
||||||
ForwardAgent = false;
|
|
||||||
AddKeysToAgent = "no";
|
|
||||||
Compression = false;
|
|
||||||
ServerAliveInterval = 0;
|
|
||||||
ServerAliveCountMax = 3;
|
|
||||||
HashKnownHosts = false;
|
|
||||||
UserKnownHostsFile = "~/.ssh/known_hosts";
|
|
||||||
ControlMaster = "no";
|
|
||||||
ControlPath = "~/.ssh/master-%r@%n:%p";
|
|
||||||
ControlPersist = "no";
|
|
||||||
};
|
|
||||||
sapphira = {
|
|
||||||
HostName = "192.168.1.20";
|
|
||||||
User = xlib.device.username;
|
|
||||||
};
|
|
||||||
sapphira-tailscale = {
|
|
||||||
HostName = "100.64.0.0";
|
|
||||||
User = xlib.device.username;
|
|
||||||
};
|
|
||||||
otreca-old = {
|
|
||||||
HostName = "217.60.3.12";
|
|
||||||
User = xlib.device.username;
|
|
||||||
};
|
|
||||||
otreca = {
|
|
||||||
HostName = "109.248.161.5";
|
|
||||||
User = xlib.device.username;
|
|
||||||
};
|
|
||||||
otreca-tailscale = {
|
|
||||||
HostName = "100.64.1.0";
|
|
||||||
User = xlib.device.username;
|
|
||||||
};
|
|
||||||
rydiwo = {
|
|
||||||
HostName = "192.168.1.102";
|
|
||||||
User = xlib.device.username;
|
|
||||||
};
|
|
||||||
epral = {
|
|
||||||
HostName = "192.168.1.101";
|
|
||||||
User = xlib.device.username;
|
|
||||||
Port = 8022;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
xlib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./minimal.nix
|
||||||
|
];
|
||||||
|
xdg = {
|
||||||
|
enable = true;
|
||||||
|
autostart.enable = true;
|
||||||
|
userDirs = {
|
||||||
|
enable = true;
|
||||||
|
createDirectories = false;
|
||||||
|
desktop = null;
|
||||||
|
documents = null;
|
||||||
|
download = null;
|
||||||
|
music = null;
|
||||||
|
pictures = null;
|
||||||
|
publicShare = null;
|
||||||
|
templates = null;
|
||||||
|
videos = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,8 +1,27 @@
|
|||||||
{
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./minimal.nix
|
./minimal.nix
|
||||||
];
|
];
|
||||||
|
xdg = {
|
||||||
|
enable = true;
|
||||||
|
autostart.enable = true;
|
||||||
|
userDirs = {
|
||||||
|
enable = true;
|
||||||
|
createDirectories = false;
|
||||||
|
desktop = null;
|
||||||
|
documents = null;
|
||||||
|
download = null;
|
||||||
|
music = null;
|
||||||
|
pictures = null;
|
||||||
|
publicShare = null;
|
||||||
|
templates = null;
|
||||||
|
videos = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-4
@@ -5,15 +5,38 @@
|
|||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
symlinksPaths = {
|
||||||
|
"${config.home.homeDirectory}/External/Music" = "Music";
|
||||||
|
"${xlib.dirs.wsl-home}" = "External";
|
||||||
|
"${xlib.dirs.wsl-storage}" = "Storage";
|
||||||
|
};
|
||||||
|
mkLinks = lib.mapAttrs' (sourcePath: targetPath: {
|
||||||
|
name = targetPath;
|
||||||
|
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
|
||||||
|
}) symlinksPaths;
|
||||||
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./apps
|
./apps
|
||||||
./minimal.nix
|
./minimal.nix
|
||||||
];
|
];
|
||||||
home.file = xlib.helpers.mkSymlinks config {
|
home.file = mkLinks;
|
||||||
"${config.home.homeDirectory}/External/Music" = "Music";
|
xdg = {
|
||||||
"${xlib.dirs.wsl-home}" = "External";
|
enable = true;
|
||||||
"${xlib.dirs.wsl-storage}" = "Storage";
|
autostart.enable = true;
|
||||||
|
userDirs = {
|
||||||
|
enable = true;
|
||||||
|
createDirectories = false;
|
||||||
|
desktop = null;
|
||||||
|
documents = null;
|
||||||
|
download = null;
|
||||||
|
music = null;
|
||||||
|
pictures = null;
|
||||||
|
publicShare = null;
|
||||||
|
templates = null;
|
||||||
|
videos = null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
home.activation = {
|
home.activation = {
|
||||||
yaziSync = ''
|
yaziSync = ''
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
inputs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# Builds a NixOS system from a host record.
|
|
||||||
#
|
|
||||||
# `xlib` is the pure host value (lib/xlib.nix `mkXlib`) built in
|
|
||||||
# configurations/default.nix. It is handed to every module as the `xlib`
|
|
||||||
# argument, so modules read plain `xlib.*` data instead of `config.xlib.*`
|
|
||||||
# and the host record stays the single source of truth.
|
|
||||||
{
|
|
||||||
xlib,
|
|
||||||
modules ? [ ],
|
|
||||||
system ? "x86_64-linux",
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
lib = inputs.nixpkgs.lib;
|
|
||||||
in
|
|
||||||
lib.nixosSystem {
|
|
||||||
inherit
|
|
||||||
system
|
|
||||||
modules
|
|
||||||
;
|
|
||||||
specialArgs = {
|
|
||||||
inherit inputs;
|
|
||||||
inherit xlib;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
# Pure host library: no module system involved.
|
|
||||||
#
|
|
||||||
# Aggregates the four concerns a host record is built from:
|
|
||||||
# device.nix identity + capability flags from the device type
|
|
||||||
# dirs.nix well-known paths, derived from username
|
|
||||||
# helpers.nix pure helper functions shared by modules
|
|
||||||
#
|
|
||||||
# `mkXlib` is called in flake-level code (configurations/default.nix) and
|
|
||||||
# handed to every module as the `xlib` argument via lib/mkSystem.nix, so
|
|
||||||
# modules read plain `xlib.*` values instead of `config.xlib.*` and nothing in
|
|
||||||
# xlib can be overridden per host — the host record is the only place to
|
|
||||||
# change it.
|
|
||||||
{
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
inherit (import ./device.nix { inherit lib; })
|
|
||||||
devices
|
|
||||||
mkDevice
|
|
||||||
;
|
|
||||||
|
|
||||||
# dirs.nix is itself a function of `username`, not an attrset.
|
|
||||||
mkDirs = import ./dirs.nix;
|
|
||||||
|
|
||||||
helpers = (import ./helpers.nix { inherit lib; });
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit
|
|
||||||
devices
|
|
||||||
helpers
|
|
||||||
mkDevice
|
|
||||||
mkDirs
|
|
||||||
;
|
|
||||||
|
|
||||||
# Full host record: identity + capability flags + well-known paths +
|
|
||||||
# shared helpers.
|
|
||||||
mkXlib =
|
|
||||||
{
|
|
||||||
hostname,
|
|
||||||
type,
|
|
||||||
username ? "oqyude",
|
|
||||||
uid ? 1000,
|
|
||||||
gid ? 1000,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
device = mkDevice {
|
|
||||||
inherit
|
|
||||||
hostname
|
|
||||||
type
|
|
||||||
username
|
|
||||||
uid
|
|
||||||
gid
|
|
||||||
;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
device = {
|
|
||||||
inherit
|
|
||||||
hostname
|
|
||||||
type
|
|
||||||
username
|
|
||||||
uid
|
|
||||||
gid
|
|
||||||
;
|
|
||||||
};
|
|
||||||
isDesktop = device.isDesktop;
|
|
||||||
isHeadless = device.isHeadless;
|
|
||||||
dirs = mkDirs username;
|
|
||||||
# Bind the host's ids into the mount helpers, so ntfs3/exfat options
|
|
||||||
# carry the same uid/gid the primary user actually has.
|
|
||||||
helpers = import ./helpers.nix {
|
|
||||||
inherit lib;
|
|
||||||
uid = device.uid;
|
|
||||||
gid = device.gid;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# Supported device types and the identity record built from one.
|
|
||||||
#
|
|
||||||
# Single source of truth for host identity: hostname, type, username and the
|
|
||||||
# capability flags derived from the type. Replaces the old `lib.types.enum`
|
|
||||||
# in modules/options.nix and the hand-written type lists in modules/default.nix
|
|
||||||
# and home/home.nix.
|
|
||||||
let
|
|
||||||
devices = {
|
|
||||||
minimal = {
|
|
||||||
desktop = false;
|
|
||||||
headless = false;
|
|
||||||
};
|
|
||||||
primary = {
|
|
||||||
desktop = true;
|
|
||||||
headless = false;
|
|
||||||
};
|
|
||||||
secondary = {
|
|
||||||
desktop = true;
|
|
||||||
headless = false;
|
|
||||||
};
|
|
||||||
server = {
|
|
||||||
desktop = false;
|
|
||||||
headless = true;
|
|
||||||
};
|
|
||||||
vds = {
|
|
||||||
desktop = false;
|
|
||||||
headless = true;
|
|
||||||
};
|
|
||||||
wsl = {
|
|
||||||
desktop = false;
|
|
||||||
headless = true;
|
|
||||||
};
|
|
||||||
termux = {
|
|
||||||
desktop = false;
|
|
||||||
headless = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit devices;
|
|
||||||
|
|
||||||
# Unknown device type fails here, at flake level, with the valid list.
|
|
||||||
mkDevice =
|
|
||||||
{
|
|
||||||
hostname,
|
|
||||||
type,
|
|
||||||
username ? "oqyude",
|
|
||||||
# The primary user is pinned to 1000 rather than left to NixOS'
|
|
||||||
# nextfree logic: the mount helpers below write uid=/gid= into
|
|
||||||
# ntfs3/exfat options, and an NTFS/exFAT volume mounted with a
|
|
||||||
# different id shows every file as owned by `nobody`.
|
|
||||||
uid ? 1000,
|
|
||||||
gid ? 1000,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
capabilities =
|
|
||||||
devices.${type}
|
|
||||||
or (throw "xlib: unknown device type '${type}', expected one of ${lib.concatStringsSep ", " (builtins.attrNames devices)}");
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit
|
|
||||||
hostname
|
|
||||||
type
|
|
||||||
username
|
|
||||||
uid
|
|
||||||
gid
|
|
||||||
;
|
|
||||||
isDesktop = capabilities.desktop;
|
|
||||||
isHeadless = capabilities.headless;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# Well-known paths. Everything derives from `username`, which is why the
|
|
||||||
# whole set can be computed outside the module system.
|
|
||||||
username:
|
|
||||||
let
|
|
||||||
user-home = "/home/${username}";
|
|
||||||
wsl-home = "/mnt/c/Users/${username}";
|
|
||||||
server-home = "${user-home}/External";
|
|
||||||
services-mnt-folder = "/mnt/services";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit
|
|
||||||
user-home
|
|
||||||
wsl-home
|
|
||||||
server-home
|
|
||||||
services-mnt-folder
|
|
||||||
;
|
|
||||||
opencode-server-env = "${user-home}/.config/opencode/server.env";
|
|
||||||
|
|
||||||
user-storage = "${user-home}/Storage";
|
|
||||||
wsl-storage = "${wsl-home}/Storage";
|
|
||||||
server-credentials = "${server-home}/Credentials/server";
|
|
||||||
storage = "${server-home}/Storage";
|
|
||||||
calibre-library = "${server-home}/Books-Library";
|
|
||||||
services-folder = "${server-home}/Services";
|
|
||||||
services-nodes-folder = "${services-mnt-folder}/nodes";
|
|
||||||
postgresql-folder = "${services-mnt-folder}/postgresql";
|
|
||||||
music-library = "${user-home}/Music";
|
|
||||||
|
|
||||||
archive-drive = "/mnt/archive";
|
|
||||||
lamet-drive = "/mnt/lamet";
|
|
||||||
mobile-drive = "/mnt/mobile";
|
|
||||||
therima-drive = "/mnt/therima";
|
|
||||||
vetymae-drive = "/mnt/vetymae";
|
|
||||||
soptur-drive = "/mnt/soptur";
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
# The primary user's ids, bound from xlib.device by mkXlib. ntfs3/exfat
|
|
||||||
# volumes carry POSIX ids, so a mount using anything other than the real
|
|
||||||
# uid/gid shows every file as owned by `nobody`.
|
|
||||||
uid,
|
|
||||||
gid,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# Pure helper functions for module definitions.
|
|
||||||
# Injected into every module via `xlib.helpers` (see default.nix).
|
|
||||||
#
|
|
||||||
# Defined in a `let` because they reference each other (mkTmpDirs uses
|
|
||||||
# mkTmpfile, mkServiceStorage uses mkTmpDirs + mkSystemdBind).
|
|
||||||
let
|
|
||||||
# tmpfiles rule: "type dir mode user group -"
|
|
||||||
mkTmpfile =
|
|
||||||
type: dir: mode: user: group:
|
|
||||||
"${type} ${dir} ${mode} ${user} ${group} -";
|
|
||||||
|
|
||||||
# several tmpfiles types for the same dir, e.g. ["d" "z"] or ["d" "Z"]
|
|
||||||
mkTmpDirs =
|
|
||||||
{
|
|
||||||
dir,
|
|
||||||
mode,
|
|
||||||
user,
|
|
||||||
group,
|
|
||||||
types ? [
|
|
||||||
"d"
|
|
||||||
"z"
|
|
||||||
],
|
|
||||||
}:
|
|
||||||
map (type: mkTmpfile type dir mode user group) types;
|
|
||||||
|
|
||||||
# fileSystems bind mount
|
|
||||||
mkBindMount =
|
|
||||||
{
|
|
||||||
what,
|
|
||||||
where,
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
"${where}" = {
|
|
||||||
device = what;
|
|
||||||
fsType = "none";
|
|
||||||
options = [
|
|
||||||
"bind"
|
|
||||||
"nofail"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# systemd.mounts bind mount (automount variant)
|
|
||||||
mkSystemdBind =
|
|
||||||
{
|
|
||||||
what,
|
|
||||||
where,
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
enable = true;
|
|
||||||
options = "bind,x-systemd.automount,nofail";
|
|
||||||
requires = [ "local-fs.target" ];
|
|
||||||
type = "none";
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
inherit what where;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Full "service storage" block: services-mnt source dir + /var/lib target,
|
|
||||||
# tmpfiles d/z + automount bind. Used as:
|
|
||||||
# storage = xlib.helpers.mkServiceStorage { name = "x"; user = "x"; group = "x"; };
|
|
||||||
# systemd = storage.systemd;
|
|
||||||
mkServiceStorage =
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
user,
|
|
||||||
group,
|
|
||||||
mode ? "0755",
|
|
||||||
target ? "/var/lib/${name}",
|
|
||||||
base ? "/mnt/services",
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
sourceDir = "${base}/${name}";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit sourceDir target;
|
|
||||||
systemd = {
|
|
||||||
tmpfiles.rules = mkTmpDirs {
|
|
||||||
dir = sourceDir;
|
|
||||||
inherit mode user group;
|
|
||||||
};
|
|
||||||
mounts = [
|
|
||||||
(mkSystemdBind {
|
|
||||||
what = sourceDir;
|
|
||||||
where = target;
|
|
||||||
})
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# ntfs3 mount, e.g. fileSystems = mkNtfsMount { path = ...; uuid = ...; }
|
|
||||||
mkNtfsMount =
|
|
||||||
{
|
|
||||||
path,
|
|
||||||
uuid,
|
|
||||||
mask ? "0007",
|
|
||||||
enable ? null,
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
"${path}" = {
|
|
||||||
device = "/dev/disk/by-uuid/${uuid}";
|
|
||||||
fsType = "ntfs3";
|
|
||||||
options = [
|
|
||||||
"defaults"
|
|
||||||
"uid=${toString uid}"
|
|
||||||
"gid=${toString gid}"
|
|
||||||
"fmask=${mask}"
|
|
||||||
"dmask=${mask}"
|
|
||||||
"nofail"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (enable != null) { inherit enable; };
|
|
||||||
};
|
|
||||||
|
|
||||||
# exfat mount, e.g. fileSystems = mkExfatMount { path = ...; uuid = ...; }
|
|
||||||
mkExfatMount =
|
|
||||||
{
|
|
||||||
path,
|
|
||||||
uuid ? null,
|
|
||||||
label ? null,
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
"${path}" = {
|
|
||||||
device = if uuid != null then "/dev/disk/by-uuid/${uuid}" else "/dev/disk/by-label/${label}";
|
|
||||||
fsType = "exfat";
|
|
||||||
options = [
|
|
||||||
"nofail"
|
|
||||||
"uid=${toString uid}"
|
|
||||||
"gid=${toString gid}"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# home-manager out-of-store symlinks: path = source (target name = attr name)
|
|
||||||
mkSymlinks =
|
|
||||||
config: paths:
|
|
||||||
lib.mapAttrs' (sourcePath: targetPath: {
|
|
||||||
name = targetPath;
|
|
||||||
value.source = config.lib.file.mkOutOfStoreSymlink "${sourcePath}";
|
|
||||||
}) paths;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit
|
|
||||||
mkTmpfile
|
|
||||||
mkTmpDirs
|
|
||||||
mkBindMount
|
|
||||||
mkSystemdBind
|
|
||||||
mkServiceStorage
|
|
||||||
mkNtfsMount
|
|
||||||
mkExfatMount
|
|
||||||
mkSymlinks
|
|
||||||
;
|
|
||||||
}
|
|
||||||
+116
-114
@@ -5,126 +5,128 @@
|
|||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
panel = "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}/3x-ui";
|
|
||||||
# Domain whose Let's Encrypt cert (at /var/lib/acme/<domain>/) gets mounted
|
|
||||||
# read-only into the 3x-ui container so the panel can terminate TLS itself.
|
|
||||||
# Null when 3x-ui serves plain HTTP and TLS is terminated by an upstream
|
|
||||||
# nginx.
|
|
||||||
certDomain = config.host."3x-ui".certDomain;
|
|
||||||
certMounts =
|
|
||||||
if certDomain == null then
|
|
||||||
[ ]
|
|
||||||
else
|
|
||||||
# LE cert mounted read-only so 3x-ui can terminate TLS itself.
|
|
||||||
# The 3x-ui settings table must point webCertFile / webKeyFile at
|
|
||||||
# /root/cert/fullchain.pem and /root/cert/key.pem.
|
|
||||||
map (f: "/var/lib/acme/${certDomain}/${f}:/root/cert/${f}:ro") [
|
|
||||||
"fullchain.pem"
|
|
||||||
"key.pem"
|
|
||||||
];
|
|
||||||
basePorts = [
|
|
||||||
# Local-only upstreams for the 3x-ui panel and subscription endpoint.
|
|
||||||
# The direct Xray inbound remains publicly reachable on 8443.
|
|
||||||
"127.0.0.1:2049:2049/tcp"
|
|
||||||
"127.0.0.1:2096:2096/tcp"
|
|
||||||
"0.0.0.0:8443:8443/tcp"
|
|
||||||
];
|
|
||||||
# VDS-only: nginx stream forwards host:443 → 127.0.0.1:15380 →
|
|
||||||
# container:443, so Xray sees its REALITY inbound on port 443.
|
|
||||||
realityPorts = lib.optional config.host."3x-ui".reality443Forwarding "127.0.0.1:15380:443/tcp";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
# `host."3x-ui"` options are declared in modules/options.nix: they are set
|
virtualisation = {
|
||||||
# by modules/server and modules/vds, so this module cannot be the only place
|
podman = {
|
||||||
# that knows they exist.
|
enable = true;
|
||||||
config = {
|
autoPrune = {
|
||||||
virtualisation = {
|
|
||||||
podman = {
|
|
||||||
enable = true;
|
enable = true;
|
||||||
autoPrune = {
|
flags = [ "--all" ];
|
||||||
enable = true;
|
|
||||||
flags = [ "--all" ];
|
|
||||||
};
|
|
||||||
dockerCompat = true;
|
|
||||||
};
|
|
||||||
oci-containers = {
|
|
||||||
backend = "podman";
|
|
||||||
containers."3xui_app" = {
|
|
||||||
image = "ghcr.io/mhsanaei/3x-ui:latest";
|
|
||||||
environment = {
|
|
||||||
"XRAY_VMESS_AEAD_FORCED" = "false";
|
|
||||||
"XUI_ENABLE_FAIL2BAN" = "true";
|
|
||||||
"TZ" = "Europe/Moscow";
|
|
||||||
};
|
|
||||||
volumes = [
|
|
||||||
"${panel}/cert/:/root/cert:rw"
|
|
||||||
"${panel}/db/:/etc/x-ui:rw"
|
|
||||||
]
|
|
||||||
++ certMounts;
|
|
||||||
log-driver = "journald";
|
|
||||||
# Adding a new inbound through the 3x-ui panel on a port outside
|
|
||||||
# the 14380-15379 range requires extending basePorts and rebuilding.
|
|
||||||
ports = basePorts ++ realityPorts;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
dockerCompat = true;
|
||||||
};
|
};
|
||||||
|
oci-containers = {
|
||||||
systemd = {
|
backend = "podman";
|
||||||
services = {
|
containers."3xui_app" = {
|
||||||
"podman-3xui_app" = {
|
image = "ghcr.io/mhsanaei/3x-ui:latest";
|
||||||
serviceConfig.Restart = lib.mkOverride 90 "always";
|
environment = {
|
||||||
partOf = [ "podman-compose-3x-ui-root.target" ];
|
"XRAY_VMESS_AEAD_FORCED" = "false";
|
||||||
wantedBy = [ "podman-compose-3x-ui-root.target" ];
|
"XUI_ENABLE_FAIL2BAN" = "true";
|
||||||
};
|
"TZ" = "Europe/Moscow";
|
||||||
"podman-update-3xui_app" = {
|
|
||||||
path = [ pkgs.podman ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
TimeoutSec = 300;
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
podman pull ghcr.io/mhsanaei/3x-ui:latest
|
|
||||||
systemctl restart podman-3xui_app.service
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
volumes = [
|
||||||
|
"${xlib.dirs.services-mnt-folder}/containers/3x-ui/cert/:/root/cert:rw"
|
||||||
|
"${xlib.dirs.services-mnt-folder}/containers/3x-ui/db/:/etc/x-ui:rw"
|
||||||
|
];
|
||||||
|
log-driver = "journald";
|
||||||
|
extraOptions = [
|
||||||
|
"--network=host"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
# Starts/stops together with all 3x-ui compose resources.
|
|
||||||
targets."podman-compose-3x-ui-root" = {
|
|
||||||
unitConfig.Description = "Root target generated by compose2nix.";
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
};
|
|
||||||
# timers."podman-update-3xui_app" = {
|
|
||||||
# wantedBy = [ "timers.target" ];
|
|
||||||
# timerConfig = {
|
|
||||||
# OnCalendar = "weekly";
|
|
||||||
# Persistent = true;
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
tmpfiles.rules = [
|
|
||||||
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755"
|
|
||||||
"root"
|
|
||||||
"root"
|
|
||||||
)
|
|
||||||
(xlib.helpers.mkTmpfile "d" panel "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" "${panel}/db" "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" "${panel}/cert" "0755" "root" "root")
|
|
||||||
# Relabel panel dir for SELinux so containers can access it.
|
|
||||||
(xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root")
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# Enable container name DNS for all Podman networks.
|
|
||||||
networking.firewall = {
|
|
||||||
interfaces =
|
|
||||||
let
|
|
||||||
matchAll = if !config.networking.nftables.enable then "podman+" else "podman*";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
"${matchAll}".allowedUDPPorts = [ 53 ];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
services = {
|
||||||
|
"podman-3xui_app" = {
|
||||||
|
serviceConfig = {
|
||||||
|
Restart = lib.mkOverride 90 "always";
|
||||||
|
};
|
||||||
|
partOf = [
|
||||||
|
"podman-compose-3x-ui-root.target"
|
||||||
|
];
|
||||||
|
wantedBy = [
|
||||||
|
"podman-compose-3x-ui-root.target"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
# Update
|
||||||
|
"podman-update-3xui_app" = {
|
||||||
|
path = [
|
||||||
|
pkgs.podman
|
||||||
|
];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
TimeoutSec = 300;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
podman pull ghcr.io/mhsanaei/3x-ui:latest
|
||||||
|
systemctl restart podman-3xui_app.service
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
# Builds
|
||||||
|
# "podman-build-3xui_app" = {
|
||||||
|
# path = [
|
||||||
|
# pkgs.podman
|
||||||
|
# pkgs.git
|
||||||
|
# ];
|
||||||
|
# serviceConfig = {
|
||||||
|
# Type = "oneshot";
|
||||||
|
# TimeoutSec = 300;
|
||||||
|
# };
|
||||||
|
# script = ''
|
||||||
|
# cd /mnt/containers/3x-ui
|
||||||
|
# podman build -t compose2nix/3xui_app -f ./Dockerfile .
|
||||||
|
# '';
|
||||||
|
# };
|
||||||
|
};
|
||||||
|
# Root service
|
||||||
|
# When started, this will automatically create all resources and start
|
||||||
|
# the containers. When stopped, this will teardown all resources.
|
||||||
|
targets."podman-compose-3x-ui-root" = {
|
||||||
|
unitConfig = {
|
||||||
|
Description = "Root target generated by compose2nix.";
|
||||||
|
};
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
};
|
||||||
|
timers."podman-update-3xui_app" = {
|
||||||
|
wantedBy = [ "timers.target" ];
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "weekly";
|
||||||
|
Persistent = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# Folders
|
||||||
|
tmpfiles.rules = [
|
||||||
|
"d /mnt 0755 root root -"
|
||||||
|
"d /mnt/services 0755 root root -"
|
||||||
|
"d /mnt/services/containers 0755 root root -"
|
||||||
|
"d /mnt/services/containers/3x-ui 0755 root root -"
|
||||||
|
"d /mnt/services/containers/3x-ui/cert 0755 root root -"
|
||||||
|
"d /mnt/services/containers/3x-ui/db 0755 root root -"
|
||||||
|
"Z /mnt/services/containers/3x-ui 0755 root root -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Enable container name DNS for all Podman networks.
|
||||||
|
networking.firewall = {
|
||||||
|
allowedUDPPortRanges = [
|
||||||
|
{
|
||||||
|
from = 14380;
|
||||||
|
to = 15380;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
allowedTCPPortRanges = [
|
||||||
|
{
|
||||||
|
from = 14380;
|
||||||
|
to = 15380;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
interfaces =
|
||||||
|
let
|
||||||
|
matchAll = if !config.networking.nftables.enable then "podman+" else "podman*";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
"${matchAll}".allowedUDPPorts = [ 53 ];
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
{
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
|
||||||
# The image is built here rather than pulled: zaakirio/kokoro-ru is a
|
|
||||||
# Hugging Face repo, not a published OCI image, and its Russian G2P has to be
|
|
||||||
# driven through the repo's own ru_g2p.py.
|
|
||||||
#
|
|
||||||
# The build context goes through the store so the image is pinned to the
|
|
||||||
# config revision: edit a file, `nixos-rebuild`, and the unit below rebuilds
|
|
||||||
# and restarts. Reading the context off a checkout at runtime would leave the
|
|
||||||
# running container untraceable back to any config.
|
|
||||||
#
|
|
||||||
# runCommand rather than linkFarm: linkFarm entries are symlinks into other
|
|
||||||
# store paths, and `podman build` only mounts the context root, so every COPY
|
|
||||||
# fails with "copier: get: lstat ...: no such file or directory". Copying the
|
|
||||||
# bytes in leaves the context with no symlinks that escape its root.
|
|
||||||
source = pkgs.runCommand "kokoro-tts-source" { } ''
|
|
||||||
mkdir -p "$out"
|
|
||||||
cp -L ${./kokoro-tts/Dockerfile} "$out/Dockerfile"
|
|
||||||
cp -L ${./kokoro-tts/app.py} "$out/app.py"
|
|
||||||
cp -L ${./kokoro-tts/fetch_assets.py} "$out/fetch_assets.py"
|
|
||||||
cp -L ${./kokoro-tts/requirements.txt} "$out/requirements.txt"
|
|
||||||
'';
|
|
||||||
|
|
||||||
image = "localhost/kokoro-tts:latest";
|
|
||||||
|
|
||||||
# Unchanged from the silero module, so whatever already points at
|
|
||||||
# http://127.0.0.1:9898/v1 keeps working without edits.
|
|
||||||
hostPort = 9898;
|
|
||||||
containerPort = 8000;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = {
|
|
||||||
virtualisation = {
|
|
||||||
podman = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
autoPrune = {
|
|
||||||
enable = true;
|
|
||||||
flags = [ "--all" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
dockerCompat = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
oci-containers = {
|
|
||||||
backend = "podman";
|
|
||||||
|
|
||||||
containers.kokoro-tts = {
|
|
||||||
image = image;
|
|
||||||
|
|
||||||
ports = [
|
|
||||||
"127.0.0.1:${toString hostPort}:${toString containerPort}"
|
|
||||||
];
|
|
||||||
|
|
||||||
environment = {
|
|
||||||
# Inference is CPU-bound and already threaded inside torch. Measured
|
|
||||||
# on a 24-logical-core host: median end-to-end latency for a 5.6 s
|
|
||||||
# utterance was 1.203 s at 4 threads, 0.979 s at 12, 0.980 s at 16
|
|
||||||
# and 1.87 s at 24, so the useful ceiling is the physical core count
|
|
||||||
# and oversubscribing it roughly doubles the wait. These three must
|
|
||||||
# stay equal to the Dockerfile ENV and the app.py default: whichever
|
|
||||||
# of the three is set wins over the others.
|
|
||||||
KOKORO_THREADS = "12";
|
|
||||||
OMP_NUM_THREADS = "12";
|
|
||||||
MKL_NUM_THREADS = "12";
|
|
||||||
TZ = "Europe/Moscow";
|
|
||||||
};
|
|
||||||
|
|
||||||
# No volumes: the checkpoints, the acute-aware espeak data and
|
|
||||||
# ruaccent's ONNX models are all baked into the image, so the
|
|
||||||
# container needs neither a host directory nor the network to start.
|
|
||||||
log-driver = "journald";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd = {
|
|
||||||
services = {
|
|
||||||
# Runs before the container. BuildKit caches the expensive layers, so
|
|
||||||
# on every boot after the first this is a no-op that still verifies the
|
|
||||||
# image exists.
|
|
||||||
"podman-build-kokoro-tts" = {
|
|
||||||
path = [ pkgs.podman ];
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
RemainAfterExit = true;
|
|
||||||
# First build pulls torch wheels plus ~700 MB of weights.
|
|
||||||
TimeoutSec = 3600;
|
|
||||||
};
|
|
||||||
|
|
||||||
script = ''
|
|
||||||
podman build -t ${image} ${source}
|
|
||||||
'';
|
|
||||||
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
"podman-kokoro-tts" = {
|
|
||||||
# The image does not exist until the build above ran, and a `latest`
|
|
||||||
# tag must be re-pulled on rebuild, so ordering has to be explicit.
|
|
||||||
after = [ "podman-build-kokoro-tts.service" ];
|
|
||||||
requires = [ "podman-build-kokoro-tts.service" ];
|
|
||||||
serviceConfig.Restart = lib.mkOverride 90 "always";
|
|
||||||
# Auto-start disabled: start manually with `systemctl start podman-kokoro-tts`.
|
|
||||||
wantedBy = [ ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Fully qualified on purpose: NixOS ships a podman registries.conf without
|
|
||||||
# unqualified-search-registries, so a bare "python:3.12-slim-bookworm" fails to
|
|
||||||
# resolve before the build even starts.
|
|
||||||
FROM docker.io/library/python:3.12-slim-bookworm
|
|
||||||
|
|
||||||
# Pinned, not "main": a rebuild that only touched the Nix module must not
|
|
||||||
# silently pick up different weights. Bump these deliberately.
|
|
||||||
ARG KOKORO_RU_REPO=zaakirio/kokoro-ru
|
|
||||||
ARG KOKORO_RU_REVISION=d649c57b239b18c4c384378127cbf01dba039bc1
|
|
||||||
# Trim to "sveta" to halve the image: masha shares her checkpoint and dima is
|
|
||||||
# a second 327 MB one.
|
|
||||||
ARG KOKORO_RU_VOICES=sveta,masha,dima
|
|
||||||
|
|
||||||
# Thread counts, not a guess: see app.py THREADS. 12 was the measured plateau on
|
|
||||||
# a 24-logical-core host, and 24 was ~2x worse. Must stay equal to the Nix
|
|
||||||
# module's environment.environment, which wins over this ENV.
|
|
||||||
ENV PYTHONUNBUFFERED=1 \
|
|
||||||
PIP_NO_CACHE_DIR=1 \
|
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
|
||||||
HF_HUB_DISABLE_TELEMETRY=1 \
|
|
||||||
HF_HUB_DISABLE_SYMLINKS_WARNING=1 \
|
|
||||||
KOKORO_RU_REPO=${KOKORO_RU_REPO} \
|
|
||||||
KOKORO_RU_REVISION=${KOKORO_RU_REVISION} \
|
|
||||||
KOKORO_RU_VOICES=${KOKORO_RU_VOICES} \
|
|
||||||
KOKORO_MODEL_DIR=/app/kokoro-ru \
|
|
||||||
KOKORO_THREADS=12 \
|
|
||||||
OMP_NUM_THREADS=12 \
|
|
||||||
MKL_NUM_THREADS=12 \
|
|
||||||
TZ=Europe/Moscow
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# libgomp1 is torch's OpenMP runtime. espeak-ng comes from the espeakng-loader
|
|
||||||
# wheel rather than the distro package because the model needs its own
|
|
||||||
# recompiled ru_dict, and libsndfile is absent because WAV/PCM are written with
|
|
||||||
# stdlib `wave` while every other format goes through imageio-ffmpeg.
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends libgomp1 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# CPU-only torch from its own index: the default PyPI wheel drags in ~2.5 GB of
|
|
||||||
# CUDA libraries for a machine that has no GPU.
|
|
||||||
RUN pip install --index-url https://download.pytorch.org/whl/cpu torch
|
|
||||||
|
|
||||||
COPY requirements.txt ./
|
|
||||||
RUN pip install -r requirements.txt
|
|
||||||
|
|
||||||
# fetch_assets.py is copied on its own and app.py only after the snapshot, never
|
|
||||||
# as one COPY. A single COPY would tie the 639 MB download to the application
|
|
||||||
# source: any edit to app.py would invalidate this layer and refetch every
|
|
||||||
# checkpoint as hundreds of anonymous, rate-limited requests.
|
|
||||||
COPY fetch_assets.py ./
|
|
||||||
|
|
||||||
# Bakes the checkpoints, the acute-aware espeak data and ruaccent's ONNX models
|
|
||||||
# into the layer, which is what lets the container start with no network and no
|
|
||||||
# writable volume.
|
|
||||||
RUN python fetch_assets.py
|
|
||||||
|
|
||||||
COPY app.py ./
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=180s --retries=3 \
|
|
||||||
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=4)"]
|
|
||||||
|
|
||||||
# No workers: the model is a shared in-process singleton, so a second worker
|
|
||||||
# would only mean a second copy of ~2 GB of weights.
|
|
||||||
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
|
|
||||||
@@ -1,585 +0,0 @@
|
|||||||
"""OpenAI-compatible TTS API backed by zaakirio/kokoro-ru.
|
|
||||||
|
|
||||||
The model itself is language-blind: phoneme ids in, 24 kHz audio out. All the
|
|
||||||
Russian lives in the G2P front-end, and the one that matters is kokoro-ru's own
|
|
||||||
`ru_g2p.py` — RUAccent resolves lexical stress, ё and homographs, then an
|
|
||||||
acute-aware espeak-ng phonemizer turns that into IPA. Stock misaki Russian is
|
|
||||||
espeak-only and gets stress wrong often enough that the model reads as
|
|
||||||
non-native (measured 27% vs 22% round-trip WER, per the model card).
|
|
||||||
|
|
||||||
So: text -> RuG2P.phonemize -> KModel(ipa, voicepack[len(ipa) - 1]) -> waveform.
|
|
||||||
|
|
||||||
Endpoints
|
|
||||||
POST /v1/audio/speech OpenAI text-to-speech
|
|
||||||
POST /v1/audio/speech/stream same, but mp3/opus emitted while synthesising
|
|
||||||
GET /v1/models OpenAI model list
|
|
||||||
GET /v1/voices voice inventory (extension, not part of OpenAI)
|
|
||||||
GET /healthz readiness, 503 until the model is loaded
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import io
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import queue
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import wave
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import TYPE_CHECKING, Iterator, Literal
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # torch is imported lazily so /healthz answers during boot
|
|
||||||
import torch
|
|
||||||
|
|
||||||
MODEL_ID = "kokoro-ru"
|
|
||||||
SAMPLE_RATE = 24000
|
|
||||||
MODEL_DIR = Path(os.environ.get("KOKORO_MODEL_DIR", "/app/kokoro-ru"))
|
|
||||||
DEFAULT_VOICE = os.environ.get("KOKORO_DEFAULT_VOICE", "sveta")
|
|
||||||
# Measured on the host this was tuned for (Ryzen AI 9 HX 370, 24 logical cores):
|
|
||||||
# median end-to-end latency for a 5.6 s utterance was 1.203 s @ 4 threads,
|
|
||||||
# 1.066 s @ 8, 0.979 s @ 12, 0.980 s @ 16, then 1.87 s @ 24. The gain stops at
|
|
||||||
# the physical core count and SMT oversubscription costs ~2x, so cap instead of
|
|
||||||
# trusting os.cpu_count(), which reports logical CPUs. Override on other hosts.
|
|
||||||
THREADS = int(os.environ.get("KOKORO_THREADS", min(12, os.cpu_count() or 4)))
|
|
||||||
# 2026-07-29, when the kokoro-ru revision we pin was published. Clients that
|
|
||||||
# cache on this treat any change as a new model, so it must stay stable.
|
|
||||||
MODEL_CREATED = 1785353253
|
|
||||||
|
|
||||||
# voice -> (checkpoint stem, gender). The checkpoint carries the timbre and the
|
|
||||||
# voicepack the identity, which is why sveta and masha share one file.
|
|
||||||
VOICE_SPECS: dict[str, tuple[str, str]] = {
|
|
||||||
"sveta": ("kokoro-ru-v2-base", "female"),
|
|
||||||
"masha": ("kokoro-ru-v2-base", "female"),
|
|
||||||
"dima": ("kokoro-ru-v2-dima", "male"),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clients that ship the OpenAI voice list (alloy, nova, echo, ...) send those
|
|
||||||
# names unless the user overrides them, so map them onto the three we have.
|
|
||||||
VOICE_ALIASES: dict[str, str] = {
|
|
||||||
"alloy": "sveta",
|
|
||||||
"ash": "sveta",
|
|
||||||
"ballad": "sveta",
|
|
||||||
"verse": "sveta",
|
|
||||||
"marin": "sveta",
|
|
||||||
"coral": "masha",
|
|
||||||
"sage": "masha",
|
|
||||||
"shimmer": "masha",
|
|
||||||
"cedar": "masha",
|
|
||||||
"echo": "dima",
|
|
||||||
"fable": "dima",
|
|
||||||
"onyx": "dima",
|
|
||||||
}
|
|
||||||
|
|
||||||
CONTENT_TYPES = {
|
|
||||||
"wav": "audio/wav",
|
|
||||||
"mp3": "audio/mpeg",
|
|
||||||
"opus": "audio/ogg",
|
|
||||||
"aac": "audio/aac",
|
|
||||||
"flac": "audio/flac",
|
|
||||||
"pcm": "audio/pcm",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Everything except wav and pcm goes through ffmpeg; those two are byte-exact
|
|
||||||
# from the stdlib and need no encoder at all.
|
|
||||||
FFMPEG_ARGS = {
|
|
||||||
"mp3": ["-c:a", "libmp3lame", "-q:a", "2"],
|
|
||||||
"opus": ["-c:a", "libopus", "-b:a", "64k"],
|
|
||||||
"aac": ["-c:a", "aac", "-b:a", "128k"],
|
|
||||||
"flac": ["-c:a", "flac"],
|
|
||||||
}
|
|
||||||
FFMPEG_CONTAINERS = {"mp3": "mp3", "opus": "ogg", "aac": "adts", "flac": "flac"}
|
|
||||||
|
|
||||||
# Kokoro's Albert context is 510 tokens and KModel.forward asserts
|
|
||||||
# len(ids) + 2 <= 510, so 508 phonemes is the hard ceiling per forward pass.
|
|
||||||
MAX_PHONEMES = 508
|
|
||||||
# Roughly 300 characters of Russian lands near 400 phonemes, comfortably under
|
|
||||||
# the ceiling, and keeps a chunk short enough that a bad sentence is a short
|
|
||||||
# chunk.
|
|
||||||
CHUNK_CHARS = 300
|
|
||||||
# Silence inserted between chunks. Without it the concatenation clicks at every
|
|
||||||
# boundary because each forward pass starts and ends on a zero crossing.
|
|
||||||
CHUNK_GAP_S = 0.08
|
|
||||||
|
|
||||||
_SENTENCE_SPLIT = re.compile(r"(?<=[.!?…])\s+")
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
||||||
log = logging.getLogger("kokoro-ru")
|
|
||||||
|
|
||||||
|
|
||||||
def split_text(text: str, budget: int = CHUNK_CHARS) -> list[str]:
|
|
||||||
"""Split into sentence-bounded chunks, hard-cutting only as a last resort.
|
|
||||||
|
|
||||||
Phonemizing per sentence rather than per paragraph keeps RUAccent's stress
|
|
||||||
decisions local and gives the model a reset point at every full stop.
|
|
||||||
"""
|
|
||||||
chunks: list[str] = []
|
|
||||||
current = ""
|
|
||||||
for sentence in _SENTENCE_SPLIT.split(text.strip()):
|
|
||||||
sentence = sentence.strip()
|
|
||||||
while len(sentence) > budget:
|
|
||||||
if current:
|
|
||||||
chunks.append(current)
|
|
||||||
current = ""
|
|
||||||
chunks.append(sentence[:budget])
|
|
||||||
sentence = sentence[budget:].strip()
|
|
||||||
if not sentence:
|
|
||||||
continue
|
|
||||||
if len(current) + len(sentence) + 1 > budget:
|
|
||||||
# Guarded: when the first sentence fills the budget exactly, or the
|
|
||||||
# previous one was hard-cut down to nothing, `current` is empty and
|
|
||||||
# a bare append would queue a zero-length chunk.
|
|
||||||
if current:
|
|
||||||
chunks.append(current)
|
|
||||||
current = sentence
|
|
||||||
else:
|
|
||||||
current = f"{current} {sentence}".strip()
|
|
||||||
if current:
|
|
||||||
chunks.append(current)
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
|
|
||||||
def split_phonemes(ps: str, limit: int = MAX_PHONEMES) -> list[str]:
|
|
||||||
"""Cut an over-long phoneme string on word boundaries."""
|
|
||||||
if len(ps) <= limit:
|
|
||||||
return [ps]
|
|
||||||
parts: list[str] = []
|
|
||||||
rest = ps
|
|
||||||
while len(rest) > limit:
|
|
||||||
cut = rest.rfind(" ", 0, limit)
|
|
||||||
if cut <= 0:
|
|
||||||
cut = limit
|
|
||||||
parts.append(rest[:cut].strip())
|
|
||||||
rest = rest[cut:].strip()
|
|
||||||
if rest:
|
|
||||||
parts.append(rest)
|
|
||||||
return [part for part in parts if part]
|
|
||||||
|
|
||||||
|
|
||||||
class KokoroRu:
|
|
||||||
"""Loaded model plus the G2P front-end, behind a single inference lock."""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._torch: torch | None = None
|
|
||||||
self._g2p = None
|
|
||||||
self._models: dict[str, torch.nn.Module] = {}
|
|
||||||
self._packs: dict[str, torch.Tensor] = {}
|
|
||||||
# The Albert encoder and the iSTFTNet decoder keep per-call scratch
|
|
||||||
# buffers; concurrent forwards on one model interleave into them. The
|
|
||||||
# model is fast enough on CPU that serialising is not the bottleneck.
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
|
|
||||||
def load(self) -> None:
|
|
||||||
import torch
|
|
||||||
from kokoro import KModel
|
|
||||||
|
|
||||||
torch.set_num_threads(THREADS)
|
|
||||||
self._torch = torch
|
|
||||||
|
|
||||||
# RuG2P is imported from the baked snapshot, not installed, and it
|
|
||||||
# resolves espeak-data/ plus kokoro-config.json next to itself.
|
|
||||||
sys.path.insert(0, str(MODEL_DIR))
|
|
||||||
from ru_g2p import RuG2P
|
|
||||||
|
|
||||||
self._g2p = RuG2P(
|
|
||||||
espeak_data=MODEL_DIR / "espeak-data",
|
|
||||||
vocab_path=MODEL_DIR / "kokoro-config.json",
|
|
||||||
)
|
|
||||||
|
|
||||||
for stem in sorted({stem for stem, _ in VOICE_SPECS.values()}):
|
|
||||||
checkpoint = MODEL_DIR / f"{stem}.pth"
|
|
||||||
if not checkpoint.exists():
|
|
||||||
log.warning("checkpoint %s missing, voices using it stay unavailable", checkpoint)
|
|
||||||
continue
|
|
||||||
# repo_id is only used to build the default model filename; passing
|
|
||||||
# both config and model keeps it from touching the HF cache at all.
|
|
||||||
self._models[stem] = KModel(
|
|
||||||
repo_id=str(MODEL_DIR),
|
|
||||||
config=str(MODEL_DIR / "config.json"),
|
|
||||||
model=str(checkpoint),
|
|
||||||
).eval()
|
|
||||||
log.info("loaded checkpoint %s", checkpoint.name)
|
|
||||||
|
|
||||||
for name in VOICE_SPECS:
|
|
||||||
pack = MODEL_DIR / "voices" / f"{name}.pt"
|
|
||||||
if pack.exists():
|
|
||||||
self._packs[name] = torch.load(str(pack), map_location="cpu", weights_only=True)
|
|
||||||
|
|
||||||
if not self.available_voices():
|
|
||||||
raise RuntimeError(f"no usable voices under {MODEL_DIR}")
|
|
||||||
|
|
||||||
def available_voices(self) -> list[str]:
|
|
||||||
return [
|
|
||||||
name
|
|
||||||
for name in VOICE_SPECS
|
|
||||||
if name in self._packs and VOICE_SPECS[name][0] in self._models
|
|
||||||
]
|
|
||||||
|
|
||||||
def phonemes(self, text: str):
|
|
||||||
for chunk in split_text(text):
|
|
||||||
ps, _oov = self._g2p.phonemize(chunk)
|
|
||||||
ps = ps.strip()
|
|
||||||
if ps:
|
|
||||||
yield from split_phonemes(ps)
|
|
||||||
|
|
||||||
def iter_audio_chunks(self, text: str, voice: str, speed: float):
|
|
||||||
"""Yields float32 audio per phoneme chunk, silence gaps interleaved.
|
|
||||||
|
|
||||||
The engine lock is held for the whole iteration, so a caller that stops
|
|
||||||
consuming early releases synthesis for everyone else.
|
|
||||||
"""
|
|
||||||
torch = self._torch
|
|
||||||
assert torch is not None, "synthesize() before load()"
|
|
||||||
stem, _gender = VOICE_SPECS[voice]
|
|
||||||
model = self._models[stem]
|
|
||||||
pack = self._packs[voice]
|
|
||||||
|
|
||||||
gap = np.zeros(int(CHUNK_GAP_S * SAMPLE_RATE), dtype=np.float32)
|
|
||||||
with self._lock:
|
|
||||||
for index, ps in enumerate(self.phonemes(text)):
|
|
||||||
# The style vector is picked by phoneme-string length, which is
|
|
||||||
# why the model sounds deterministic for identical text.
|
|
||||||
style = pack[len(ps) - 1]
|
|
||||||
# The packs ship as [510, 256]; KModel wants a batch of one.
|
|
||||||
if style.dim() == 1:
|
|
||||||
style = style.unsqueeze(0)
|
|
||||||
if index:
|
|
||||||
yield gap
|
|
||||||
yield np.asarray(
|
|
||||||
model(ps, style, speed, return_output=True).audio,
|
|
||||||
dtype=np.float32,
|
|
||||||
).reshape(-1)
|
|
||||||
|
|
||||||
def synthesize(self, text: str, voice: str, speed: float) -> np.ndarray:
|
|
||||||
chunks = list(self.iter_audio_chunks(text, voice, speed))
|
|
||||||
if not chunks:
|
|
||||||
return np.zeros(0, dtype=np.float32)
|
|
||||||
return np.concatenate(chunks)
|
|
||||||
|
|
||||||
|
|
||||||
def encode(audio: np.ndarray, fmt: str) -> bytes:
|
|
||||||
clipped = np.clip(audio, -1.0, 1.0)
|
|
||||||
if fmt == "pcm":
|
|
||||||
# OpenAI's pcm is raw signed 16-bit little-endian mono at 24 kHz.
|
|
||||||
return (clipped * 32767.0).astype("<i2").tobytes()
|
|
||||||
|
|
||||||
buffer = io.BytesIO()
|
|
||||||
with wave.open(buffer, "wb") as out:
|
|
||||||
out.setnchannels(1)
|
|
||||||
out.setsampwidth(2)
|
|
||||||
out.setframerate(SAMPLE_RATE)
|
|
||||||
out.writeframes((clipped * 32767.0).astype("<i2").tobytes())
|
|
||||||
wav = buffer.getvalue()
|
|
||||||
|
|
||||||
if fmt == "wav":
|
|
||||||
return wav
|
|
||||||
|
|
||||||
import imageio_ffmpeg
|
|
||||||
|
|
||||||
command = [
|
|
||||||
imageio_ffmpeg.get_ffmpeg_exe(),
|
|
||||||
"-hide_banner",
|
|
||||||
"-loglevel",
|
|
||||||
"error",
|
|
||||||
"-i",
|
|
||||||
"pipe:0",
|
|
||||||
"-ar",
|
|
||||||
str(SAMPLE_RATE),
|
|
||||||
"-ac",
|
|
||||||
"1",
|
|
||||||
*FFMPEG_ARGS[fmt],
|
|
||||||
"-f",
|
|
||||||
FFMPEG_CONTAINERS[fmt],
|
|
||||||
"pipe:1",
|
|
||||||
]
|
|
||||||
done = subprocess.run(command, input=wav, capture_output=True, check=False)
|
|
||||||
if done.returncode != 0:
|
|
||||||
raise RuntimeError(done.stderr.decode("utf-8", "replace").strip()[-400:])
|
|
||||||
return done.stdout
|
|
||||||
|
|
||||||
|
|
||||||
class StreamEncoder:
|
|
||||||
"""One long-lived ffmpeg per request: raw PCM in, encoded bytes out.
|
|
||||||
|
|
||||||
A single process is what keeps the container valid. Handing it the audio in
|
|
||||||
pieces as they are synthesised avoids any byte-level concatenation, whereas
|
|
||||||
encoding the pieces separately and joining the results would emit chained
|
|
||||||
Ogg for opus, which plenty of players reject.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, fmt: str) -> None:
|
|
||||||
import imageio_ffmpeg
|
|
||||||
|
|
||||||
self._proc = subprocess.Popen(
|
|
||||||
[
|
|
||||||
imageio_ffmpeg.get_ffmpeg_exe(),
|
|
||||||
"-hide_banner",
|
|
||||||
"-loglevel",
|
|
||||||
"error",
|
|
||||||
"-f",
|
|
||||||
"s16le",
|
|
||||||
"-ar",
|
|
||||||
str(SAMPLE_RATE),
|
|
||||||
"-ac",
|
|
||||||
"1",
|
|
||||||
"-i",
|
|
||||||
"pipe:0",
|
|
||||||
*FFMPEG_ARGS[fmt],
|
|
||||||
"-f",
|
|
||||||
FFMPEG_CONTAINERS[fmt],
|
|
||||||
"pipe:1",
|
|
||||||
],
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
self._blocks: queue.Queue[bytes | None] = queue.Queue()
|
|
||||||
self._reader = threading.Thread(target=self._pump, daemon=True)
|
|
||||||
self._reader.start()
|
|
||||||
|
|
||||||
def _pump(self) -> None:
|
|
||||||
assert self._proc.stdout is not None
|
|
||||||
while True:
|
|
||||||
block = self._proc.stdout.read(8192)
|
|
||||||
if not block:
|
|
||||||
break
|
|
||||||
self._blocks.put(block)
|
|
||||||
self._blocks.put(None)
|
|
||||||
|
|
||||||
def push(self, audio: np.ndarray) -> None:
|
|
||||||
assert self._proc.stdin is not None
|
|
||||||
clipped = np.clip(audio, -1.0, 1.0)
|
|
||||||
self._proc.stdin.write((clipped * 32767.0).astype("<i2").tobytes())
|
|
||||||
self._proc.stdin.flush()
|
|
||||||
|
|
||||||
def drain(self) -> Iterator[bytes]:
|
|
||||||
"""Yields whatever ffmpeg has already emitted, without waiting for more."""
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
block = self._blocks.get_nowait()
|
|
||||||
except queue.Empty:
|
|
||||||
return
|
|
||||||
if block is None:
|
|
||||||
return
|
|
||||||
yield block
|
|
||||||
|
|
||||||
def finish(self) -> Iterator[bytes]:
|
|
||||||
assert self._proc.stdin is not None
|
|
||||||
self._proc.stdin.close()
|
|
||||||
self._reader.join(timeout=120)
|
|
||||||
code = self._proc.wait(timeout=30)
|
|
||||||
error = self._proc.stderr.read().decode("utf-8", "replace").strip()[-400:]
|
|
||||||
if code != 0:
|
|
||||||
raise RuntimeError(error or f"ffmpeg exited with {code}")
|
|
||||||
yield from self.drain()
|
|
||||||
|
|
||||||
def abort(self) -> None:
|
|
||||||
if self._proc.poll() is None:
|
|
||||||
self._proc.kill()
|
|
||||||
|
|
||||||
|
|
||||||
engine = KokoroRu()
|
|
||||||
state: dict[str, str | None] = {"status": "loading", "error": None}
|
|
||||||
|
|
||||||
|
|
||||||
def boot() -> None:
|
|
||||||
try:
|
|
||||||
engine.load()
|
|
||||||
state["status"] = "ready"
|
|
||||||
log.info("ready: voices=%s", ", ".join(engine.available_voices()))
|
|
||||||
except Exception as exc:
|
|
||||||
state["status"] = "error"
|
|
||||||
state["error"] = f"{type(exc).__name__}: {exc}"
|
|
||||||
log.exception("model failed to load")
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(_app: FastAPI):
|
|
||||||
# Off the event loop: loading pulls ~700 MB of weights and runs three ONNX
|
|
||||||
# sessions, and /healthz has to stay answerable while it happens.
|
|
||||||
threading.Thread(target=boot, name="kokoro-load", daemon=True).start()
|
|
||||||
yield
|
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(title="kokoro-ru OpenAI TTS", version="1.0.0", lifespan=lifespan)
|
|
||||||
|
|
||||||
Format = Literal["mp3", "opus", "aac", "flac", "wav", "pcm"]
|
|
||||||
|
|
||||||
|
|
||||||
class SpeechRequest(BaseModel):
|
|
||||||
# `protected_namespaces` silences pydantic's warning about the `model_`
|
|
||||||
# prefix; `extra="ignore"` absorbs the fields newer OpenAI clients add
|
|
||||||
# (instructions, the legacy `format` alias) without failing the request.
|
|
||||||
model_config = ConfigDict(extra="ignore", protected_namespaces=())
|
|
||||||
|
|
||||||
input: str = Field(min_length=1)
|
|
||||||
model: str = MODEL_ID
|
|
||||||
voice: str | None = None
|
|
||||||
response_format: Format = "wav"
|
|
||||||
speed: float | None = Field(default=None, ge=0.25, le=4.0)
|
|
||||||
|
|
||||||
|
|
||||||
class StreamSpeechRequest(SpeechRequest):
|
|
||||||
# Streaming needs a container that tolerates unknown length up front, so wav
|
|
||||||
# (whose header declares the final sizes) and the raw formats are out. mp3
|
|
||||||
# and opus emit bytes as they go, which is the whole point of the endpoint.
|
|
||||||
response_format: Literal["mp3", "opus"] = "mp3"
|
|
||||||
|
|
||||||
|
|
||||||
def fail(status: int, message: str, param: str | None = None, code: str | None = None) -> JSONResponse:
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=status,
|
|
||||||
content={
|
|
||||||
"error": {
|
|
||||||
"message": message,
|
|
||||||
"type": "invalid_request_error" if status < 500 else "server_error",
|
|
||||||
"param": param,
|
|
||||||
"code": code,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_voice(requested: str | None) -> str | None:
|
|
||||||
name = (requested or DEFAULT_VOICE).strip().lower()
|
|
||||||
name = VOICE_ALIASES.get(name, name)
|
|
||||||
return name if name in engine.available_voices() else None
|
|
||||||
|
|
||||||
|
|
||||||
# response_model=None: the handler returns a Response subclass directly, and
|
|
||||||
# FastAPI would otherwise try to build a Pydantic model out of the union.
|
|
||||||
@app.post("/v1/audio/speech", response_model=None)
|
|
||||||
def create_speech(request: SpeechRequest) -> Response | JSONResponse:
|
|
||||||
if state["status"] != "ready":
|
|
||||||
return fail(503, f"model is not ready: {state['status']}", code="model_not_ready")
|
|
||||||
|
|
||||||
voice = resolve_voice(request.voice)
|
|
||||||
if voice is None:
|
|
||||||
available = ", ".join(engine.available_voices())
|
|
||||||
return fail(
|
|
||||||
400,
|
|
||||||
f"unknown voice {request.voice!r}; available: {available}",
|
|
||||||
param="voice",
|
|
||||||
code="unknown_voice",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
audio = engine.synthesize(request.input, voice, request.speed or 1.0)
|
|
||||||
except Exception as exc:
|
|
||||||
log.exception("synthesis failed")
|
|
||||||
return fail(500, f"synthesis failed: {exc}", code="synthesis_failed")
|
|
||||||
|
|
||||||
if audio.size == 0:
|
|
||||||
return fail(
|
|
||||||
400,
|
|
||||||
"input contains no speakable text for the Russian G2P",
|
|
||||||
param="input",
|
|
||||||
code="no_phonemes",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
payload = encode(audio, request.response_format)
|
|
||||||
except Exception as exc:
|
|
||||||
log.exception("encoding to %s failed", request.response_format)
|
|
||||||
return fail(500, f"encoding to {request.response_format} failed: {exc}", code="encoding_failed")
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
content=payload,
|
|
||||||
media_type=CONTENT_TYPES[request.response_format],
|
|
||||||
headers={"model-id": MODEL_ID, "voice-id": voice},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# response_model=None for the same reason as create_speech above.
|
|
||||||
@app.post("/v1/audio/speech/stream", response_model=None)
|
|
||||||
def stream_speech(request: StreamSpeechRequest) -> Response | JSONResponse:
|
|
||||||
if state["status"] != "ready":
|
|
||||||
return fail(503, f"model is not ready: {state['status']}", code="model_not_ready")
|
|
||||||
|
|
||||||
voice = resolve_voice(request.voice)
|
|
||||||
if voice is None:
|
|
||||||
available = ", ".join(engine.available_voices())
|
|
||||||
return fail(
|
|
||||||
400,
|
|
||||||
f"unknown voice {request.voice!r}; available: {available}",
|
|
||||||
param="voice",
|
|
||||||
code="unknown_voice",
|
|
||||||
)
|
|
||||||
|
|
||||||
chunks = engine.iter_audio_chunks(request.input, voice, request.speed or 1.0)
|
|
||||||
try:
|
|
||||||
# Pulled before responding: once the status line is sent it cannot become
|
|
||||||
# a 400, and input with no speakable text has to keep failing that way.
|
|
||||||
first = next(chunks)
|
|
||||||
except StopIteration:
|
|
||||||
return fail(
|
|
||||||
400,
|
|
||||||
"input contains no speakable text for the Russian G2P",
|
|
||||||
param="input",
|
|
||||||
code="no_phonemes",
|
|
||||||
)
|
|
||||||
|
|
||||||
def body() -> Iterator[bytes]:
|
|
||||||
encoder = StreamEncoder(request.response_format)
|
|
||||||
try:
|
|
||||||
encoder.push(first)
|
|
||||||
yield from encoder.drain()
|
|
||||||
for chunk in chunks:
|
|
||||||
encoder.push(chunk)
|
|
||||||
yield from encoder.drain()
|
|
||||||
yield from encoder.finish()
|
|
||||||
except Exception:
|
|
||||||
log.exception("streaming synthesis failed")
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
chunks.close()
|
|
||||||
encoder.abort()
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
body(),
|
|
||||||
media_type=CONTENT_TYPES[request.response_format],
|
|
||||||
headers={"model-id": MODEL_ID, "voice-id": voice},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v1/models")
|
|
||||||
def list_models() -> dict:
|
|
||||||
return {
|
|
||||||
"object": "list",
|
|
||||||
"data": [{"id": MODEL_ID, "object": "model", "created": MODEL_CREATED, "owned_by": "zaakirio"}],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/v1/voices")
|
|
||||||
def list_voices() -> dict:
|
|
||||||
return {
|
|
||||||
"object": "list",
|
|
||||||
"ready": state["status"] == "ready",
|
|
||||||
"data": [
|
|
||||||
{"id": name, "object": "voice", "checkpoint": VOICE_SPECS[name][0], "gender": VOICE_SPECS[name][1]}
|
|
||||||
for name in engine.available_voices()
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/healthz")
|
|
||||||
def healthz() -> JSONResponse:
|
|
||||||
ready = state["status"] == "ready"
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=200 if ready else 503,
|
|
||||||
content={
|
|
||||||
"status": state["status"],
|
|
||||||
"model": MODEL_ID,
|
|
||||||
"voices": engine.available_voices(),
|
|
||||||
"sample_rate": SAMPLE_RATE,
|
|
||||||
"error": state["error"],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
"""Bake every kokoro-ru asset the server needs into the image.
|
|
||||||
|
|
||||||
Two things make a plain `FROM python` image useless for this model at runtime,
|
|
||||||
and both are fixed here at build time:
|
|
||||||
|
|
||||||
* kokoro-ru's checkpoints and its recompiled espeak-ng data live in the HF
|
|
||||||
cache by default, and the HF cache is part of the disposable container
|
|
||||||
layer, so every `podman run` would re-download ~700 MB.
|
|
||||||
* ruaccent writes its ONNX models, dictionaries and Koziev data into its own
|
|
||||||
`site-packages/ruaccent` directory. It only downloads when those files are
|
|
||||||
missing, so a single `load()` here means the runtime never touches the
|
|
||||||
network.
|
|
||||||
|
|
||||||
RuG2P resolves espeak-data/ and kokoro-config.json relative to ru_g2p.py, so
|
|
||||||
the snapshot layout has to stay flat inside KOKORO_MODEL_DIR.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from huggingface_hub import snapshot_download
|
|
||||||
|
|
||||||
REPO = os.environ.get("KOKORO_RU_REPO", "zaakirio/kokoro-ru")
|
|
||||||
# A commit, not a branch: "main" would silently change the weights under a
|
|
||||||
# rebuild that only touched an unrelated line of the Nix module.
|
|
||||||
REVISION = os.environ.get("KOKORO_RU_REVISION", "main")
|
|
||||||
DEST = Path(os.environ.get("KOKORO_MODEL_DIR", "/app/kokoro-ru"))
|
|
||||||
|
|
||||||
VOICES = [v.strip() for v in os.environ.get("KOKORO_RU_VOICES", "sveta,masha,dima").split(",") if v.strip()]
|
|
||||||
|
|
||||||
# sveta and masha share one checkpoint and differ only by voicepack, so the two
|
|
||||||
# female voices cost one 327 MB download, not two.
|
|
||||||
CHECKPOINTS = {
|
|
||||||
"sveta": "kokoro-ru-v2-base.pth",
|
|
||||||
"masha": "kokoro-ru-v2-base.pth",
|
|
||||||
"dima": "kokoro-ru-v2-dima.pth",
|
|
||||||
}
|
|
||||||
|
|
||||||
PATTERNS = [
|
|
||||||
# KModel reads config.json; RuG2P reads kokoro-config.json for the phoneme
|
|
||||||
# vocab. They are not the same file and both are required.
|
|
||||||
"config.json",
|
|
||||||
"kokoro-config.json",
|
|
||||||
"ru_g2p.py",
|
|
||||||
# Stock espeak-ng ru_dict ignores combining-acute stress marks, which is the
|
|
||||||
# one thing this whole front-end exists to fix. The model repo ships a
|
|
||||||
# recompiled dictsource; there is no substitute to fall back to.
|
|
||||||
"espeak-data/**",
|
|
||||||
*(CHECKPOINTS[v] for v in VOICES if v in CHECKPOINTS),
|
|
||||||
*(f"voices/{v}.pt" for v in VOICES),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
||||||
|
|
||||||
DEST.mkdir(parents=True, exist_ok=True)
|
|
||||||
snapshot_download(
|
|
||||||
repo_id=REPO,
|
|
||||||
revision=REVISION,
|
|
||||||
allow_patterns=PATTERNS,
|
|
||||||
local_dir=str(DEST),
|
|
||||||
)
|
|
||||||
logging.info("kokoro-ru assets in %s at %s", DEST, REVISION)
|
|
||||||
|
|
||||||
missing = [name for name in VOICES if not (DEST / "voices" / f"{name}.pt").exists()]
|
|
||||||
if missing:
|
|
||||||
raise SystemExit(f"voice packs missing after download: {missing}")
|
|
||||||
|
|
||||||
# Warm ruaccent into site-packages so `load()` short-circuits at runtime.
|
|
||||||
from ruaccent import RUAccent
|
|
||||||
|
|
||||||
accent = RUAccent()
|
|
||||||
accent.load(omograph_model_size="turbo3.1", use_dictionary=True, tiny_mode=False)
|
|
||||||
logging.info("ruaccent warm: %s", accent.process_all("Здравствуйте, как ваши дела?"))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# torch is installed separately in the Dockerfile from the CPU-only index;
|
|
||||||
# do not add it here or pip would pull the ~2.5 GB CUDA build over it.
|
|
||||||
#
|
|
||||||
# ru_g2p.py (from zaakirio/kokoro-ru) imports three things stock kokoro does not
|
|
||||||
# pull on its own: the espeak-ng backend of misaki, ruaccent for stress, and the
|
|
||||||
# phonemizer fork whose EspeakWrapper misaki drives.
|
|
||||||
kokoro==0.9.4
|
|
||||||
misaki[en]>=0.9.4
|
|
||||||
phonemizer-fork
|
|
||||||
espeakng-loader
|
|
||||||
ruaccent
|
|
||||||
|
|
||||||
# kokoro's Albert encoder and ruaccent's ONNX exports both go through
|
|
||||||
# transformers. ru_g2p.py shims token_type_ids for v5, so the floor is what
|
|
||||||
# matters, not the ceiling.
|
|
||||||
transformers>=4.46
|
|
||||||
|
|
||||||
fastapi
|
|
||||||
uvicorn
|
|
||||||
imageio-ffmpeg
|
|
||||||
numpy>=1.26,<3
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
xlib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# Open WebUI — self-hosted AI chat UI, deployed here as a UI-client for
|
|
||||||
# external LLM APIs (OpenAI-compatible: OpenAI, OpenRouter, vLLM, LM Studio,
|
|
||||||
# GroqCloud, Mistral, etc.). Runs locally without bundled Ollama.
|
|
||||||
#
|
|
||||||
# Architecture mirrors modules/containers/{3x-ui,tape-rotation}.nix:
|
|
||||||
# - one container, one systemd unit + a root.target
|
|
||||||
# - data on /mnt/services/nodes/<host>/open-webui/data → /app/backend/data
|
|
||||||
# (see AGENTS.md §Подтверждённые инварианты #2 — guard chain is satisfied
|
|
||||||
# because mkServiceStorage already bind-mounts /mnt/services on boot)
|
|
||||||
# - host port bound to 127.0.0.1 only — the only ingress is the nginx
|
|
||||||
# vhost open.zeroq.su (no firewall exception, no public exposure).
|
|
||||||
# Same pattern as 3x-ui.nix:30-31 binding the panel to 127.0.0.1:2049.
|
|
||||||
#
|
|
||||||
# Secrets come from a single sops-encrypted dotenv file
|
|
||||||
# (format = "dotenv", key = "" → whole file). The owner creates the
|
|
||||||
# encrypted file with `sops modules/containers/secrets/open-webui.env`
|
|
||||||
# after filling the .example template next to it.
|
|
||||||
#
|
|
||||||
# Hard requirement (env.py:762 — SystemExit at startup):
|
|
||||||
# WEBUI_SECRET_KEY must be set when WEBUI_AUTH=true.
|
|
||||||
# Generate with: head -c 24 /dev/urandom | base64
|
|
||||||
#
|
|
||||||
# Reverse-proxy requirements (docs.openwebui.com/reference/https):
|
|
||||||
# - WEBUI_URL = public HTTPS URL (OAuth callbacks, internal links)
|
|
||||||
# - CORS_ALLOW_ORIGIN = same public URL (else WebSocket fails silently)
|
|
||||||
# - proxy_buffering off (else SSE streaming breaks markdown)
|
|
||||||
# - proxy_read_timeout ≥ 300s (LLM responses can run minutes)
|
|
||||||
# - WebSocket pass-through (Upgrade / Connection headers)
|
|
||||||
# All of the above are wired into modules/server/nginx.nix:open.zeroq.su.
|
|
||||||
let
|
|
||||||
panel = "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}/open-webui";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
virtualisation = {
|
|
||||||
podman = {
|
|
||||||
enable = true;
|
|
||||||
autoPrune = {
|
|
||||||
enable = true;
|
|
||||||
flags = [ "--all" ];
|
|
||||||
};
|
|
||||||
dockerCompat = true;
|
|
||||||
};
|
|
||||||
oci-containers = {
|
|
||||||
backend = "podman";
|
|
||||||
containers."open-webui" = {
|
|
||||||
image = "ghcr.io/open-webui/open-webui:main";
|
|
||||||
environment = {
|
|
||||||
TZ = "Europe/Moscow";
|
|
||||||
# Container-internal port (also the upstream default).
|
|
||||||
PORT = "8080";
|
|
||||||
# Required when behind a public HTTPS URL — OAuth callbacks,
|
|
||||||
# share links and internal redirects resolve against this.
|
|
||||||
WEBUI_URL = "https://open.zeroq.su";
|
|
||||||
# Must exactly match WEBUI_URL or WebSocket connections fail
|
|
||||||
# silently (per upstream HTTPS docs). nginx (127.0.0.1) is the
|
|
||||||
# only allowed origin, so a single explicit URL is enough.
|
|
||||||
CORS_ALLOW_ORIGIN = "https://open.zeroq.su";
|
|
||||||
# Honour X-Forwarded-* headers from the reverse proxy.
|
|
||||||
FORWARDED_ALLOW_IPS = "127.0.0.1";
|
|
||||||
# Closed self-hosted: admin creates accounts manually after the
|
|
||||||
# first boot via WEBUI_ADMIN_* from the sops env file.
|
|
||||||
WEBUI_AUTH = "True";
|
|
||||||
ENABLE_SIGNUP = "False";
|
|
||||||
ENABLE_LOGIN_FORM = "True";
|
|
||||||
ENABLE_VERSION_UPDATE_CHECK = "False";
|
|
||||||
# Out of the box Open WebUI phones home to Scarf. The opt-outs
|
|
||||||
# below preserve the previous behaviour from the stub at
|
|
||||||
# modules/server/open-webui.nix (still in tree, commented out in
|
|
||||||
# modules/server/default.nix:41) until that file is removed.
|
|
||||||
ANONYMIZED_TELEMETRY = "False";
|
|
||||||
DO_NOT_TRACK = "True";
|
|
||||||
SCARF_NO_ANALYTICS = "True";
|
|
||||||
# No bundled providers. Owners wire OPENAI_API_KEY /
|
|
||||||
# OPENAI_API_BASE_URL / etc. either via the sops env file
|
|
||||||
# (see sops.secrets."open-webui-env" below) or interactively in
|
|
||||||
# Admin → Settings → Connections once WEBUI_AUTH=true. Empty
|
|
||||||
# base URL is intentional: an empty OPENAI_API_BASE_URL
|
|
||||||
# disables the default /ollama proxy and prevents the container
|
|
||||||
# from probing localhost:11434 on boot.
|
|
||||||
OLLAMA_BASE_URL = "";
|
|
||||||
OPENAI_API_BASE_URL = "";
|
|
||||||
};
|
|
||||||
# Mount the decrypted dotenv only when the sops file exists. Until
|
|
||||||
# the owner creates ./secrets/open-webui.env, the inline environment
|
|
||||||
# is the only source — and the container will refuse to start with
|
|
||||||
# WEBUI_SECRET_KEY="" (env.py:762 — SystemExit). The error message is
|
|
||||||
# the clear signal that the secret needs to be created.
|
|
||||||
environmentFiles = lib.optional (builtins.pathExists ./secrets/open-webui.env)
|
|
||||||
"/run/secrets/open-webui-env";
|
|
||||||
volumes = [
|
|
||||||
"${panel}/data:/app/backend/data:rw"
|
|
||||||
];
|
|
||||||
log-driver = "journald";
|
|
||||||
# 127.0.0.1 only — the container is not exposed externally.
|
|
||||||
ports = [ "127.0.0.1:8080:8080/tcp" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Enable container name DNS for all Podman networks (mirrors 3x-ui.nix:120-128).
|
|
||||||
networking.firewall.interfaces =
|
|
||||||
let
|
|
||||||
matchAll = if !config.networking.nftables.enable then "podman+" else "podman*";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
"${matchAll}".allowedUDPPorts = [ 53 ];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd = {
|
|
||||||
services = {
|
|
||||||
"podman-open-webui" = {
|
|
||||||
serviceConfig.Restart = lib.mkOverride 90 "always";
|
|
||||||
partOf = [ "podman-compose-open-webui-root.target" ];
|
|
||||||
wantedBy = [ "podman-compose-open-webui-root.target" ];
|
|
||||||
};
|
|
||||||
"podman-update-open-webui" = {
|
|
||||||
path = [ pkgs.podman ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
TimeoutSec = 300;
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
podman pull ghcr.io/open-webui/open-webui:main
|
|
||||||
systemctl restart podman-open-webui.service
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
# Starts/stops together with the open-webui container.
|
|
||||||
targets."podman-compose-open-webui-root" = {
|
|
||||||
unitConfig.Description = "Root target for open-webui.";
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
};
|
|
||||||
# Enable automatic image updates:
|
|
||||||
# systemd.timers."podman-update-open-webui" = {
|
|
||||||
# wantedBy = [ "timers.target" ];
|
|
||||||
# timerConfig = {
|
|
||||||
# OnCalendar = "weekly";
|
|
||||||
# Persistent = true;
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
tmpfiles.rules = [
|
|
||||||
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755"
|
|
||||||
"root"
|
|
||||||
"root"
|
|
||||||
)
|
|
||||||
(xlib.helpers.mkTmpfile "d" panel "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" "${panel}/data" "0755" "root" "root")
|
|
||||||
# Relabel panel dir for SELinux so containers can access it.
|
|
||||||
(xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root")
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
# sops secret is declared only when the encrypted file actually exists,
|
|
||||||
# so the flake still evaluates (and rebuilds apply) on a host that hasn't
|
|
||||||
# created the secret yet. Once ./secrets/open-webui.env is created and
|
|
||||||
# encrypted with `sops modules/containers/secrets/open-webui.env`, this
|
|
||||||
# condition becomes true and the secret is wired in.
|
|
||||||
#
|
|
||||||
# Hard requirement (env.py:762 — SystemExit at startup):
|
|
||||||
# WEBUI_SECRET_KEY must be present in the env file when WEBUI_AUTH=true.
|
|
||||||
sops.secrets = lib.optionalAttrs (builtins.pathExists ./secrets/open-webui.env) {
|
|
||||||
"open-webui-env" = {
|
|
||||||
# key = "" → decrypt the whole file, not a single key.
|
|
||||||
# format = "dotenv" → the file IS one .env ready for environmentFiles:
|
|
||||||
# every non-comment KEY=VALUE line lands in the container environment.
|
|
||||||
# After this module is wired the file is mounted at
|
|
||||||
# /run/secrets/open-webui-env (sops-nix default for this attr name).
|
|
||||||
key = "";
|
|
||||||
format = "dotenv";
|
|
||||||
sopsFile = ./secrets/open-webui.env;
|
|
||||||
mode = "0400";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
WEBUI_SECRET_KEY=ENC[AES256_GCM,data:l6USiQmMkMz/zniIebT35HfXxZI8qrhe6Cdl8hpT98c=,iv:Po9bova4dfiykl+ckH4v6DqzSJOgULx7ro3kXMFRvFI=,tag:7jurD14N7QDRHX5ruFDEeQ==,type:str]
|
|
||||||
WEBUI_ADMIN_EMAIL=ENC[AES256_GCM,data:EZgNXSpbpROz3TZRLaSQTQ==,iv:i98kChemam9nB3iCMwCTRYB69b2eUBy6QCoeZ3AjAP0=,tag:INnB1Zp9VA6M//yyAhRh3A==,type:str]
|
|
||||||
WEBUI_ADMIN_NAME=ENC[AES256_GCM,data:8l8dJ85p,iv:LltveatNlX4FEGmxhtYLYmviIvLK0xSMuVsk9DRRglw=,tag:OrTlnOLlQe03hoYTkaPotg==,type:str]
|
|
||||||
WEBUI_ADMIN_PASSWORD=ENC[AES256_GCM,data:PKfZQHiAa96vcGUCGigjXQ==,iv:WLb1mgCV3IJHnHcBvf4yAPiautgXqCC2L2bzt6i0t7U=,tag:nw78tvyUOYmGMunBwvIr+A==,type:str]
|
|
||||||
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4d3pNVlZEQS85d2R3WUZX\nKy9iOFZ4MjU2UkQwVHdobTlBY3l0MldONWlvCnU5dllobmtLQXlMM28xN0FSTmxD\nNnhmZVRwdnpaZ3NkZDVCWERBckZiQjgKLS0tIFBPeXZMNXRjZ3pBQUlndXB5MTBB\nMVdhSGJvZkE2VzZiZ2VxL0RKTDJ2aDQKsxlibeAoO74411VemXT+8UBG0JdemgHD\nVONIEp/VsbEJDWgDfSGhLaH4KN2hTsCtyhdkCU0FohgWB+xWyJz6MA==\n-----END AGE ENCRYPTED FILE-----\n
|
|
||||||
sops_age__list_0__map_recipient=age13l2gtk0nzr484zprp7e0pkrt0ne0j4asyn2pjmlaw73nte7t7d8q4sqtxm
|
|
||||||
sops_lastmodified=2026-10-07T09:32:44Z
|
|
||||||
sops_mac=ENC[AES256_GCM,data:SSHgEEc3u2Zf13q5W4LD7bkrVlQTzLIYiZWXhBiDS6CjC4fUZcJq99OTSTNixzqpxSdnjeRtmzA6d6vGNfxvEOmsE1f4hBNm9ps0RHU4yLfr5vQG9Ff973uDwDU+JMqP3aMU+xpUuPkhW0zRpeST+w0thuPtjzhR2z/A2yPvYzU=,iv:5/cfD51eK0R9cGsr4wZu6CnwEdMjP0CYj3CM7+X4XQg=,tag:1FRcAULHG+XzmRiTMK8aWQ==,type:str]
|
|
||||||
sops_unencrypted_suffix=_unencrypted
|
|
||||||
sops_version=3.13.3
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
JWT_SECRET=ENC[AES256_GCM,data:+ut+3v4KrckbDT5m85JhQd8x2ayF55Uy5FiEw8qTJoBgz7Zz+HprhxPB09RDd6CX11SrcsDcf6vW3wOE7IdeQA==,iv:c3GqspoQH2+2NQvsqip3bs4XW1PWSZtK+l7HzE83Qj8=,tag:hDqFgPa8gYLqPeA0svGWfw==,type:str]
|
|
||||||
ADMIN_PASSWORD=ENC[AES256_GCM,data:UxcSEO7opTme9DR4XM3/FQ==,iv:nSpFt7rVp0K+hAc3aAoorw5XDMNK0V+zeBw4GHwTsOs=,tag:fQ1u/WtlVfEhc7fZnLnboQ==,type:str]
|
|
||||||
APP_URL=ENC[AES256_GCM,data:OJAv0C1DHYbFltgXmcSG/s97Lf3qJovkcEsPA9Xr,iv:Fw9Mh/+dYgah+/OWPBtRRXkO42KXWvKIuylyXfcaRK8=,tag:a6A8xS9aRyjvEfZvSxgMuQ==,type:str]
|
|
||||||
CORS_ORIGINS=ENC[AES256_GCM,data:z4MvIbQcvk+aUaME/llV7rWaDtCFYIT0nVGtlD8j,iv:oAL5NcWOfez+vVbKoibUIOagePROKW+4QV81sK+Cets=,tag:+QftIwvmTADEFMEz+ZCh4A==,type:str]
|
|
||||||
SMTP_HOST=
|
|
||||||
SMTP_PORT=ENC[AES256_GCM,data:QnI=,iv:PQwsVoOnLmTrnpUTaQAEOX3VG2hTLm/qnZjwIOL9kac=,tag:SZxCnlr0qTxH65bZ53rvQQ==,type:str]
|
|
||||||
SMTP_USER=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
SMTP_FROM=ENC[AES256_GCM,data:TaHhXO7WVUzywpUxTr5l+IG7QfXY,iv:gLqOSZBBzC9v/BT+r+ENLjApTmLsra2jDbenJLHn4AY=,tag:HCmGtfnVIjDktQpTtUbc9A==,type:str]
|
|
||||||
NOTIFY_EMAIL=
|
|
||||||
TAPE_LABEL_REGEX=
|
|
||||||
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnb283UjRSRU1SYjBxZWlz\nOWw2cXM2TTU2QmdWUG5nUU9vWVZhUDZoelJRCi9McmV0Q05hNVpXSllsbUYwdkEw\nTnU3OWRCcFhrQzg4blhuRFJjdDVkUFkKLS0tIEt4Nzc2ZWtOL1VQQzVObzZWYURu\nWFUwVWp5OUhDME0xVlBRS3psdVBSd0EKsy77QR7CveXQdKlo+JeNSaNpnUh//AoP\nV+hbUSIj05Ws20rr9uk8uTDnjnc91r2vxGWxznXf6M9putZfARBdfQ==\n-----END AGE ENCRYPTED FILE-----\n
|
|
||||||
sops_age__list_0__map_recipient=age13l2gtk0nzr484zprp7e0pkrt0ne0j4asyn2pjmlaw73nte7t7d8q4sqtxm
|
|
||||||
sops_lastmodified=2026-09-24T13:11:03Z
|
|
||||||
sops_mac=ENC[AES256_GCM,data:xJO2u9jQM8XiVYekVvwN+iv3megGpf80F1ANib9Kro/kgvTQUZU14jmku8OjfRtjrFsM9b/cBr+ml0Z+MSknmwtR4D3mfRIa0yFfqmS/VZJs8SrH2+c/a8kYGhDNcWgAbx+u/tZB8q0QrQsbDYmN8yvsNwWi2ixMLKlv2thPIbA=,iv:CEgU5499Gr0gD+M5iSYJ315r7RU9RWKKapXywVCQivo=,tag:9YTO7K/zsINSOXfR6PaG8A==,type:str]
|
|
||||||
sops_unencrypted_suffix=_unencrypted
|
|
||||||
sops_version=3.13.3
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
xlib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
# TapeRotation — web app for tracking and rotation of backup tape
|
|
||||||
# cartridges. https://github.com/ElizarovEugene/TapeRotation
|
|
||||||
#
|
|
||||||
# Podman adaptation of the upstream docker-compose deployment. Two
|
|
||||||
# containers on a shared "taperotation_default" network (mirrors the
|
|
||||||
# compose project network):
|
|
||||||
# - taperotation-backend: FastAPI/uvicorn on :8001, SQLite at /data,
|
|
||||||
# file attachments at /app/uploads
|
|
||||||
# - taperotation-frontend: nginx serving the built React app on :80,
|
|
||||||
# proxying /api to http://backend:8001
|
|
||||||
# The backend container gets a static IP on the shared network and the
|
|
||||||
# frontend maps "backend" → that IP via --add-host, because this host's
|
|
||||||
# CoreDNS service owns port 53 on every interface: the podman network DNS
|
|
||||||
# plugin (aardvark-dns) cannot bind on the network gateway, so a network
|
|
||||||
# with dns_enabled would refuse to attach containers.
|
|
||||||
#
|
|
||||||
# Published host port 5174 → container:80 for the web UI. Keep it out
|
|
||||||
# of networking.firewall like the other panel ports and front it with an
|
|
||||||
# nginx vhost, e.g. in server/nginx.nix:
|
|
||||||
# { domain = "tape-rotation.zeroq.su"; port = 5174; }
|
|
||||||
# and set APP_URL / CORS_ORIGINS in the sops-encrypted env file.
|
|
||||||
#
|
|
||||||
# Instance config lives in one sops-encrypted .env file (mirrors the
|
|
||||||
# upstream .env.example, sops-nix format = "dotenv", key = "" → whole
|
|
||||||
# file): sops modules/containers/secrets/tape-rotation.env
|
|
||||||
# On first boot the admin account is created from ADMIN_USERNAME /
|
|
||||||
# ADMIN_PASSWORD from that file.
|
|
||||||
let
|
|
||||||
panel = "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}/tape-rotation";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
virtualisation = {
|
|
||||||
podman = {
|
|
||||||
enable = true;
|
|
||||||
autoPrune = {
|
|
||||||
enable = true;
|
|
||||||
flags = [ "--all" ];
|
|
||||||
};
|
|
||||||
dockerCompat = true;
|
|
||||||
};
|
|
||||||
oci-containers = {
|
|
||||||
backend = "podman";
|
|
||||||
containers = {
|
|
||||||
"taperotation-backend" = {
|
|
||||||
image = "docker.io/elizaroveugene/taperotation-backend:latest";
|
|
||||||
environment = {
|
|
||||||
"DATABASE_URL" = "sqlite:////data/taperotation.db";
|
|
||||||
"JWT_EXPIRE_MINUTES" = "480";
|
|
||||||
"ADMIN_USERNAME" = "admin";
|
|
||||||
"ADMIN_LANGUAGE" = "en";
|
|
||||||
"NOTIFY_DAYS_BEFORE" = "7";
|
|
||||||
"TZ" = "Europe/Moscow";
|
|
||||||
};
|
|
||||||
environmentFiles = [ "/run/secrets/tape-rotation-env" ];
|
|
||||||
volumes = [
|
|
||||||
"${panel}/db:/data:rw"
|
|
||||||
"${panel}/uploads:/app/uploads:rw"
|
|
||||||
];
|
|
||||||
log-driver = "journald";
|
|
||||||
extraOptions = [
|
|
||||||
"--network=taperotation_default"
|
|
||||||
# Static IP the frontend reaches "backend" at (see --add-host
|
|
||||||
# in the frontend container; network DNS is disabled).
|
|
||||||
"--ip=10.89.0.10"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
"taperotation-frontend" = {
|
|
||||||
image = "docker.io/elizaroveugene/taperotation-frontend:latest";
|
|
||||||
ports = [
|
|
||||||
"0.0.0.0:5174:80/tcp"
|
|
||||||
];
|
|
||||||
log-driver = "journald";
|
|
||||||
extraOptions = [
|
|
||||||
"--network=taperotation_default"
|
|
||||||
# Baked-in nginx upstream is http://backend:8001; resolve it via
|
|
||||||
# /etc/hosts since the network has no DNS plugin.
|
|
||||||
"--add-host=backend:10.89.0.10"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Enable container name DNS for all Podman networks.
|
|
||||||
networking.firewall.interfaces =
|
|
||||||
let
|
|
||||||
matchAll = if !config.networking.nftables.enable then "podman+" else "podman*";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
"${matchAll}".allowedUDPPorts = [ 53 ];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd = {
|
|
||||||
services = {
|
|
||||||
"podman-taperotation-backend" = {
|
|
||||||
serviceConfig.Restart = lib.mkOverride 90 "always";
|
|
||||||
after = [ "podman-network-taperotation_default.service" ];
|
|
||||||
requires = [ "podman-network-taperotation_default.service" ];
|
|
||||||
partOf = [ "podman-compose-tape-rotation-root.target" ];
|
|
||||||
wantedBy = [ "podman-compose-tape-rotation-root.target" ];
|
|
||||||
};
|
|
||||||
"podman-taperotation-frontend" = {
|
|
||||||
serviceConfig.Restart = lib.mkOverride 90 "always";
|
|
||||||
after = [
|
|
||||||
"podman-network-taperotation_default.service"
|
|
||||||
"podman-taperotation-backend.service"
|
|
||||||
];
|
|
||||||
requires = [ "podman-network-taperotation_default.service" ];
|
|
||||||
partOf = [ "podman-compose-tape-rotation-root.target" ];
|
|
||||||
wantedBy = [ "podman-compose-tape-rotation-root.target" ];
|
|
||||||
};
|
|
||||||
"podman-network-taperotation_default" = {
|
|
||||||
path = [ pkgs.podman ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
RemainAfterExit = true;
|
|
||||||
ExecStop = "podman network rm -f taperotation_default";
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
# Always (re)create the stack network: the host's CoreDNS owns :53
|
|
||||||
# on every interface, so the network DNS plugin (aardvark-dns)
|
|
||||||
# can't bind on the gateway → --disable-dns. --subnet backs the
|
|
||||||
# backend's static IP. Recreate-on-start also self-heals after a
|
|
||||||
# `podman system prune` removed the (temporarily unused) network.
|
|
||||||
podman network rm -f taperotation_default >/dev/null 2>&1 || true
|
|
||||||
podman network create --disable-dns --subnet=10.89.0.0/24 taperotation_default
|
|
||||||
'';
|
|
||||||
partOf = [ "podman-compose-tape-rotation-root.target" ];
|
|
||||||
wantedBy = [ "podman-compose-tape-rotation-root.target" ];
|
|
||||||
};
|
|
||||||
"podman-update-taperotation" = {
|
|
||||||
path = [ pkgs.podman ];
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
TimeoutSec = 300;
|
|
||||||
};
|
|
||||||
script = ''
|
|
||||||
podman pull docker.io/elizaroveugene/taperotation-backend:latest
|
|
||||||
podman pull docker.io/elizaroveugene/taperotation-frontend:latest
|
|
||||||
systemctl restart podman-taperotation-backend.service podman-taperotation-frontend.service
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
# Starts/stops together with all TapeRotation containers.
|
|
||||||
targets."podman-compose-tape-rotation-root" = {
|
|
||||||
unitConfig.Description = "Root target generated by compose2nix.";
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
};
|
|
||||||
# Enable automatic image updates:
|
|
||||||
# systemd.timers."podman-update-taperotation" = {
|
|
||||||
# wantedBy = [ "timers.target" ];
|
|
||||||
# timerConfig = {
|
|
||||||
# OnCalendar = "weekly";
|
|
||||||
# Persistent = true;
|
|
||||||
# };
|
|
||||||
# };
|
|
||||||
tmpfiles.rules = [
|
|
||||||
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755"
|
|
||||||
"root"
|
|
||||||
"root"
|
|
||||||
)
|
|
||||||
(xlib.helpers.mkTmpfile "d" panel "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" "${panel}/db" "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" "${panel}/uploads" "0755" "root" "root")
|
|
||||||
# Relabel panel dir for SELinux so containers can access it.
|
|
||||||
(xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root")
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
sops.secrets."tape-rotation-env" = {
|
|
||||||
# key = "" → decrypt the whole file, not a single key.
|
|
||||||
# format = "dotenv" → the file IS one .env ready for environmentFiles:
|
|
||||||
# every non-comment KEY=VALUE line lands in the container environment.
|
|
||||||
key = "";
|
|
||||||
format = "dotenv";
|
|
||||||
sopsFile = ./secrets/tape-rotation.env;
|
|
||||||
mode = "0400";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+34
-23
@@ -1,60 +1,71 @@
|
|||||||
{ inputs, ... }@flakeContext:
|
{ inputs, ... }@flakeContext:
|
||||||
let
|
let
|
||||||
# NixOS-only modules. termux runs nix-on-droid (its own module system,
|
|
||||||
# class = "nixOnDroid"): options like services.*, users.*, sops.*, disko.*
|
|
||||||
# and nixpkgs.overlays (flake assertion) do not exist there.
|
|
||||||
#
|
|
||||||
# `xlib` arrives as a module argument (see lib/mkSystem.nix) and is plain
|
|
||||||
# data, not a module option, so nothing here has to declare or set it.
|
|
||||||
defaultModule =
|
defaultModule =
|
||||||
{
|
{
|
||||||
|
config,
|
||||||
|
deviceType,
|
||||||
lib,
|
lib,
|
||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
|
# NixOS-only modules. termux runs nix-on-droid (its own module system,
|
||||||
|
# class = "nixOnDroid"): options like services.*, users.*, sops.*, disko.*
|
||||||
|
# and nixpkgs.overlays (flake assertion) do not exist there.
|
||||||
imports =
|
imports =
|
||||||
with inputs;
|
with inputs;
|
||||||
[
|
[
|
||||||
./essentials
|
|
||||||
./options.nix
|
./options.nix
|
||||||
|
(./. + "/${deviceType}") # specific modules
|
||||||
|
]
|
||||||
|
++ lib.optionals (deviceType != "termux") [
|
||||||
|
./essentials
|
||||||
./users.nix
|
./users.nix
|
||||||
|
|
||||||
home-manager.nixosModules.home-manager # home-manager module
|
home-manager.nixosModules.home-manager # home-manager module
|
||||||
# nix-index-database.nixosModules.nix-index # nix-index module
|
# nix-index-database.nixosModules.nix-index # nix-index module
|
||||||
grub2-themes.nixosModules.default # grub2 themes module
|
grub2-themes.nixosModules.default # grub2 themes module
|
||||||
sops-nix.nixosModules.sops # sops module
|
sops-nix.nixosModules.sops # sops module
|
||||||
justray.nixosModules.default
|
|
||||||
self.homeConfigurations.default.nixosModule # default homeConfigurations
|
self.homeConfigurations.default.nixosModule # default homeConfigurations
|
||||||
disko.nixosModules.disko # disko module
|
disko.nixosModules.disko # disko module
|
||||||
|
];
|
||||||
|
# nix-on-droid asserts that nixpkgs.* stays unset in flake mode.
|
||||||
|
nixpkgs.overlays = lib.mkIf (deviceType != "termux") (
|
||||||
|
with inputs; [
|
||||||
|
self.nixosOverlays.default
|
||||||
]
|
]
|
||||||
# desktop class: primary/secondary
|
);
|
||||||
++ lib.optional xlib.isDesktop ./desktop
|
_module.args = {
|
||||||
# device-type module dir; "minimal" has no extra modules
|
inputs = inputs;
|
||||||
++ lib.optional (!xlib.isDesktop && xlib.device.type != "minimal") (./. + "/${xlib.device.type}");
|
xlib = config.xlib;
|
||||||
nixpkgs.overlays = with inputs; [
|
};
|
||||||
self.nixosOverlays.default
|
|
||||||
];
|
|
||||||
networking.hostName = lib.mkDefault xlib.device.hostname;
|
|
||||||
};
|
};
|
||||||
strictModule =
|
publicModule =
|
||||||
{
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = with inputs; [
|
||||||
# ./essentials
|
./essentials
|
||||||
# ./users.nix
|
./users.nix
|
||||||
./options.nix
|
./options.nix
|
||||||
(./. + "/${xlib.device.type}")
|
|
||||||
# sops-nix.nixosModules.sops
|
disko.nixosModules.disko # disko module
|
||||||
|
sops-nix.nixosModules.sops # sops module
|
||||||
];
|
];
|
||||||
|
|
||||||
|
_module.args = {
|
||||||
|
inputs = inputs;
|
||||||
|
xlib = config.xlib;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
nixosModules = {
|
nixosModules = {
|
||||||
default = defaultModule;
|
default = defaultModule;
|
||||||
strict = strictModule;
|
public = publicModule;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-49
@@ -3,7 +3,6 @@
|
|||||||
inputs,
|
inputs,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
xlib,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
@@ -12,54 +11,6 @@
|
|||||||
./theming.nix
|
./theming.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
# Things every desktop host has in common
|
|
||||||
hardware.bluetooth.enable = true;
|
|
||||||
|
|
||||||
i18n.extraLocaleSettings = {
|
|
||||||
LC_ADDRESS = "ru_RU.UTF-8";
|
|
||||||
LC_IDENTIFICATION = "ru_RU.UTF-8";
|
|
||||||
LC_MEASUREMENT = "ru_RU.UTF-8";
|
|
||||||
LC_MONETARY = "ru_RU.UTF-8";
|
|
||||||
LC_NAME = "ru_RU.UTF-8";
|
|
||||||
LC_NUMERIC = "ru_RU.UTF-8";
|
|
||||||
LC_PAPER = "ru_RU.UTF-8";
|
|
||||||
LC_TELEPHONE = "ru_RU.UTF-8";
|
|
||||||
LC_TIME = "ru_RU.UTF-8";
|
|
||||||
};
|
|
||||||
|
|
||||||
networking = {
|
|
||||||
networkmanager.enable = true;
|
|
||||||
firewall.enable = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
security.rtkit.enable = true;
|
|
||||||
|
|
||||||
services = {
|
|
||||||
syncthing = {
|
|
||||||
enable = true;
|
|
||||||
systemService = true;
|
|
||||||
configDir = "${xlib.dirs.user-storage}/persist/Syncthing/${config.system.name}";
|
|
||||||
dataDir = "${xlib.dirs.user-home}";
|
|
||||||
group = "users";
|
|
||||||
user = "${xlib.device.username}";
|
|
||||||
};
|
|
||||||
thermald.enable = true;
|
|
||||||
xserver = {
|
|
||||||
enable = true;
|
|
||||||
xkb = {
|
|
||||||
layout = "us,ru";
|
|
||||||
variant = "";
|
|
||||||
# options = "grp:alt_shift_toggle";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
libinput.enable = true;
|
|
||||||
colord.enable = true;
|
|
||||||
printing = {
|
|
||||||
enable = true;
|
|
||||||
cups-pdf.enable = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
boot = {
|
boot = {
|
||||||
plymouth = {
|
plymouth = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -102,6 +53,22 @@
|
|||||||
steam.enable = true;
|
steam.enable = true;
|
||||||
xwayland.enable = true;
|
xwayland.enable = true;
|
||||||
};
|
};
|
||||||
|
services = {
|
||||||
|
xserver = {
|
||||||
|
enable = true;
|
||||||
|
xkb = {
|
||||||
|
layout = "us,ru";
|
||||||
|
variant = "";
|
||||||
|
# options = "grp:alt_shift_toggle";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
libinput.enable = true;
|
||||||
|
colord.enable = true;
|
||||||
|
printing = {
|
||||||
|
enable = true;
|
||||||
|
cups-pdf.enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
# environment = {
|
# environment = {
|
||||||
# systemPackages = [
|
# systemPackages = [
|
||||||
# pkgs.pcbu-desktop
|
# pkgs.pcbu-desktop
|
||||||
|
|||||||
@@ -6,5 +6,6 @@
|
|||||||
./kde.nix
|
./kde.nix
|
||||||
# ./gnome.nix
|
# ./gnome.nix
|
||||||
# ./noctalia.nix
|
# ./noctalia.nix
|
||||||
|
# ./xfce.nix
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
|
||||||
|
services.xserver.displayManager.lightdm.enable = true;
|
||||||
|
#services.displayManager.defaultSession = "lomiri";
|
||||||
|
# services.xserver.desktopManager.budgie.enable = true;
|
||||||
|
#services.xserver.displayManager.lightdm.greeters.lomiri.enable= true;
|
||||||
|
|
||||||
|
#services.desktopManager.lomiri.enable = true;
|
||||||
|
#-services.xserver.desktopManager.mate.enable = true;
|
||||||
|
#-services.xserver.desktopManager.lxqt.enable = true;
|
||||||
|
# services.xserver.desktopManager.lumina.enable = true;
|
||||||
|
# services.xserver.desktopManager.cde.enable = true;
|
||||||
|
# services.xserver.desktopManager.cinnamon.enable = true;
|
||||||
|
# services.xserver.desktopManager.enlightenment.enable = true;
|
||||||
|
# services.desktopManager.cosmic.xwayland.enable = true;
|
||||||
|
# services.desktopManager.cosmic.enable = true;
|
||||||
|
|
||||||
|
services.xserver = {
|
||||||
|
enable = true;
|
||||||
|
desktopManager = {
|
||||||
|
#xterm.enable = false;
|
||||||
|
xfce.enable = true;
|
||||||
|
xfce.enableWaylandSession = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#- services.xserver.desktopManager.pantheon.enable = true;
|
||||||
|
#- services.pantheon.apps.enable = true;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
./packages.nix
|
./packages.nix
|
||||||
./services.nix
|
./services.nix
|
||||||
./settings.nix
|
./settings.nix
|
||||||
./ssh.nix
|
|
||||||
./systemd-routines.nix
|
./systemd-routines.nix
|
||||||
./shell.nix
|
./shell.nix
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
inputs,
|
inputs,
|
||||||
xlib,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
master = import inputs.nixpkgs-master {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
};
|
||||||
|
in
|
||||||
{
|
{
|
||||||
environment = {
|
environment = {
|
||||||
systemPackages = with pkgs; [
|
systemPackages = with pkgs; [
|
||||||
@@ -101,9 +105,6 @@
|
|||||||
# Test
|
# Test
|
||||||
rgx
|
rgx
|
||||||
net-tools
|
net-tools
|
||||||
usbtree
|
|
||||||
iperf3
|
|
||||||
glow
|
|
||||||
# lazydocker
|
# lazydocker
|
||||||
# dtop
|
# dtop
|
||||||
# framework-tool-tui
|
# framework-tool-tui
|
||||||
@@ -112,9 +113,6 @@
|
|||||||
environment.variables.EDITOR = "fresh";
|
environment.variables.EDITOR = "fresh";
|
||||||
programs = {
|
programs = {
|
||||||
# nix-ld.enable = true;
|
# nix-ld.enable = true;
|
||||||
justray = {
|
|
||||||
enable = true;
|
|
||||||
};
|
|
||||||
nano = {
|
nano = {
|
||||||
enable = true;
|
enable = true;
|
||||||
nanorc = ''
|
nanorc = ''
|
||||||
@@ -187,7 +185,7 @@
|
|||||||
enable = true;
|
enable = true;
|
||||||
config = {
|
config = {
|
||||||
user = {
|
user = {
|
||||||
name = xlib.device.username;
|
name = "oqyude";
|
||||||
email = "oqyude@gmail.com";
|
email = "oqyude@gmail.com";
|
||||||
};
|
};
|
||||||
pull = {
|
pull = {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
config,
|
||||||
|
inputs,
|
||||||
lib,
|
lib,
|
||||||
|
pkgs,
|
||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
services.tailscale.enable = xlib.device.type != "wsl"; # true, if not wsl
|
|
||||||
|
|
||||||
# All real hosts (not the bare "minimal" test config) get OOM protection
|
|
||||||
# and a bounded journal.
|
|
||||||
services = {
|
services = {
|
||||||
earlyoom.enable = lib.mkIf (xlib.device.type != "minimal") true;
|
tailscale.enable = xlib.device.type != "wsl"; # true, if not wsl
|
||||||
journald.settings.Journal = lib.mkIf (xlib.device.type != "minimal") {
|
|
||||||
SystemMaxUse = "512M";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
settings = {
|
settings = {
|
||||||
require-sigs = false;
|
require-sigs = false;
|
||||||
substituters = [
|
substituters = [
|
||||||
"https://nix-cache.zeroq.su"
|
"http://100.64.0.0:5000"
|
||||||
"https://cache.nixos.org"
|
"https://cache.nixos.org"
|
||||||
"https://nix-community.cachix.org"
|
"https://nix-community.cachix.org"
|
||||||
"https://mirror.yandex.ru/nixos"
|
"https://mirror.yandex.ru/nixos"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
# "https://nixos.snix.store" # https://nixos.snix.store/
|
# "https://nixos.snix.store" # https://nixos.snix.store/
|
||||||
];
|
];
|
||||||
trusted-public-keys = [
|
trusted-public-keys = [
|
||||||
"nix-cache.zeroq.su:be5jFLkiwNyOep/McxSafB3jguBmztxx+oJ46ySyc/s="
|
"nix-cache.home.arpa:be5jFLkiwNyOep/McxSafB3jguBmztxx+oJ46ySyc/s="
|
||||||
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
|
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
|
||||||
];
|
];
|
||||||
stalled-download-timeout = 8;
|
stalled-download-timeout = 8;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
xlib,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
@@ -21,7 +20,7 @@
|
|||||||
};
|
};
|
||||||
shellInit = ''
|
shellInit = ''
|
||||||
beet-p() {
|
beet-p() {
|
||||||
local base="${xlib.dirs.user-home}/.config/beets/My"
|
local base="/home/oqyude/.config/beets/My"
|
||||||
local rel
|
local rel
|
||||||
rel=$(realpath --relative-to="$base" "$PWD")
|
rel=$(realpath --relative-to="$base" "$PWD")
|
||||||
beet mod "path:$rel" playlist="$*"
|
beet mod "path:$rel" playlist="$*"
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
beet im ./ -S $*
|
beet im ./ -S $*
|
||||||
}
|
}
|
||||||
beet-path() {
|
beet-path() {
|
||||||
realpath --relative-to="${xlib.dirs.user-home}/.config/beets/My" "$1"
|
realpath --relative-to="/home/oqyude/.config/beets/My" "$1"
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
shellAliases = {
|
shellAliases = {
|
||||||
@@ -46,7 +45,7 @@
|
|||||||
gc = "git add . && git commit -m 'dev: автокоммит $(date +'%Y-%m-%d %H:%M:%S')'";
|
gc = "git add . && git commit -m 'dev: автокоммит $(date +'%Y-%m-%d %H:%M:%S')'";
|
||||||
y = "yazi";
|
y = "yazi";
|
||||||
nix-shellp = "nix-shell --run $SHELL -p";
|
nix-shellp = "nix-shell --run $SHELL -p";
|
||||||
beet-path-library = "realpath --relative-to='${xlib.dirs.user-home}/.config/beets/My' .";
|
beet-path-library = "realpath --relative-to='/home/oqyude/.config/beets/My' .";
|
||||||
z-proxy = "export ALL_PROXY=socks5://localhost:10808";
|
z-proxy = "export ALL_PROXY=socks5://localhost:10808";
|
||||||
zh-proxy = "export HTTPS_PROXY=http://localhost:10808 && export HTTP_PROXY=http://localhost:10808";
|
zh-proxy = "export HTTPS_PROXY=http://localhost:10808 && export HTTP_PROXY=http://localhost:10808";
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
options.host.ssh = {
|
|
||||||
enable = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Enable the SSH server with the shared config below.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf config.host.ssh.enable {
|
|
||||||
services.openssh = {
|
|
||||||
enable = true;
|
|
||||||
allowSFTP = true;
|
|
||||||
openFirewall = lib.mkDefault false;
|
|
||||||
hostKeys = [
|
|
||||||
{
|
|
||||||
path = "/etc/ssh/id_ed25519";
|
|
||||||
type = "ed25519";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
settings = {
|
|
||||||
PasswordAuthentication = false;
|
|
||||||
PermitRootLogin = "yes";
|
|
||||||
UsePAM = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -11,13 +11,13 @@
|
|||||||
description = "Prebuild NixOS closure";
|
description = "Prebuild NixOS closure";
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
CPUQuota = "20%";
|
CPUQuota = "20%";
|
||||||
User = xlib.device.username;
|
User = "oqyude";
|
||||||
Group = "users";
|
Group = "users";
|
||||||
Nice = 10;
|
Nice = 10;
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
WorkingDirectory = "/tmp";
|
WorkingDirectory = "/tmp";
|
||||||
Environment = [
|
Environment = [
|
||||||
"HOME=${xlib.dirs.user-home}"
|
"HOME=/home/oqyude"
|
||||||
];
|
];
|
||||||
ExecStart = ''
|
ExecStart = ''
|
||||||
${pkgs.nix}/bin/nix build --no-link /etc/nixos#nixosConfigurations.${config.networking.hostName}.config.system.build.toplevel
|
${pkgs.nix}/bin/nix build --no-link /etc/nixos#nixosConfigurations.${config.networking.hostName}.config.system.build.toplevel
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{ inputs, ... }@flakeContext:
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
systemd.services.zapret = {
|
||||||
|
enable = true;
|
||||||
|
description = "zapret complete";
|
||||||
|
unitConfig = {
|
||||||
|
After = [ "network-online.target" ];
|
||||||
|
Wants = [ "network-online.target" ];
|
||||||
|
};
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
path = [ "/run/current-system/sw" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
Restart = "on-failure";
|
||||||
|
User = "root";
|
||||||
|
WorkingDirectory = "${inputs.zapret.script-dir}";
|
||||||
|
ExecStart = "/run/current-system/sw/bin/bash ./main_script.sh -nointeractive";
|
||||||
|
ExecStop = "/run/current-system/sw/bin/bash ./stop_and_clean_nft.sh";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
environment = {
|
||||||
|
systemPackages = with pkgs; [
|
||||||
|
nftables
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
# imports = [
|
||||||
|
# ];
|
||||||
|
}
|
||||||
+121
-68
@@ -1,76 +1,129 @@
|
|||||||
{
|
{
|
||||||
|
config,
|
||||||
lib,
|
lib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Cross-module options: declared here, not in the module that reads them.
|
|
||||||
#
|
|
||||||
# An option belongs in this file when at least one context *sets* it while
|
|
||||||
# another module *reads* it — the reader cannot be the only place that knows
|
|
||||||
# the option exists. `modules/essentials/ssh.nix` does not belong here: it
|
|
||||||
# declares and reads `host.ssh.enable` itself, within one module.
|
|
||||||
{
|
{
|
||||||
# Remote-builder wiring. A coordinator (e.g. sapphira) sets
|
options = {
|
||||||
# `host.builder.clients` to register remote build machines;
|
xlib = {
|
||||||
# a builder host (e.g. the WSL on vetymae) sets `host.builder.enable`
|
device = {
|
||||||
# to advertise itself. The two halves are intentionally split so a single
|
type = lib.mkOption {
|
||||||
# declaration in configurations/* is enough to flip each side.
|
type = lib.types.enum [
|
||||||
options.host.builder = {
|
"minimal"
|
||||||
enable = lib.mkOption {
|
"primary"
|
||||||
type = lib.types.bool;
|
"secondary"
|
||||||
default = false;
|
"server"
|
||||||
description = ''
|
"vds"
|
||||||
Advertise this host as a remote Nix builder and accept builds
|
"vds-new"
|
||||||
from other machines in the flake over SSH.
|
"wsl"
|
||||||
'';
|
"termux"
|
||||||
};
|
];
|
||||||
clients = lib.mkOption {
|
default = "minimal";
|
||||||
type = lib.types.listOf lib.types.attrs;
|
description = "Type of device for this host.";
|
||||||
default = [ ];
|
};
|
||||||
description = ''
|
username = lib.mkOption {
|
||||||
List of remote Nix build machines this coordinator should
|
type = lib.types.str;
|
||||||
register via `nix.buildMachines`. Each entry matches the NixOS
|
default = "oqyude";
|
||||||
option schema (hostName, sshUser, sshKey, systems,
|
description = "Username for host.";
|
||||||
supportedFeatures, ...). Two extra attributes are consumed by
|
};
|
||||||
modules/server/builder.nix and stripped before reaching
|
hostname = lib.mkOption {
|
||||||
`nix.buildMachines`:
|
type = lib.types.str;
|
||||||
- `proxyCommand` — generates a per-builder Host block in the
|
default = "nixos";
|
||||||
system-wide OpenSSH config (the nix-daemon runs as root and
|
description = "Hostname...";
|
||||||
cannot see the user's ~/.ssh/config).
|
};
|
||||||
- `hostKeyAlias` — alias used inside that SSH matchBlock.
|
};
|
||||||
A builder reachable on its own (no ProxyCommand needed) omits
|
dirs = {
|
||||||
both and gets no SSH matchBlock. Empty by default — opt in by
|
user-home = lib.mkOption {
|
||||||
setting this list.
|
type = lib.types.str;
|
||||||
'';
|
default = "/home/${config.xlib.device.username}";
|
||||||
};
|
description = "User home directory.";
|
||||||
};
|
};
|
||||||
|
user-storage = lib.mkOption {
|
||||||
options.host."3x-ui" = {
|
type = lib.types.str;
|
||||||
# Domain whose Let's Encrypt cert (at /var/lib/acme/<domain>/)
|
default = "${config.xlib.dirs.user-home}/Storage";
|
||||||
# gets mounted read-only into the 3x-ui container so the panel
|
description = "User storage directory.";
|
||||||
# can terminate TLS itself. Set null if 3x-ui serves plain HTTP
|
};
|
||||||
# and TLS is terminated by an upstream nginx.
|
archive-drive = lib.mkOption {
|
||||||
certDomain = lib.mkOption {
|
type = lib.types.str;
|
||||||
type = lib.types.nullOr lib.types.str;
|
default = "/mnt/archive";
|
||||||
default = null;
|
description = "Archive drive mount point.";
|
||||||
example = "pubray1.zeroq.su";
|
};
|
||||||
description = ''
|
lamet-drive = lib.mkOption {
|
||||||
Domain whose LE cert should be mounted into the 3x-ui
|
type = lib.types.str;
|
||||||
container at /root/cert/fullchain.pem and key.pem.
|
default = "/mnt/lamet";
|
||||||
'';
|
description = "Lamet drive mount point.";
|
||||||
};
|
};
|
||||||
# Publish host:15380 → container:443. Only nodes that host an
|
mobile-drive = lib.mkOption {
|
||||||
# Xray REALITY inbound on container:443 need this (so nginx
|
type = lib.types.str;
|
||||||
# stream can forward TLS to Xray via 127.0.0.1:15380 while Xray
|
default = "/mnt/mobile";
|
||||||
# itself sees incoming connections on its configured port 443).
|
description = "Mobile drive mount point.";
|
||||||
# Set false on nodes that only run the 3x-ui panel.
|
};
|
||||||
reality443Forwarding = lib.mkOption {
|
therima-drive = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.str;
|
||||||
default = false;
|
default = "/mnt/therima";
|
||||||
description = ''
|
description = "Therima drive mount point.";
|
||||||
When true, publish host:15380 → container:443 so Xray
|
};
|
||||||
inside the container can serve REALITY on its real
|
vetymae-drive = lib.mkOption {
|
||||||
configured port 443 (nginx stream forwards 443 → 15380).
|
type = lib.types.str;
|
||||||
'';
|
default = "/mnt/vetymae";
|
||||||
|
description = "Vetymae drive mount point.";
|
||||||
|
};
|
||||||
|
soptur-drive = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/mnt/soptur";
|
||||||
|
description = "Soptur drive mount point.";
|
||||||
|
};
|
||||||
|
wsl-home = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/mnt/c/Users/${config.xlib.device.username}";
|
||||||
|
description = "WSL home directory.";
|
||||||
|
};
|
||||||
|
wsl-storage = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "${config.xlib.dirs.wsl-home}/Storage";
|
||||||
|
description = "WSL storage directory.";
|
||||||
|
};
|
||||||
|
server-home = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/home/${config.xlib.device.username}/External";
|
||||||
|
description = "Server home directory.";
|
||||||
|
};
|
||||||
|
server-credentials = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "${config.xlib.dirs.server-home}/Credentials/server";
|
||||||
|
description = "Server credentials directory.";
|
||||||
|
};
|
||||||
|
storage = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "${config.xlib.dirs.server-home}/Storage";
|
||||||
|
description = "General storage directory.";
|
||||||
|
};
|
||||||
|
calibre-library = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "${config.xlib.dirs.server-home}/Books-Library";
|
||||||
|
description = "Calibre library directory.";
|
||||||
|
};
|
||||||
|
music-library = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "${config.xlib.dirs.user-home}/Music";
|
||||||
|
description = "Music library directory.";
|
||||||
|
};
|
||||||
|
services-folder = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "${config.xlib.dirs.server-home}/Services";
|
||||||
|
description = "All services folder.";
|
||||||
|
};
|
||||||
|
services-mnt-folder = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "/mnt/services";
|
||||||
|
description = "All services folder.";
|
||||||
|
};
|
||||||
|
postgresql-folder = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "${config.xlib.dirs.services-mnt-folder}/postgresql";
|
||||||
|
description = "PostgreSQL service folder.";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
xlib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
user = "snity";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
users = {
|
||||||
|
users = {
|
||||||
|
"${user}" = {
|
||||||
|
name = "${user}";
|
||||||
|
isNormalUser = true;
|
||||||
|
group = "users";
|
||||||
|
description = "Snity";
|
||||||
|
hashedPassword = "$y$j9T$851xwObfIp7SYzIyFtH.k1$mNofT2sxEAV50Kxgmwvqc6Kj/3B/fJoPP8qgn./siEB";
|
||||||
|
homeMode = "700";
|
||||||
|
home = "/home/${user}";
|
||||||
|
extraGroups = [
|
||||||
|
"audio"
|
||||||
|
"disk"
|
||||||
|
"gamemode"
|
||||||
|
"networkmanager"
|
||||||
|
"pipewire"
|
||||||
|
"wheel"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../desktop
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../desktop
|
||||||
|
];
|
||||||
|
}
|
||||||
+11
-11
@@ -8,20 +8,20 @@
|
|||||||
acme = {
|
acme = {
|
||||||
acceptTerms = true;
|
acceptTerms = true;
|
||||||
defaults = {
|
defaults = {
|
||||||
email = "oqyude@gmail.com";
|
email = "oqyude@zeroq.su";
|
||||||
|
server = "https://localhost:9000/acme/acme/directory";
|
||||||
|
dnsProvider = null;
|
||||||
};
|
};
|
||||||
# certs = {
|
# certs = {
|
||||||
# "home.arpa" = {
|
# # "home.arpa" = {
|
||||||
# email = "oqyude@zeroq.su";
|
# # domain = "*.home.arpa";
|
||||||
# domain = "*.home.arpa";
|
# # server = "https://localhost:9000/acme/acme/directory";
|
||||||
# server = "https://localhost:9000/acme/acme/directory";
|
# # listenHTTP = ":80";
|
||||||
# listenHTTP = ":80";
|
|
||||||
# dnsProvider = null;
|
|
||||||
# };
|
|
||||||
# # "turn.home.arpa" = {
|
|
||||||
# # listenHTTP = "127.0.0.1:80";
|
|
||||||
# # group = "turnserver";
|
|
||||||
# # };
|
# # };
|
||||||
|
# "turn.home.arpa" = {
|
||||||
|
# listenHTTP = "127.0.0.1:80";
|
||||||
|
# group = "turnserver";
|
||||||
|
# };
|
||||||
# };
|
# };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
# sapphira (and any other server-class coordinator) — register remote
|
|
||||||
# builders, and make sure the nix daemon (running as root) can resolve the
|
|
||||||
# SSH host alias with its ProxyCommand chain.
|
|
||||||
#
|
|
||||||
# The `host.builder.clients` option itself is declared in
|
|
||||||
# modules/options.nix (cross-module). The actual builder list is set by the
|
|
||||||
# configuration (e.g. configurations/server.nix) — this module is generic
|
|
||||||
# over every entry on the list.
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
# Attributes that belong to the SSH matchBlock only — NOT to
|
|
||||||
# `nix.buildMachines` (that schema has no hostKeyAlias/proxyCommand).
|
|
||||||
# Strip them before handing the list to nix.buildMachines.
|
|
||||||
sshOnlyAttrs = [
|
|
||||||
"hostKeyAlias"
|
|
||||||
"proxyCommand"
|
|
||||||
];
|
|
||||||
forNix = b: removeAttrs b sshOnlyAttrs;
|
|
||||||
# After NixOS's nix.buildMachines submodule runs, each entry has all
|
|
||||||
# attributes defaulted (protocol=ssh, systems=[], etc.). Read from that
|
|
||||||
# processed list so the formatter never trips on a missing field.
|
|
||||||
processedBuilders = config.nix.buildMachines;
|
|
||||||
|
|
||||||
# Serialise one builder to the textual format Nix's daemon expects in
|
|
||||||
# `nix.conf`'s `builders` line. Mirrors `buildMachinesText` from
|
|
||||||
# nixos/modules/config/nix-remote-build.nix so the result is identical
|
|
||||||
# to what NixOS writes to /etc/nix/machines — we just inline it instead
|
|
||||||
# of relying on `@/etc/nix/machines`, which Nix 2.34 parses but does
|
|
||||||
# not act on (the daemon's `external-builders` list stays empty and the
|
|
||||||
# client reports "configure remote builders via 'builders'" forever).
|
|
||||||
formatBuilder = b:
|
|
||||||
let
|
|
||||||
# Nix 2.34 refuses to dispatch derivations to a builder whose protocol
|
|
||||||
# is `ssh` (the NixOS default): the daemon leaves `external-builders`
|
|
||||||
# empty even when the `builders` line is well-formed, and the client
|
|
||||||
# falls back to local. `ssh-ng` (the new in-band protocol) actually
|
|
||||||
# opens the dispatcher. Override the NixOS default here.
|
|
||||||
proto = "ssh-ng://";
|
|
||||||
user = if b.sshUser != null && b.sshUser != "" then "${b.sshUser}@" else "";
|
|
||||||
systems =
|
|
||||||
if b.system != null then b.system
|
|
||||||
else if b.systems != [ ] then lib.concatStringsSep "," b.systems
|
|
||||||
else "-";
|
|
||||||
sshKey = if b.sshKey != null && b.sshKey != "" then b.sshKey else "-";
|
|
||||||
maxJobs = toString b.maxJobs;
|
|
||||||
speedFactor = toString b.speedFactor;
|
|
||||||
allFeats = b.supportedFeatures ++ b.mandatoryFeatures;
|
|
||||||
supported =
|
|
||||||
if allFeats == [ ] then "-"
|
|
||||||
else lib.concatStringsSep "," allFeats;
|
|
||||||
mandatory =
|
|
||||||
if b.mandatoryFeatures == [ ] then "-"
|
|
||||||
else lib.concatStringsSep "," b.mandatoryFeatures;
|
|
||||||
publicKey = if b.publicHostKey != null then b.publicHostKey else "-";
|
|
||||||
in
|
|
||||||
lib.concatStringsSep " " [
|
|
||||||
"${proto}${user}${b.hostName}"
|
|
||||||
systems
|
|
||||||
sshKey
|
|
||||||
maxJobs
|
|
||||||
speedFactor
|
|
||||||
supported
|
|
||||||
mandatory
|
|
||||||
publicKey
|
|
||||||
];
|
|
||||||
inlineBuilders = lib.concatMapStringsSep "\n" formatBuilder processedBuilders;
|
|
||||||
|
|
||||||
# One OpenSSH host block per builder that needs a ProxyCommand.
|
|
||||||
# Placed in `programs.ssh.extraConfig` so it ends up in
|
|
||||||
# /etc/ssh/ssh_config (the file OpenSSH consults system-wide, including
|
|
||||||
# for the nix-daemon running as root).
|
|
||||||
#
|
|
||||||
# Only builders with a `proxyCommand` attribute get a block: a builder
|
|
||||||
# reachable on its own (e.g. otreca on a public IP) needs no help from
|
|
||||||
# here. The attribute is the literal ProxyCommand string (passed
|
|
||||||
# verbatim to ssh); the configuration is responsible for matching it
|
|
||||||
# with the `hostName` field.
|
|
||||||
hostBlock = b: ''
|
|
||||||
Host ${b.hostName}
|
|
||||||
User ${b.sshUser}
|
|
||||||
HostKeyAlias ${b.hostKeyAlias or b.hostName}
|
|
||||||
ProxyCommand ${b.proxyCommand}
|
|
||||||
StrictHostKeyChecking accept-new
|
|
||||||
ServerAliveInterval 30
|
|
||||||
ServerAliveCountMax 3
|
|
||||||
ControlMaster auto
|
|
||||||
ControlPersist 60
|
|
||||||
ConnectTimeout 15
|
|
||||||
'';
|
|
||||||
blocks = map hostBlock (lib.filter (b: b ? proxyCommand) config.host.builder.clients);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf (config.host.builder.clients != [ ]) {
|
|
||||||
# Off-by-default in NixOS. Without this, the nix-remote-build module
|
|
||||||
# sets `nix.settings.builders = null` and the list is dropped from
|
|
||||||
# /etc/nix/nix.conf entirely, even though `nix.buildMachines` is
|
|
||||||
# populated. (The build-machine list still lands in /etc/nix/machines
|
|
||||||
# but nix-daemon reads `builders`, not /etc/nix/machines, when
|
|
||||||
# distributedBuilds is false.)
|
|
||||||
nix.distributedBuilds = true;
|
|
||||||
nix.buildMachines = map forNix config.host.builder.clients;
|
|
||||||
# Nix 2.34's daemon does not act on `@/etc/nix/machines` (the file
|
|
||||||
# format NixOS's nix-remote-build writes to): the `builders` config
|
|
||||||
# key is parsed for display but `external-builders` stays empty and
|
|
||||||
# the scheduler ignores it. Inlining the same builder text here — in
|
|
||||||
# the exact format the NixOS module itself uses — actually wires up
|
|
||||||
# the SSH dispatch. `mkForce` is required because the nix-remote-build
|
|
||||||
# module sets `builders = null` whenever distributedBuilds is *false*;
|
|
||||||
# our config flips it to *true*, so the module's mkIf does not fire
|
|
||||||
# and there is no actual conflict — but pinning it with mkForce makes
|
|
||||||
# the intent obvious and survives any future change in default
|
|
||||||
# behaviour.
|
|
||||||
nix.settings.builders = lib.mkForce inlineBuilders;
|
|
||||||
|
|
||||||
# Append per-builder Host blocks to the system-wide OpenSSH client
|
|
||||||
# config. `programs.ssh.extraConfig` is of type `lines`, merged across
|
|
||||||
# modules, and prepended (before `Host *`) in /etc/ssh/ssh_config —
|
|
||||||
# which is exactly the spot where specific Host blocks have to live.
|
|
||||||
programs.ssh.extraConfig = lib.concatStrings blocks;
|
|
||||||
|
|
||||||
# Parallel builds on sapphira itself stay at 2 — that matches the
|
|
||||||
# physical cores and keeps the coordinator responsive while the WSL
|
|
||||||
# absorbs the heavy lifting. The essentials/settings.nix already
|
|
||||||
# leaves max-jobs at the default `auto` (2 here); no override needed.
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -6,21 +6,18 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# stable = import inputs.nixpkgs-previous {
|
stable = import inputs.nixpkgs-previous {
|
||||||
# system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
# };
|
};
|
||||||
libraryDir = "${xlib.dirs.services-mnt-folder}/calibre-web-library";
|
|
||||||
sourceDir = "${xlib.dirs.services-mnt-folder}/calibre-web";
|
|
||||||
targetDir = "/var/lib/calibre-web";
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services = {
|
services = {
|
||||||
calibre-web = {
|
calibre-web = {
|
||||||
# package = stable.calibre-web;
|
package = stable.calibre-web;
|
||||||
enable = true;
|
enable = true;
|
||||||
# dataDir = "${xlib.dirs.services-mnt-folder}/calibre-web";
|
# dataDir = "${xlib.dirs.services-mnt-folder}/calibre-web";
|
||||||
options = {
|
options = {
|
||||||
calibreLibrary = "${libraryDir}";
|
calibreLibrary = "${xlib.dirs.services-mnt-folder}/calibre-web-library";
|
||||||
enableBookUploading = true;
|
enableBookUploading = true;
|
||||||
enableKepubify = true;
|
enableKepubify = true;
|
||||||
enableBookConversion = false;
|
enableBookConversion = false;
|
||||||
@@ -42,30 +39,19 @@ in
|
|||||||
# };
|
# };
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules =
|
systemd.tmpfiles.rules = [
|
||||||
xlib.helpers.mkTmpDirs {
|
"d ${xlib.dirs.services-mnt-folder}/calibre-web 0755 calibre-web calibre-web -"
|
||||||
dir = libraryDir;
|
"d ${xlib.dirs.services-mnt-folder}/calibre-web-library 0755 calibre-web calibre-web -"
|
||||||
mode = "0755";
|
];
|
||||||
user = "calibre-web";
|
|
||||||
group = "calibre-web";
|
fileSystems = {
|
||||||
types = [
|
"/var/lib/calibre-web" = {
|
||||||
"d"
|
device = "${xlib.dirs.services-mnt-folder}/calibre-web";
|
||||||
"Z"
|
fsType = "none";
|
||||||
];
|
options = [
|
||||||
}
|
"bind"
|
||||||
++ xlib.helpers.mkTmpDirs {
|
"nofail"
|
||||||
dir = sourceDir;
|
|
||||||
mode = "0755";
|
|
||||||
user = "calibre-web";
|
|
||||||
group = "calibre-web";
|
|
||||||
types = [
|
|
||||||
"d"
|
|
||||||
"Z"
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems = xlib.helpers.mkBindMount {
|
|
||||||
what = sourceDir;
|
|
||||||
where = targetDir;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,11 @@
|
|||||||
192.168.1.20 flux.zeroq.su
|
192.168.1.20 flux.zeroq.su
|
||||||
192.168.1.20 git.zeroq.su
|
192.168.1.20 git.zeroq.su
|
||||||
192.168.1.20 glances.zeroq.su
|
192.168.1.20 glances.zeroq.su
|
||||||
192.168.1.20 homebox.zeroq.su
|
192.168.1.20 homebox.home.arpa
|
||||||
192.168.1.20 immich.zeroq.su
|
192.168.1.20 immich.zeroq.su
|
||||||
192.168.1.20 kuma.zeroq.su
|
192.168.1.20 kuma.zeroq.su
|
||||||
192.168.1.20 navidrome.zeroq.su
|
192.168.1.20 navidrome.zeroq.su
|
||||||
192.168.1.20 nextcloud.zeroq.su
|
192.168.1.20 nextcloud.zeroq.su
|
||||||
192.168.1.20 open.zeroq.su
|
|
||||||
192.168.1.20 office.zeroq.su
|
192.168.1.20 office.zeroq.su
|
||||||
192.168.1.20 pdf.zeroq.su
|
192.168.1.20 pdf.zeroq.su
|
||||||
192.168.1.20 syncthing.zeroq.su
|
192.168.1.20 syncthing.zeroq.su
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
xlib,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../containers/3x-ui.nix
|
../software/beets
|
||||||
../containers/open-webui.nix
|
|
||||||
../containers/tape-rotation.nix
|
|
||||||
../pkgs/beets.nix
|
|
||||||
./acme.nix
|
./acme.nix
|
||||||
./bentopdf.nix
|
./bentopdf.nix
|
||||||
./builder.nix
|
|
||||||
./calibre-web.nix
|
./calibre-web.nix
|
||||||
./chrony.nix
|
./chrony.nix
|
||||||
./coredns.nix
|
./coredns.nix
|
||||||
@@ -28,32 +23,23 @@
|
|||||||
./postgresql.nix
|
./postgresql.nix
|
||||||
./power.nix
|
./power.nix
|
||||||
./samba.nix
|
./samba.nix
|
||||||
|
./step-ca.nix
|
||||||
./syncthing.nix
|
./syncthing.nix
|
||||||
./systemd.nix
|
./systemd.nix
|
||||||
./uptime-kuma.nix
|
|
||||||
# ../containers/remnawave.nix
|
# ../containers/remnawave.nix
|
||||||
# ./coturn.nix
|
# ./coturn.nix
|
||||||
# ./mealie.nix
|
# ./mealie.nix
|
||||||
# ./memos.nix
|
# ./memos.nix
|
||||||
# ./minecraft.nix
|
|
||||||
# ./n8n.nix
|
# ./n8n.nix
|
||||||
# ./netdata.nix
|
# ./netdata.nix
|
||||||
# ./nfs.nix
|
# ./nfs.nix
|
||||||
|
# ./node-red.nix
|
||||||
|
# ./open-webui.nix
|
||||||
# ./rsync.nix
|
# ./rsync.nix
|
||||||
# ./step-ca.nix
|
|
||||||
# ./stirling-pdf.nix
|
# ./stirling-pdf.nix
|
||||||
# ./transmission.nix
|
# ./transmission.nix
|
||||||
# ./trilium.nix
|
# ./trilium.nix
|
||||||
|
# ./uptime-kuma.nix
|
||||||
# ./zerotier.nix
|
# ./zerotier.nix
|
||||||
];
|
];
|
||||||
# Server's 3x-ui is the controller panel at x.zeroq.su (nginx HTTP
|
|
||||||
# terminates TLS upstream, no SNI-routing on 443 needed here because
|
|
||||||
# there are other vhosts on the same port). Cert is still mounted in
|
|
||||||
# case 3x-ui is later reconfigured to terminate TLS itself (e.g. for
|
|
||||||
# direct node-API access); nginx doesn't have to use it.
|
|
||||||
host."3x-ui".certDomain = "x.zeroq.su";
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
(xlib.helpers.mkTmpfile "d" "/mnt" "0755" "root" "root")
|
|
||||||
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root")
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
appName = "ZeroQ Gitea Service";
|
appName = "ZeroQ Gitea Service";
|
||||||
settings = {
|
settings = {
|
||||||
server = {
|
server = {
|
||||||
DOMAIN = "git.zeroq.su";
|
DOMAIN = "git.home.arpa";
|
||||||
HTTP_PORT = 3000;
|
HTTP_PORT = 3000;
|
||||||
};
|
};
|
||||||
service.DISABLE_REGISTRATION = true;
|
service.DISABLE_REGISTRATION = true;
|
||||||
@@ -22,10 +22,8 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = xlib.helpers.mkTmpDirs {
|
systemd.tmpfiles.rules = [
|
||||||
dir = config.services.gitea.stateDir;
|
"d ${config.services.gitea.stateDir} 0755 gitea gitea -"
|
||||||
mode = "0755";
|
"z ${config.services.gitea.stateDir} 0755 gitea gitea -"
|
||||||
user = "gitea";
|
];
|
||||||
group = "gitea";
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-10
@@ -5,11 +5,8 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
storage = xlib.helpers.mkServiceStorage {
|
configDir = "${xlib.dirs.services-mnt-folder}/homebox";
|
||||||
name = "homebox";
|
varDir = "/var/lib/homebox";
|
||||||
user = "homebox";
|
|
||||||
group = "homebox";
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.homebox = {
|
services.homebox = {
|
||||||
@@ -17,17 +14,31 @@ in
|
|||||||
settings = {
|
settings = {
|
||||||
HBOX_WEB_HOST = "0.0.0.0";
|
HBOX_WEB_HOST = "0.0.0.0";
|
||||||
HBOX_WEB_PORT = "7745";
|
HBOX_WEB_PORT = "7745";
|
||||||
HBOX_STORAGE_CONN_STRING = "file://${storage.target}";
|
HBOX_STORAGE_CONN_STRING = "file://${varDir}";
|
||||||
HBOX_STORAGE_PREFIX_PATH = "data";
|
HBOX_STORAGE_PREFIX_PATH = "data";
|
||||||
HBOX_DATABASE_DRIVER = "sqlite3";
|
HBOX_DATABASE_DRIVER = "sqlite3";
|
||||||
HBOX_DATABASE_SQLITE_PATH = "${storage.target}/data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1";
|
HBOX_DATABASE_SQLITE_PATH = "${varDir}/data/homebox.db?_pragma=busy_timeout=999&_pragma=journal_mode=WAL&_fk=1";
|
||||||
HBOX_OPTIONS_ALLOW_REGISTRATION = "true";
|
HBOX_OPTIONS_ALLOW_REGISTRATION = "true";
|
||||||
HBOX_OPTIONS_GITHUB_RELEASE_CHECK = "false";
|
HBOX_OPTIONS_GITHUB_RELEASE_CHECK = "false";
|
||||||
HBOX_MODE = "production";
|
HBOX_MODE = "production";
|
||||||
HOME = "${storage.target}";
|
HOME = "${varDir}";
|
||||||
TMPDIR = "${storage.target}/tmp";
|
TMPDIR = "${varDir}/tmp";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd = storage.systemd;
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${configDir} 0755 homebox homebox -"
|
||||||
|
"z ${configDir} 0755 homebox homebox -"
|
||||||
|
];
|
||||||
|
|
||||||
|
fileSystems = {
|
||||||
|
"${varDir}" = {
|
||||||
|
device = "${configDir}";
|
||||||
|
fsType = "none";
|
||||||
|
options = [
|
||||||
|
"bind"
|
||||||
|
"nofail"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,16 @@
|
|||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
new = import inputs.nixpkgs-unstable {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
};
|
||||||
|
in
|
||||||
{
|
{
|
||||||
services = {
|
services = {
|
||||||
immich = {
|
immich = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
# package = new.immich;
|
||||||
port = 2283;
|
port = 2283;
|
||||||
host = "0.0.0.0";
|
host = "0.0.0.0";
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
@@ -20,7 +26,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
(xlib.helpers.mkTmpfile "z" config.services.immich.mediaLocation "0755" "immich" "immich")
|
"z ${config.services.immich.mediaLocation} 0755 immich immich -"
|
||||||
];
|
];
|
||||||
|
|
||||||
users.users.immich.extraGroups = [
|
users.users.immich.extraGroups = [
|
||||||
|
|||||||
@@ -21,6 +21,6 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
(xlib.helpers.mkTmpfile "z" "${xlib.dirs.services-mnt-folder}/memos" "0750" "memos" "memos")
|
"z /mnt/services/memos 0750 memos memos -"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
{
|
|
||||||
config,
|
|
||||||
inputs,
|
|
||||||
pkgs,
|
|
||||||
xlib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
storage = xlib.helpers.mkServiceStorage {
|
|
||||||
name = "minecraft";
|
|
||||||
user = "minecraft";
|
|
||||||
group = "minecraft";
|
|
||||||
mode = "770";
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports = [ inputs.nix-minecraft.nixosModules.minecraft-servers ];
|
|
||||||
nixpkgs.overlays = [ inputs.nix-minecraft.overlay ];
|
|
||||||
services.minecraft-servers = {
|
|
||||||
enable = true;
|
|
||||||
eula = true;
|
|
||||||
openFirewall = true;
|
|
||||||
dataDir = "/var/lib/minecraft";
|
|
||||||
servers = {
|
|
||||||
vanilla = {
|
|
||||||
enable = true;
|
|
||||||
package = pkgs.fabricServers.fabric-26_2.override {
|
|
||||||
jre_headless = pkgs.jdk25_headless;
|
|
||||||
};
|
|
||||||
jvmOpts = "-Xmx2G -Xms1G";
|
|
||||||
enableReload = true;
|
|
||||||
serverProperties = {
|
|
||||||
view-distance = 6;
|
|
||||||
simulation-distance = 4;
|
|
||||||
online-mode = false;
|
|
||||||
difficulty = 3;
|
|
||||||
gamemode = 1;
|
|
||||||
max-players = 5;
|
|
||||||
server-port = 25565;
|
|
||||||
motd = "ZeroQ сервак майна епта!";
|
|
||||||
enable-rcon = true;
|
|
||||||
"rcon.password" = "zeroq";
|
|
||||||
};
|
|
||||||
symlinks.mods = pkgs.linkFarmFromDrvs "mods" (
|
|
||||||
builtins.attrValues {
|
|
||||||
Lithium = pkgs.fetchurl {
|
|
||||||
name = "lithium-fabric-0.25.3+mc26.2.jar";
|
|
||||||
url = "https://cdn.modrinth.com/data/gvQqBUqZ/versions/f7vZ0VWU/lithium-fabric-0.25.3%2Bmc26.2.jar";
|
|
||||||
hash = "sha256-/d6S4jjoB1+JrX9wHyo9WFSviLqaZ2VxhKRAexBKxWM=";
|
|
||||||
};
|
|
||||||
FerriteCore = pkgs.fetchurl {
|
|
||||||
name = "ferritecore-9.0.0-fabric.jar";
|
|
||||||
url = "https://cdn.modrinth.com/data/uXXizFIs/versions/d5ddUdiB/ferritecore-9.0.0-fabric.jar";
|
|
||||||
hash = "sha256-ITlmxy7ZZ6zHOSvrKKhm+6MB/1a5l2wueAHC233mvyI=";
|
|
||||||
};
|
|
||||||
Krypton = pkgs.fetchurl {
|
|
||||||
name = "krypton-0.3.1.jar";
|
|
||||||
url = "https://cdn.modrinth.com/data/fQEb0iXm/versions/5WeL0Nkz/krypton-0.3.1.jar";
|
|
||||||
hash = "sha256-XqiQFWGXPSnlHnUUadUtkhAPNIq0YeEYb2cBLpNCDEg=";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
systemd = storage.systemd;
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
CLEANUP_FREQUENCY = 48;
|
CLEANUP_FREQUENCY = 48;
|
||||||
LISTEN_ADDR = "0.0.0.0:6061";
|
LISTEN_ADDR = "0.0.0.0:6061";
|
||||||
};
|
};
|
||||||
|
# adminCredentialsFile = "${inputs.zeroq-credentials}/services/miniflux/admin-pass.txt";
|
||||||
adminCredentialsFile = config.sops.secrets.minifluxenv.path;
|
adminCredentialsFile = config.sops.secrets.minifluxenv.path;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+16
-7
@@ -7,22 +7,31 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
storage = xlib.helpers.mkServiceStorage {
|
configDir = "${xlib.dirs.services-mnt-folder}/n8n";
|
||||||
name = "n8n";
|
varDir = "/var/lib/n8n";
|
||||||
user = "nobody";
|
|
||||||
group = "nogroup";
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.n8n = {
|
services.n8n = {
|
||||||
enable = false;
|
enable = false;
|
||||||
environment = {
|
environment = {
|
||||||
# N8N_USER_FOLDER = lib.mkForce "${sourceDir}";
|
# N8N_USER_FOLDER = lib.mkForce "${configDir}";
|
||||||
N8N_SECURE_COOKIE = "false";
|
N8N_SECURE_COOKIE = "false";
|
||||||
N8N_PORT = 5678;
|
N8N_PORT = 5678;
|
||||||
};
|
};
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd = storage.systemd;
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${configDir} 0755 nobody nogroup -"
|
||||||
|
"z ${configDir} 0755 nobody nogroup -"
|
||||||
|
];
|
||||||
|
|
||||||
|
fileSystems.${varDir} = {
|
||||||
|
device = "${configDir}";
|
||||||
|
fsType = "none";
|
||||||
|
options = [
|
||||||
|
"bind"
|
||||||
|
"nofail"
|
||||||
|
];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,6 @@
|
|||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
libraryDir = "${xlib.dirs.server-home}/Music";
|
|
||||||
pointDir = "/var/lib/services/navidrome-point";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
services = {
|
services = {
|
||||||
navidrome = {
|
navidrome = {
|
||||||
@@ -19,14 +15,8 @@ in
|
|||||||
settings = {
|
settings = {
|
||||||
Address = "0.0.0.0";
|
Address = "0.0.0.0";
|
||||||
Port = 4533;
|
Port = 4533;
|
||||||
MusicFolder = "${pointDir}";
|
MusicFolder = "/mnt/beets/music";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
systemd.mounts = [
|
|
||||||
(xlib.helpers.mkSystemdBind {
|
|
||||||
what = libraryDir;
|
|
||||||
where = pointDir;
|
|
||||||
})
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,19 @@
|
|||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
previous = import inputs.nixpkgs-master {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
config.allowUnfree = true;
|
||||||
|
config.allowUnfreePredicate = true;
|
||||||
|
};
|
||||||
|
in
|
||||||
{
|
{
|
||||||
services = {
|
services = {
|
||||||
nextcloud-whiteboard-server = {
|
nextcloud-whiteboard-server = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings = {
|
settings = {
|
||||||
NEXTCLOUD_URL = "https://nextcloud.zeroq.su";
|
NEXTCLOUD_URL = "https://nextcloud.home.arpa";
|
||||||
};
|
};
|
||||||
secrets = [ config.sops.secrets.nextcloud-whiteboard-jwt.path ];
|
secrets = [ config.sops.secrets.nextcloud-whiteboard-jwt.path ];
|
||||||
};
|
};
|
||||||
@@ -20,8 +27,8 @@
|
|||||||
hostName = "talk.private";
|
hostName = "talk.private";
|
||||||
backends.nextcloud = {
|
backends.nextcloud = {
|
||||||
urls = [
|
urls = [
|
||||||
|
"https://nextcloud.home.arpa"
|
||||||
"https://nextcloud.zeroq.su"
|
"https://nextcloud.zeroq.su"
|
||||||
# "https://nextcloud.home.arpa"
|
|
||||||
];
|
];
|
||||||
secretFile = config.sops.secrets.nextcloud-talk-secret.path;
|
secretFile = config.sops.secrets.nextcloud-talk-secret.path;
|
||||||
};
|
};
|
||||||
@@ -40,16 +47,16 @@
|
|||||||
secretFile = config.sops.secrets.turn-secret.path;
|
secretFile = config.sops.secrets.turn-secret.path;
|
||||||
apikeyFile = config.sops.secrets.turn-api-key.path;
|
apikeyFile = config.sops.secrets.turn-api-key.path;
|
||||||
servers = [
|
servers = [
|
||||||
"turn:turn.zeroq.su:3478?transport=udp"
|
"turn:turn.home.arpa:3478?transport=udp"
|
||||||
"turn:turn.zeroq.su:3478?transport=tcp"
|
"turn:turn.home.arpa:3478?transport=tcp"
|
||||||
# "turns:turn.zeroq.su:5349?transport=tcp"
|
# "turns:turn.home.arpa:5349?transport=tcp"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
nextcloud = {
|
nextcloud = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.nextcloud35;
|
package = pkgs.nextcloud34;
|
||||||
hostName = "nextcloud.private";
|
hostName = "nextcloud.private";
|
||||||
database.createLocally = true;
|
database.createLocally = true;
|
||||||
home = "${xlib.dirs.services-mnt-folder}/nextcloud";
|
home = "${xlib.dirs.services-mnt-folder}/nextcloud";
|
||||||
@@ -64,7 +71,7 @@
|
|||||||
dbtype = "pgsql";
|
dbtype = "pgsql";
|
||||||
dbuser = "nextcloud";
|
dbuser = "nextcloud";
|
||||||
dbname = "nextcloud";
|
dbname = "nextcloud";
|
||||||
adminuser = xlib.device.username;
|
adminuser = "oqyude";
|
||||||
adminpassFile = config.sops.secrets.nextcloud-adminpass.path;
|
adminpassFile = config.sops.secrets.nextcloud-adminpass.path;
|
||||||
};
|
};
|
||||||
settings = {
|
settings = {
|
||||||
@@ -72,19 +79,17 @@
|
|||||||
trusted_domains = [
|
trusted_domains = [
|
||||||
"100.64.0.0"
|
"100.64.0.0"
|
||||||
"192.168.1.20"
|
"192.168.1.20"
|
||||||
"37.128.246.126"
|
|
||||||
"localhost"
|
"localhost"
|
||||||
"nextcloud.home.arpa"
|
|
||||||
"nextcloud.private"
|
"nextcloud.private"
|
||||||
"nextcloud.zeroq.su"
|
"nextcloud.zeroq.su"
|
||||||
"office.home.arpa"
|
|
||||||
"office.zeroq.su"
|
"office.zeroq.su"
|
||||||
|
"office.home.arpa"
|
||||||
|
"nextcloud.home.arpa"
|
||||||
];
|
];
|
||||||
trusted_proxies = [
|
trusted_proxies = [
|
||||||
"100.64.1.0"
|
"100.64.1.0"
|
||||||
"109.248.161.5"
|
|
||||||
"192.168.1.20"
|
"192.168.1.20"
|
||||||
"37.128.246.126"
|
"109.248.161.5"
|
||||||
];
|
];
|
||||||
overwriteprotocol = "https"; # maybe no
|
overwriteprotocol = "https"; # maybe no
|
||||||
};
|
};
|
||||||
@@ -93,7 +98,7 @@
|
|||||||
notify_push = {
|
notify_push = {
|
||||||
enable = true;
|
enable = true;
|
||||||
bendDomainToLocalhost = true;
|
bendDomainToLocalhost = true;
|
||||||
nextcloudUrl = "https://nextcloud.zeroq.su";
|
nextcloudUrl = "https://nextcloud.home.arpa";
|
||||||
};
|
};
|
||||||
# phpPackage = pkgs.php85;
|
# phpPackage = pkgs.php85;
|
||||||
extraApps = {
|
extraApps = {
|
||||||
@@ -123,9 +128,9 @@
|
|||||||
mail
|
mail
|
||||||
music
|
music
|
||||||
notes
|
notes
|
||||||
# onlyoffice
|
onlyoffice
|
||||||
polls
|
polls
|
||||||
# previewgenerator
|
previewgenerator
|
||||||
spreed
|
spreed
|
||||||
tables
|
tables
|
||||||
tasks
|
tasks
|
||||||
@@ -201,7 +206,7 @@
|
|||||||
# };
|
# };
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
(xlib.helpers.mkTmpfile "z" config.services.nextcloud.home "0750" "nextcloud" "nextcloud")
|
"z ${config.services.nextcloud.home} 0750 nextcloud nextcloud -"
|
||||||
];
|
];
|
||||||
|
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
(xlib.helpers.mkTmpfile "z" "/export" "0755" "nobody" "nogroup")
|
"z /export 0755 nobody nogroup -"
|
||||||
];
|
];
|
||||||
services.nfs = {
|
services.nfs = {
|
||||||
server = {
|
server = {
|
||||||
|
|||||||
+245
-227
@@ -5,250 +5,268 @@
|
|||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# Standard reverse-proxy: HTTP/S termination upstream, backend on the LAN.
|
|
||||||
# x.zeroq.su is the 3x-ui controller panel — see 3x-ui.nix for the
|
|
||||||
# /subs/, /subsjs/, /clash/ routing logic.
|
|
||||||
let
|
let
|
||||||
server = "192.168.1.20";
|
server = "192.168.1.20";
|
||||||
|
|
||||||
mkProxy =
|
|
||||||
{
|
|
||||||
domain,
|
|
||||||
port,
|
|
||||||
addSSL ? false,
|
|
||||||
extraConfig ? "",
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
name = domain;
|
|
||||||
value = {
|
|
||||||
enableACME = true;
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://${server}:${toString port}";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (!addSSL) { forceSSL = true; }
|
|
||||||
// lib.optionalAttrs addSSL { addSSL = true; }
|
|
||||||
// lib.optionalAttrs (extraConfig != "") { inherit extraConfig; };
|
|
||||||
};
|
|
||||||
|
|
||||||
bigUploads = "client_max_body_size 5G;";
|
|
||||||
|
|
||||||
sites = [
|
|
||||||
{
|
|
||||||
domain = "immich.zeroq.su";
|
|
||||||
port = 2283;
|
|
||||||
addSSL = true;
|
|
||||||
extraConfig = bigUploads;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "kuma.zeroq.su";
|
|
||||||
port = 4001;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "health.zeroq.su";
|
|
||||||
port = 19999;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "git.zeroq.su";
|
|
||||||
port = 3000;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "homebox.zeroq.su";
|
|
||||||
port = 7745;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "flux.zeroq.su";
|
|
||||||
port = 6061;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "tape-rotation.zeroq.su";
|
|
||||||
port = 5174;
|
|
||||||
}
|
|
||||||
# NOTE: open.zeroq.su is intentionally NOT in this `sites` list —
|
|
||||||
# mkProxy hard-codes ${server} = 192.168.1.20, but the Open WebUI
|
|
||||||
# container binds to 127.0.0.1:8080 only (loopback, see
|
|
||||||
# modules/containers/open-webui.nix). The vhost is added directly
|
|
||||||
# to `virtualHosts` below, alongside x.zeroq.su (3x-ui panel,
|
|
||||||
# same loopback-only pattern).
|
|
||||||
{
|
|
||||||
domain = "navidrome.zeroq.su";
|
|
||||||
port = 4533;
|
|
||||||
addSSL = true;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "calibre.zeroq.su";
|
|
||||||
port = 8083;
|
|
||||||
extraConfig = bigUploads;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "nix-cache.zeroq.su";
|
|
||||||
port = 5000;
|
|
||||||
extraConfig = bigUploads;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "pdf.zeroq.su";
|
|
||||||
port = 8446;
|
|
||||||
extraConfig = bigUploads;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.nginx = {
|
services = {
|
||||||
enable = true;
|
nginx = {
|
||||||
recommendedGzipSettings = true;
|
enable = true;
|
||||||
recommendedOptimisation = true;
|
recommendedGzipSettings = true;
|
||||||
recommendedProxySettings = true;
|
recommendedOptimisation = true;
|
||||||
recommendedTlsSettings = true;
|
recommendedProxySettings = true;
|
||||||
virtualHosts = (builtins.listToAttrs (map mkProxy sites)) // {
|
recommendedTlsSettings = true;
|
||||||
"nextcloud.private" = {
|
virtualHosts = {
|
||||||
forceSSL = false;
|
"nextcloud.private" = {
|
||||||
enableACME = false;
|
forceSSL = false;
|
||||||
listen = [
|
enableACME = false;
|
||||||
{
|
listen = [
|
||||||
addr = "100.64.0.0";
|
{
|
||||||
port = 10000;
|
addr = "100.64.0.0";
|
||||||
}
|
port = 10000;
|
||||||
{
|
}
|
||||||
addr = "192.168.1.20";
|
{
|
||||||
port = 10000;
|
addr = "192.168.1.20";
|
||||||
}
|
port = 10000;
|
||||||
{
|
}
|
||||||
addr = "127.0.0.1";
|
{
|
||||||
port = 10000;
|
addr = "127.0.0.1";
|
||||||
}
|
port = 10000;
|
||||||
];
|
}
|
||||||
};
|
];
|
||||||
"office.zeroq.su" = {
|
};
|
||||||
forceSSL = true;
|
"office.home.arpa" = {
|
||||||
enableACME = true;
|
forceSSL = true;
|
||||||
};
|
enableACME = true;
|
||||||
"pdf.private" = {
|
};
|
||||||
forceSSL = false;
|
"pdf.private" = {
|
||||||
enableACME = false;
|
forceSSL = false;
|
||||||
listen = [
|
enableACME = false;
|
||||||
{
|
listen = [
|
||||||
addr = "0.0.0.0";
|
{
|
||||||
port = 80;
|
addr = "0.0.0.0";
|
||||||
}
|
port = 80;
|
||||||
{
|
}
|
||||||
addr = "100.64.0.0";
|
{
|
||||||
port = 8446;
|
addr = "100.64.0.0";
|
||||||
}
|
port = 8446;
|
||||||
{
|
}
|
||||||
addr = "192.168.1.20";
|
{
|
||||||
port = 8446;
|
addr = "192.168.1.20";
|
||||||
}
|
port = 8446;
|
||||||
{
|
}
|
||||||
addr = "127.0.0.1";
|
{
|
||||||
port = 8446;
|
addr = "127.0.0.1";
|
||||||
}
|
port = 8446;
|
||||||
];
|
}
|
||||||
extraConfig = bigUploads;
|
];
|
||||||
};
|
extraConfig = ''
|
||||||
"x.zeroq.su" = {
|
client_max_body_size 5G;
|
||||||
forceSSL = true;
|
'';
|
||||||
enableACME = true;
|
};
|
||||||
locations = {
|
"pdf.home.arpa" = {
|
||||||
"/" = {
|
forceSSL = true;
|
||||||
proxyPass = "http://127.0.0.1:2049";
|
enableACME = true;
|
||||||
|
locations = {
|
||||||
|
"/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:8446";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
extraConfig = ''
|
||||||
|
client_max_body_size 5G;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"homebox.home.arpa" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations = {
|
||||||
|
"/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:7745";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"nextcloud.home.arpa" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations = {
|
||||||
|
"/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:10000";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
"/whiteboard" = {
|
||||||
|
proxyPass = "http://127.0.0.1:3002";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
extraConfig = ''
|
||||||
|
client_max_body_size 5G;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
# "talk.home.arpa" = {
|
||||||
|
# forceSSL = true;
|
||||||
|
# enableACME = true;
|
||||||
|
# # locations = {
|
||||||
|
# # "/" = {
|
||||||
|
# # proxyPass = "http://127.0.0.1:7880";
|
||||||
|
# # proxyWebsockets = true;
|
||||||
|
# # };
|
||||||
|
# # };
|
||||||
|
# extraConfig = ''
|
||||||
|
# client_max_body_size 5G;
|
||||||
|
# '';
|
||||||
|
# };
|
||||||
|
# "turn.home.arpa" = {
|
||||||
|
# forceSSL = true;
|
||||||
|
# enableACME = true;
|
||||||
|
# locations = {
|
||||||
|
# "/" = {
|
||||||
|
# proxyPass = "http://127.0.0.1:5349";
|
||||||
|
# proxyWebsockets = true;
|
||||||
|
# };
|
||||||
|
# };
|
||||||
|
# extraConfig = ''
|
||||||
|
# client_max_body_size 5G;
|
||||||
|
# '';
|
||||||
|
# };
|
||||||
|
"ca.home.arpa" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:9000";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
"/subs/" = {
|
extraConfig = ''
|
||||||
proxyPass = "http://127.0.0.1:2096";
|
client_max_body_size 5G;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"git.home.arpa" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:3000";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
"/subsjs/" = {
|
extraConfig = ''
|
||||||
proxyPass = "http://127.0.0.1:2096";
|
client_max_body_size 5G;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"git.zeroq.su" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:3000";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
"/clash/" = {
|
extraConfig = ''
|
||||||
proxyPass = "http://127.0.0.1:2096";
|
client_max_body_size 5G;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
# "n8n.home.arpa" = {
|
||||||
|
# forceSSL = true;
|
||||||
|
# enableACME = true;
|
||||||
|
# locations."/" = {
|
||||||
|
# proxyPass = "http://${server}:5678";
|
||||||
|
# proxyWebsockets = true;
|
||||||
|
# };
|
||||||
|
# extraConfig = ''
|
||||||
|
# client_max_body_size 5G;
|
||||||
|
# '';
|
||||||
|
# };
|
||||||
|
"kuma.home.arpa" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:4001";
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
"flux.home.arpa" = {
|
||||||
# Open WebUI — same loopback-only pattern as x.zeroq.su above.
|
addSSL = true;
|
||||||
# The container listens on 127.0.0.1:8080 (modules/containers/open-webui.nix),
|
enableACME = true;
|
||||||
# so we proxy_pass to 127.0.0.1, not the LAN IP. The two extra
|
locations."/" = {
|
||||||
# directives are required by the upstream HTTPS docs:
|
proxyPass = "http://127.0.0.1:6061";
|
||||||
# proxy_buffering off for SSE streaming (markdown in chat breaks
|
|
||||||
# under the default `proxy_buffering on` from recommendedProxySettings),
|
|
||||||
# and a 300 s read timeout for long LLM completions.
|
|
||||||
"open.zeroq.su" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://127.0.0.1:8080";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
};
|
|
||||||
extraConfig = ''
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
"zeroq.su" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
root = pkgs.writeTextDir "index.html" ''
|
|
||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<pre>What are you doing here?</pre>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
'';
|
|
||||||
locations."/guest/" = {
|
|
||||||
proxyPass = "http://${server}:80";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
"vetymae.opencodes.zeroq.su" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://100.86.62.4:4096";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
"lamet.opencodes.zeroq.su" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://100.106.21.39:6061";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
# sapphira itself: opencode web runs as a systemd user service
|
|
||||||
# (programs.opencode.web.enable in home/modules/opencode.nix) on
|
|
||||||
# 127.0.0.1:4096 with --hostname 0.0.0.0.
|
|
||||||
"opencode.zeroq.su" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://127.0.0.1:4096";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
"nextcloud.zeroq.su" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations = {
|
|
||||||
"/" = {
|
|
||||||
proxyPass = "http://${server}:10000";
|
|
||||||
proxyWebsockets = true;
|
|
||||||
};
|
|
||||||
"/whiteboard" = {
|
|
||||||
proxyPass = "http://${server}:3002";
|
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
extraConfig = bigUploads;
|
"navidrome.home.arpa" = {
|
||||||
|
addSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:4533";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"immich.home.arpa" = {
|
||||||
|
addSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:2283";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
extraConfig = ''
|
||||||
|
client_max_body_size 5G;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"calibre.home.arpa" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:8083";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
extraConfig = ''
|
||||||
|
client_max_body_size 5G;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"dns.home.arpa" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:53";
|
||||||
|
};
|
||||||
|
extraConfig = ''
|
||||||
|
client_max_body_size 5G;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"glances.home.arpa" = {
|
||||||
|
forceSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:61208";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"syncthing.home.arpa" = {
|
||||||
|
addSSL = true;
|
||||||
|
enableACME = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:8384";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# "zeroq.home.arpa" = {
|
||||||
|
# forceSSL = true;
|
||||||
|
# enableACME = true;
|
||||||
|
# root = pkgs.writeTextDir "index.html" ''
|
||||||
|
# <!doctype html>
|
||||||
|
# <html>
|
||||||
|
# <body>
|
||||||
|
# <pre>This server is running in backend.</pre>
|
||||||
|
# </body>
|
||||||
|
# </html>
|
||||||
|
# '';
|
||||||
|
# listen = [
|
||||||
|
# {
|
||||||
|
# addr = "100.64.0.0";
|
||||||
|
# port = 80;
|
||||||
|
# }
|
||||||
|
# {
|
||||||
|
# addr = "192.168.1.20";
|
||||||
|
# port = 80;
|
||||||
|
# }
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
networking.firewall.allowedTCPPorts = [
|
|
||||||
80
|
|
||||||
443
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
xlib,
|
||||||
|
inputs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
services.node-red = {
|
||||||
|
enable = false;
|
||||||
|
port = 1880;
|
||||||
|
openFirewall = true;
|
||||||
|
userDir = "${xlib.dirs.services-mnt-folder}/node-red";
|
||||||
|
configFile = "${inputs.zeroq-credentials}/configs/node-red/settings.js";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"z ${config.services.node-red.userDir} 0750 node-red node-red -"
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -6,18 +6,18 @@
|
|||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
# let
|
let
|
||||||
# previous = import inputs.nixpkgs-master {
|
previous = import inputs.nixpkgs-master {
|
||||||
# system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
# config.allowUnfree = true;
|
config.allowUnfree = true;
|
||||||
# config.allowUnfreePredicate = true;
|
config.allowUnfreePredicate = true;
|
||||||
# };
|
};
|
||||||
# in
|
in
|
||||||
{
|
{
|
||||||
services.onlyoffice = {
|
services.onlyoffice = {
|
||||||
enable = true;
|
enable = true;
|
||||||
# package = previous.onlyoffice-documentserver;
|
# package = previous.onlyoffice-documentserver;
|
||||||
hostname = "office.zeroq.su";
|
hostname = "office.home.arpa";
|
||||||
port = 8090;
|
port = 8090;
|
||||||
allowLocalConnections = true;
|
allowLocalConnections = true;
|
||||||
wopi = true;
|
wopi = true;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
inputs,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
services = {
|
||||||
|
open-webui = {
|
||||||
|
enable = false;
|
||||||
|
host = "0.0.0.0";
|
||||||
|
port = 11112;
|
||||||
|
openFirewall = true;
|
||||||
|
environment = {
|
||||||
|
ANONYMIZED_TELEMETRY = "False";
|
||||||
|
DO_NOT_TRACK = "True";
|
||||||
|
SCARF_NO_ANALYTICS = "True";
|
||||||
|
OPENAI_API_BASE_URL = "http://192.168.1.100:1234/v1";
|
||||||
|
#OLLAMA_API_BASE_URL = "http://127.0.0.1:1234";
|
||||||
|
WEBUI_AUTH = "True";
|
||||||
|
ENABLE_SIGNUP = "False";
|
||||||
|
ENABLE_SIGNUP_PASSWORD_CONFIRMATION = "True";
|
||||||
|
ENABLE_VERSION_UPDATE_CHECK = "False";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,11 +7,8 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
storage = xlib.helpers.mkServiceStorage {
|
master = import inputs.nixpkgs-master {
|
||||||
name = "postgresql";
|
system = "x86_64-linux";
|
||||||
user = "postgres";
|
|
||||||
group = "postgres";
|
|
||||||
mode = "0760";
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -19,9 +16,22 @@ in
|
|||||||
postgresql = {
|
postgresql = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = pkgs.postgresql_17;
|
package = pkgs.postgresql_17;
|
||||||
|
# dataDir = "${xlib.dirs.services-mnt-folder}/postgresql";
|
||||||
};
|
};
|
||||||
# postgresqlBackup.enable = true;
|
# postgresqlBackup.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd = storage.systemd;
|
fileSystems."/var/lib/postgresql" = {
|
||||||
|
device = "${xlib.dirs.services-mnt-folder}/postgresql";
|
||||||
|
fsType = "none";
|
||||||
|
options = [
|
||||||
|
"bind"
|
||||||
|
"nofail"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"z ${xlib.dirs.services-mnt-folder}/postgresql 0760 postgres postgres -"
|
||||||
|
# "z ${config.services.postgresql.dataDir} 0760 postgres postgres -"
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,10 @@
|
|||||||
power-profiles-daemon.enable = lib.mkForce false;
|
power-profiles-daemon.enable = lib.mkForce false;
|
||||||
throttled.enable = false;
|
throttled.enable = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
systemPackages = with pkgs; [
|
||||||
|
cpupower-gui
|
||||||
|
];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,8 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
storage = xlib.helpers.mkServiceStorage {
|
stable = import inputs.nixpkgs-master {
|
||||||
name = "samba";
|
system = "x86_64-linux";
|
||||||
user = "root";
|
|
||||||
group = "root";
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
@@ -17,7 +15,7 @@ in
|
|||||||
samba-wsdd = {
|
samba-wsdd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
hostname = "sapphira";
|
hostname = "sapphira.home.arpa";
|
||||||
discovery = true;
|
discovery = true;
|
||||||
};
|
};
|
||||||
samba = {
|
samba = {
|
||||||
@@ -72,5 +70,19 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd = storage.systemd;
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${xlib.dirs.services-mnt-folder}/samba 0755 root root -"
|
||||||
|
"z ${xlib.dirs.services-mnt-folder}/samba 0755 root root -"
|
||||||
|
];
|
||||||
|
|
||||||
|
fileSystems = {
|
||||||
|
"/var/lib/samba" = {
|
||||||
|
device = "${xlib.dirs.services-mnt-folder}/samba";
|
||||||
|
fsType = "none";
|
||||||
|
options = [
|
||||||
|
"bind"
|
||||||
|
"nofail"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-17
@@ -6,8 +6,8 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
sourceDir = "${xlib.dirs.services-mnt-folder}/step-ca";
|
configDir = "${xlib.dirs.services-mnt-folder}/step-ca";
|
||||||
targetDir = "/var/lib/step-ca";
|
varDir = "/var/lib/step-ca";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.step-ca = {
|
services.step-ca = {
|
||||||
@@ -17,9 +17,9 @@ in
|
|||||||
openFirewall = true;
|
openFirewall = true;
|
||||||
intermediatePasswordFile = config.sops.secrets.intermediate-password.path;
|
intermediatePasswordFile = config.sops.secrets.intermediate-password.path;
|
||||||
settings = {
|
settings = {
|
||||||
root = "${targetDir}/certs/root_ca.crt";
|
root = "${varDir}/certs/root_ca.crt";
|
||||||
crt = "${targetDir}/certs/intermediate_ca.crt";
|
crt = "${varDir}/certs/intermediate_ca.crt";
|
||||||
key = "${targetDir}/secrets/intermediate_ca_key";
|
key = "${varDir}/secrets/intermediate_ca_key";
|
||||||
# address = "0.0.0.0:9000";
|
# address = "0.0.0.0:9000";
|
||||||
dnsNames = [
|
dnsNames = [
|
||||||
"*.zeroq.su"
|
"*.zeroq.su"
|
||||||
@@ -28,7 +28,7 @@ in
|
|||||||
];
|
];
|
||||||
db = {
|
db = {
|
||||||
type = "badgerv2";
|
type = "badgerv2";
|
||||||
dataSource = "${targetDir}/db";
|
dataSource = "${varDir}/db";
|
||||||
};
|
};
|
||||||
authority = {
|
authority = {
|
||||||
claims = {
|
claims = {
|
||||||
@@ -69,9 +69,13 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fileSystems = xlib.helpers.mkBindMount {
|
fileSystems."${varDir}" = {
|
||||||
what = sourceDir;
|
device = "${configDir}";
|
||||||
where = targetDir;
|
fsType = "none";
|
||||||
|
options = [
|
||||||
|
"bind"
|
||||||
|
"nofail"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
environment = {
|
environment = {
|
||||||
@@ -80,14 +84,11 @@ in
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules =
|
systemd.tmpfiles.rules = [
|
||||||
xlib.helpers.mkTmpDirs {
|
"d ${configDir} 0755 nobody nogroup -"
|
||||||
dir = sourceDir;
|
"z ${configDir} 0755 nobody nogroup -"
|
||||||
mode = "0755";
|
"Z ${configDir}/ 0700 nobody nogroup -"
|
||||||
user = "nobody";
|
];
|
||||||
group = "nogroup";
|
|
||||||
}
|
|
||||||
++ [ (xlib.helpers.mkTmpfile "Z" "${sourceDir}/" "0700" "nobody" "nogroup") ];
|
|
||||||
|
|
||||||
sops.secrets = {
|
sops.secrets = {
|
||||||
intermediate-password = {
|
intermediate-password = {
|
||||||
|
|||||||
@@ -4,12 +4,18 @@
|
|||||||
inputs,
|
inputs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
let
|
||||||
|
master = import inputs.nixpkgs-master {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
};
|
||||||
|
in
|
||||||
{
|
{
|
||||||
services.syncthing = {
|
services.syncthing = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
# package = master.syncthing;
|
||||||
systemService = true;
|
systemService = true;
|
||||||
guiAddress = "0.0.0.0:8384";
|
guiAddress = "0.0.0.0:8384";
|
||||||
configDir = "${xlib.dirs.storage}/persist/Syncthing/${xlib.device.hostname}";
|
configDir = "${xlib.dirs.storage}/Syncthing/${xlib.device.hostname}";
|
||||||
dataDir = "${xlib.dirs.server-home}";
|
dataDir = "${xlib.dirs.server-home}";
|
||||||
group = "users";
|
group = "users";
|
||||||
user = "${xlib.device.username}";
|
user = "${xlib.device.username}";
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
xlib,
|
xlib,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
sourceDir = "${xlib.dirs.services-mnt-folder}/trilium";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
services.trilium-server = {
|
services.trilium-server = {
|
||||||
enable = false;
|
enable = false;
|
||||||
@@ -14,10 +11,10 @@ in
|
|||||||
hostName = "trilium";
|
hostName = "trilium";
|
||||||
};
|
};
|
||||||
host = "0.0.0.0";
|
host = "0.0.0.0";
|
||||||
dataDir = "${sourceDir}";
|
dataDir = "/mnt/services/trilium";
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
(xlib.helpers.mkTmpfile "z" sourceDir "0750" "trilium" "trilium")
|
"z /mnt/services/trilium 0750 trilium trilium -"
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,9 @@
|
|||||||
inputs,
|
inputs,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
targetDir = "/var/lib/private/uptime-kuma";
|
|
||||||
sourceDir = "${xlib.dirs.services-mnt-folder}/uptime-kuma";
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
services.uptime-kuma = {
|
services.uptime-kuma = {
|
||||||
enable = true;
|
enable = false;
|
||||||
settings = {
|
settings = {
|
||||||
PORT = "4001";
|
PORT = "4001";
|
||||||
HOST = "0.0.0.0";
|
HOST = "0.0.0.0";
|
||||||
@@ -20,11 +16,15 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
(xlib.helpers.mkTmpfile "z" sourceDir "0755" "nobody" "nogroup")
|
"z ${xlib.dirs.services-mnt-folder}/uptime-kuma 0755 nobody nogroup -"
|
||||||
];
|
];
|
||||||
|
|
||||||
fileSystems = xlib.helpers.mkBindMount {
|
fileSystems."/var/lib/private/uptime-kuma" = {
|
||||||
what = sourceDir;
|
device = "${xlib.dirs.services-mnt-folder}/uptime-kuma";
|
||||||
where = targetDir;
|
fsType = "none";
|
||||||
|
options = [
|
||||||
|
"bind"
|
||||||
|
"nofail"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
xlib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
services.tts.servers = {
|
||||||
|
russian = {
|
||||||
|
enable = true;
|
||||||
|
port = 5301;
|
||||||
|
};
|
||||||
|
english = {
|
||||||
|
#enable = false;
|
||||||
|
port = 5300;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{ pkgs, ... }:
|
||||||
|
#? 🙏 https://github.com/ViZiD/dotfiles/blob/master/modules/shared/zapret.nix
|
||||||
|
#? https://github.com/Flowseal/zapret-discord-youtube/blob/main/general.bat
|
||||||
|
let
|
||||||
|
list-general = pkgs.fetchurl {
|
||||||
|
# https://github.com/Flowseal/zapret-discord-youtube/blob/main/lists/list-general.txt
|
||||||
|
url = "https://raw.githubusercontent.com/Flowseal/zapret-discord-youtube/779853740f9c957551685bdebf59ad3a788b5004/lists/list-general.txt";
|
||||||
|
sha256 = "sha256-/9dYk5fiVLfN+bY0STlqutnRQQoInS9NBGg9fWHZedk=";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
services.zapret = {
|
||||||
|
enable = true;
|
||||||
|
udpSupport = true;
|
||||||
|
udpPorts = [
|
||||||
|
"443"
|
||||||
|
"50000:50099"
|
||||||
|
];
|
||||||
|
params = [
|
||||||
|
"--filter-tcp=80"
|
||||||
|
"--hostlist=${list-general}"
|
||||||
|
"--dpi-desync=fake,split2"
|
||||||
|
"--dpi-desync-autottl=2"
|
||||||
|
"--dpi-desync-fooling=md5sig"
|
||||||
|
|
||||||
|
"--new"
|
||||||
|
"--filter-tcp=443"
|
||||||
|
"--hostlist=${list-general}"
|
||||||
|
"--dpi-desync=fake,multidisorder"
|
||||||
|
"--dpi-desync-split-pos=midsld"
|
||||||
|
"--dpi-desync-repeats=8"
|
||||||
|
"--dpi-desync-fooling=md5sig,badseq"
|
||||||
|
|
||||||
|
"--new"
|
||||||
|
"--filter-udp=443"
|
||||||
|
"--hostlist=${list-general}"
|
||||||
|
"--dpi-desync=fake"
|
||||||
|
"--dpi-desync-repeats=6"
|
||||||
|
|
||||||
|
"--new"
|
||||||
|
"--filter-udp=50000-50099"
|
||||||
|
"--filter-l7=discord,stun"
|
||||||
|
"--dpi-desync=fake"
|
||||||
|
"--dpi-desync-repeats=6"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
xlib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
# services.zapret = {
|
||||||
|
# enable = true;
|
||||||
|
# params = [
|
||||||
|
# "--dpi-desync=fake,disorder2"
|
||||||
|
# "--dpi-desync-ttl=1"
|
||||||
|
# "--dpi-desync-autottl=2"
|
||||||
|
# ];
|
||||||
|
# };
|
||||||
|
|
||||||
|
systemd.services.zapret = {
|
||||||
|
enable = true;
|
||||||
|
after = [ "network.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
description = "Custom Script Service";
|
||||||
|
path = with pkgs; [
|
||||||
|
sudo
|
||||||
|
git
|
||||||
|
nftables
|
||||||
|
iproute2
|
||||||
|
zapret
|
||||||
|
coreutils
|
||||||
|
];
|
||||||
|
unitConfig = {
|
||||||
|
After = [ "network-online.target" ];
|
||||||
|
Wants = [ "network-online.target" ];
|
||||||
|
};
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
WorkingDirectory = "${xlib.dirs.user-services}/zapret";
|
||||||
|
User = "root";
|
||||||
|
ExecStart = ''
|
||||||
|
/run/current-system/sw/bin/bash ${xlib.dirs.user-services}/zapret/main_script.sh -nointeractive
|
||||||
|
'';
|
||||||
|
ExecStop = ''
|
||||||
|
/run/current-system/sw/bin/bash ${xlib.dirs.user-services}/zapret/stop_and_clean_nft.sh
|
||||||
|
'';
|
||||||
|
# ExecStopPost = ''
|
||||||
|
# /run/current-system/sw/bin/echo "Сервис завершён"
|
||||||
|
# '';
|
||||||
|
PIDFile = ''
|
||||||
|
/run/zapret_discord_youtube.pid
|
||||||
|
'';
|
||||||
|
# Restart = "on-failure";
|
||||||
|
# RestartSec = "5s";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -7,8 +7,31 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
stable = import inputs.nixpkgs-beets {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
let
|
||||||
|
# depsOverlay = import ./dependencies.nix {
|
||||||
|
# # ./dependencies-full.nix if broken
|
||||||
|
# inherit (pkgs) fetchurl fetchgit fetchhg;
|
||||||
|
# inherit pkgs;
|
||||||
|
# };
|
||||||
|
# python3 = pkgs.python3.override {
|
||||||
|
# packageOverrides = depsOverlay;
|
||||||
|
# };
|
||||||
beetsEnv = pkgs.python314.withPackages (
|
beetsEnv = pkgs.python314.withPackages (
|
||||||
ps: with ps; [
|
ps: with ps; [
|
||||||
|
# et-xmlfile
|
||||||
|
# exceptiongroup
|
||||||
|
# markdown-it-py
|
||||||
|
# mdurl
|
||||||
|
# munkres
|
||||||
|
# musicbrainzngs
|
||||||
|
# openpyxl
|
||||||
|
# pygments
|
||||||
|
# rich
|
||||||
|
# sniffio
|
||||||
anyio
|
anyio
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
beetcamp
|
beetcamp
|
||||||
@@ -45,6 +68,7 @@ let
|
|||||||
requests
|
requests
|
||||||
requests-ratelimiter
|
requests-ratelimiter
|
||||||
scipy
|
scipy
|
||||||
|
# setuptools
|
||||||
six
|
six
|
||||||
socksio
|
socksio
|
||||||
soupsieve
|
soupsieve
|
||||||
@@ -68,9 +92,14 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
systemd.mounts = [
|
systemd.mounts = [
|
||||||
(xlib.helpers.mkSystemdBind {
|
{
|
||||||
|
enable = true;
|
||||||
|
options = "bind,x-systemd.automount,nofail";
|
||||||
|
requires = [ "local-fs.target" ];
|
||||||
|
type = "none";
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
what = "/home/${xlib.device.username}/Music";
|
what = "/home/${xlib.device.username}/Music";
|
||||||
where = "/home/${xlib.device.username}/.config/beets";
|
where = "/home/${xlib.device.username}/.config/beets";
|
||||||
})
|
}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
# Generated by pip2nix 0.8.0.dev1
|
||||||
|
# See https://github.com/nix-community/pip2nix
|
||||||
|
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
fetchurl,
|
||||||
|
fetchgit,
|
||||||
|
fetchhg,
|
||||||
|
}:
|
||||||
|
|
||||||
|
self: super: {
|
||||||
|
"PyYAML" = super.buildPythonPackage rec {
|
||||||
|
pname = "PyYAML";
|
||||||
|
version = "6.0.2";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz";
|
||||||
|
sha256 = "0gmwggzm0j0iprx074g5hah91y2f68sfhhldq0f8crddj7ndk16m";
|
||||||
|
};
|
||||||
|
format = "setuptools";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"Unidecode" = super.buildPythonPackage rec {
|
||||||
|
pname = "Unidecode";
|
||||||
|
version = "1.3.8";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/84/b7/6ec57841fb67c98f52fc8e4a2d96df60059637cba077edc569a302a8ffc7/Unidecode-1.3.8-py3-none-any.whl";
|
||||||
|
sha256 = "0fgxj6h9lkjq4saynkjqf2wb9plsr5wyg3xxld482vv9wqfacc6i";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"anyio" = super.buildPythonPackage rec {
|
||||||
|
pname = "anyio";
|
||||||
|
version = "4.7.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/a0/7a/4daaf3b6c08ad7ceffea4634ec206faeff697526421c20f07628c7372156/anyio-4.7.0-py3-none-any.whl";
|
||||||
|
sha256 = "0lp30wfn1hs2wvaz3wadzwwcb3l9ii4b1k78yzzscaxl79rc6q7a";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."exceptiongroup"
|
||||||
|
self."idna"
|
||||||
|
self."sniffio"
|
||||||
|
self."typing-extensions"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"beautifulsoup4" = super.buildPythonPackage rec {
|
||||||
|
pname = "beautifulsoup4";
|
||||||
|
version = "4.12.3";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/b1/fe/e8c672695b37eecc5cbf43e1d0638d88d66ba3a44c4d321c796f4e59167f/beautifulsoup4-4.12.3-py3-none-any.whl";
|
||||||
|
sha256 = "1vc2w3wvnhbp2q287ilzjsiqyvd0vc5s52ysalz32481yk4ph25q";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."soupsieve"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"beetcamp" = super.buildPythonPackage rec {
|
||||||
|
pname = "beetcamp";
|
||||||
|
version = "0.21.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/6c/d3/94cad1ba1e65a9445655968a6dcdd528cb1352e2389f0921a9f8c0ccd4a0/beetcamp-0.21.0-py3-none-any.whl";
|
||||||
|
sha256 = "08mxqmckg2fx9rkm5a1n9zs2sjccjj75vgxac22xjyi3fw1k0wz2";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
#self."beets" # its doesnt matter?
|
||||||
|
self."httpx"
|
||||||
|
self."packaging"
|
||||||
|
self."pycountry"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"beets" = super.buildPythonPackage rec {
|
||||||
|
pname = "beets";
|
||||||
|
version = "2.2.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/f3/44/1c53c2ac111e5526083e58f50a22504ad7c609be1ce660c0339938a42c33/beets-2.2.0-py3-none-any.whl";
|
||||||
|
sha256 = "076hl1j74cgyh6n1piwprnzb89gihy2vmajm8lzfhy1jjcrfrpbd";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."PyYAML"
|
||||||
|
self."Unidecode"
|
||||||
|
self."confuse"
|
||||||
|
self."jellyfish"
|
||||||
|
self."mediafile"
|
||||||
|
self."munkres"
|
||||||
|
self."musicbrainzngs"
|
||||||
|
self."platformdirs"
|
||||||
|
self."typing-extensions"
|
||||||
|
# ext
|
||||||
|
self."requests" # For spotify, deezer, embedart, fetchart, lyrics
|
||||||
|
self."python3-discogs-client" # For discogs
|
||||||
|
self."pylast" # For lastgenre
|
||||||
|
# self."beetcamp" # Another
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"certifi" = super.buildPythonPackage rec {
|
||||||
|
pname = "certifi";
|
||||||
|
version = "2024.12.14";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl";
|
||||||
|
sha256 = "0mpccx4yscnk6rhl12fk8wpgwrpq62m4w23k27y4wip9bfjgfx8j";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"charset-normalizer" = super.buildPythonPackage rec {
|
||||||
|
pname = "charset-normalizer";
|
||||||
|
version = "3.4.1";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz";
|
||||||
|
sha256 = "18sfsqpdmxbddr3g3yg0sln10ghq4sp0vl2xb1b5p9v8rlc1y9a4";
|
||||||
|
};
|
||||||
|
format = "setuptools";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"colorama" = super.buildPythonPackage rec {
|
||||||
|
pname = "colorama";
|
||||||
|
version = "0.4.6";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl";
|
||||||
|
sha256 = "1ijz53xpmxds2qf02l9yf0rnp7bznwh3ci4xkw8wmh5cyn8rj7ag";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"confuse" = super.buildPythonPackage rec {
|
||||||
|
pname = "confuse";
|
||||||
|
version = "2.0.1";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/32/1f/cf496479814d41fc252004482deeb90b740b4a6a391a3355c0b11d7e0abf/confuse-2.0.1-py3-none-any.whl";
|
||||||
|
sha256 = "0amxm8vnxcayh7inahvj3fzj33n8gs8lvcfaicqrpjz2f2y5p7lv";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."PyYAML"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"exceptiongroup" = super.buildPythonPackage rec {
|
||||||
|
pname = "exceptiongroup";
|
||||||
|
version = "1.3.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl";
|
||||||
|
sha256 = "044alxyhkfdlr5z3xlpnf5lv78310bnsgnkdmm669l0k1ip1w4ad";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."typing-extensions"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"filetype" = super.buildPythonPackage rec {
|
||||||
|
pname = "filetype";
|
||||||
|
version = "1.2.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl";
|
||||||
|
sha256 = "099d3igvmfcdgg9dcylz8advva5n3qpplsf8gb7l24hqh1l1prvw";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"h11" = super.buildPythonPackage rec {
|
||||||
|
pname = "h11";
|
||||||
|
version = "0.14.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl";
|
||||||
|
sha256 = "0qd7z9p38dwx215048q708vd1sn2abdh1mb3hg66ii2ip324mzp3";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"httpcore" = super.buildPythonPackage rec {
|
||||||
|
pname = "httpcore";
|
||||||
|
version = "1.0.7";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl";
|
||||||
|
sha256 = "1p8f8bnrir1d50s6z19jndca98qghgqrr7rx6syxaq627psgizx3";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."certifi"
|
||||||
|
self."h11"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"httpx" = super.buildPythonPackage rec {
|
||||||
|
pname = "httpx";
|
||||||
|
version = "0.28.1";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl";
|
||||||
|
sha256 = "1barpaw8as8xb7b2bsmzdmdbq5nqljlq5jhlz3xcgy0hq76gq2fr";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."anyio"
|
||||||
|
self."certifi"
|
||||||
|
self."httpcore"
|
||||||
|
self."idna"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"idna" = super.buildPythonPackage rec {
|
||||||
|
pname = "idna";
|
||||||
|
version = "3.10";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl";
|
||||||
|
sha256 = "1lw72a5swas501zvkpd6dsryj5hzjijqxs3526kbp7151md1jvcl";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
# "jellyfish" = super.buildPythonPackage rec { # That is Rust package
|
||||||
|
# pname = "jellyfish";
|
||||||
|
# version = "1.1.3";
|
||||||
|
# src = fetchurl {
|
||||||
|
# url = "https://files.pythonhosted.org/packages/5b/3a/f607d7d44ee5cbad51ce8e2966bde112789eeb53633558f500bc4e44c053/jellyfish-1.1.3.tar.gz";
|
||||||
|
# sha256 = "17wgy021wsp8jj95v638kfk34r9yzbry3q7shnglj5npmgfs22v5";
|
||||||
|
# };
|
||||||
|
# format = "setuptools";
|
||||||
|
# doCheck = false;
|
||||||
|
# buildInputs = [ ];
|
||||||
|
# checkInputs = [ ];
|
||||||
|
# nativeBuildInputs = [ ];
|
||||||
|
# propagatedBuildInputs = [ ];
|
||||||
|
# };
|
||||||
|
"langdetect" = super.buildPythonPackage rec {
|
||||||
|
pname = "langdetect";
|
||||||
|
version = "1.0.9";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz";
|
||||||
|
sha256 = "1805svvb7xjm4sf1j7b6nc3409x37pd1xmabfwwjf1ldkzwgxhfb";
|
||||||
|
};
|
||||||
|
format = "setuptools";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."six"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"mediafile" = super.buildPythonPackage rec {
|
||||||
|
pname = "mediafile";
|
||||||
|
version = "0.13.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/9f/b0/363b4d1443a593398f9d3784f406385f075e8fd0991c35356e73fc37638a/mediafile-0.13.0-py3-none-any.whl";
|
||||||
|
sha256 = "1jqlwmwpgn0fxkbxrj8y5a4wr3ikwgs2rsc678hbaw861qyii3fd";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."filetype"
|
||||||
|
self."mutagen"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"munkres" = super.buildPythonPackage rec {
|
||||||
|
pname = "munkres";
|
||||||
|
version = "1.1.4";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/90/ab/0301c945a704218bc9435f0e3c88884f6b19ef234d8899fb47ce1ccfd0c9/munkres-1.1.4-py2.py3-none-any.whl";
|
||||||
|
sha256 = "0apdpkbhg4wq5pis5d2mkqg46ikwix5nwcm2mrjxi04499yqc0bb";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"musicbrainzngs" = super.buildPythonPackage rec {
|
||||||
|
pname = "musicbrainzngs";
|
||||||
|
version = "0.7.1";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/6d/fd/cef7b2580436910ccd2f8d3deec0f3c81743e15c0eb5b97dde3fbf33c0c8/musicbrainzngs-0.7.1-py2.py3-none-any.whl";
|
||||||
|
sha256 = "040s1q4ia6gl2bjjxrjs384980854s9za28b55r0lk0hfpwshhg8";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"mutagen" = super.buildPythonPackage rec {
|
||||||
|
pname = "mutagen";
|
||||||
|
version = "1.47.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/b0/7a/620f945b96be1f6ee357d211d5bf74ab1b7fe72a9f1525aafbfe3aee6875/mutagen-1.47.0-py3-none-any.whl";
|
||||||
|
sha256 = "06d7miq4z6m7j8rx2czkmqhgbjb2bwjagfz5v0wraylhqm86zngd";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"oauthlib" = super.buildPythonPackage rec {
|
||||||
|
pname = "oauthlib";
|
||||||
|
version = "3.2.2";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl";
|
||||||
|
sha256 = "1jpvcxq0xr3z50fdg828in1icgz8cfcy3sc04r85vqhkmjdg4fc1";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"packaging" = super.buildPythonPackage rec {
|
||||||
|
pname = "packaging";
|
||||||
|
version = "24.2";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl";
|
||||||
|
sha256 = "0nd7a421brjgd4prm8fbs8a6bcv4n1yplgxalgs02p16rnyb3aq9";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"pillow" = super.buildPythonPackage rec {
|
||||||
|
pname = "pillow";
|
||||||
|
version = "11.0.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz";
|
||||||
|
sha256 = "0fbpcwgiac19ap0h1qa1imsqhq6vxv8kg67zkgm3y05c4jpwpfkj";
|
||||||
|
};
|
||||||
|
format = "setuptools";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"platformdirs" = super.buildPythonPackage rec {
|
||||||
|
pname = "platformdirs";
|
||||||
|
version = "4.3.6";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl";
|
||||||
|
sha256 = "1yy39iay8fdb3c1r4gm011lla1sk1mc9fsw300wi1f4a83hpbrbk";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"pycountry" = super.buildPythonPackage rec {
|
||||||
|
pname = "pycountry";
|
||||||
|
version = "24.6.1";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/b1/ec/1fb891d8a2660716aadb2143235481d15ed1cbfe3ad669194690b0604492/pycountry-24.6.1-py3-none-any.whl";
|
||||||
|
sha256 = "0vz0dhfkbjld5jagh9wafwy27k5d83bmd5fkxy74y8fp3hwzp97i";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"pylast" = super.buildPythonPackage rec {
|
||||||
|
pname = "pylast";
|
||||||
|
version = "5.3.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/9b/57/e25206d012afe3fe5e3336a875babb5413b81c00706411a645a38185ad3b/pylast-5.3.0-py3-none-any.whl";
|
||||||
|
sha256 = "023ki92jgc9mk2k9c4li48zf23yz2wn022m1rsjj9bsvn3f7ri2c";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."httpx"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"python-dateutil" = super.buildPythonPackage rec {
|
||||||
|
pname = "python-dateutil";
|
||||||
|
version = "2.9.0.post0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl";
|
||||||
|
sha256 = "09q48zvsbagfa3w87zkd2c5xl54wmb9rf2hlr20j4a5fzxxvrcm8";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."six"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"python3-discogs-client" = super.buildPythonPackage rec {
|
||||||
|
pname = "python3-discogs-client";
|
||||||
|
version = "2.7.1";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/02/eb/b3d321440728addb72296e75ae2bfddd8fd3518b5ce5bd54a1ad821227a2/python3_discogs_client-2.7.1-py3-none-any.whl";
|
||||||
|
sha256 = "0i3lfdn2ncxfvmmldg31gmdv7vdcicjl890mihncxa48yb9g7daz";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."oauthlib"
|
||||||
|
self."python-dateutil"
|
||||||
|
self."requests"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"requests" = super.buildPythonPackage rec {
|
||||||
|
pname = "requests";
|
||||||
|
version = "2.32.3";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl";
|
||||||
|
sha256 = "1inwsrhx0m16q0wa1z6dfm8i8xkrfns73xm25arcwwy70gz1qxkh";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
self."certifi"
|
||||||
|
self."charset-normalizer"
|
||||||
|
self."idna"
|
||||||
|
self."urllib3"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"six" = super.buildPythonPackage rec {
|
||||||
|
pname = "six";
|
||||||
|
version = "1.17.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl";
|
||||||
|
sha256 = "0x1jdic712dylbnyiqdj4xyxrlx0gaacynmbmkfiym4hxn8z68a7";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"sniffio" = super.buildPythonPackage rec {
|
||||||
|
pname = "sniffio";
|
||||||
|
version = "1.3.1";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl";
|
||||||
|
sha256 = "18i50l85yppn9w1ily8m342yd577h0bg8y24hkfzvq7is4ca8v9g";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"soupsieve" = super.buildPythonPackage rec {
|
||||||
|
pname = "soupsieve";
|
||||||
|
version = "2.6";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl";
|
||||||
|
sha256 = "1jfc0b39kwnh4n30458mr8gmh50mx3k5zxghm6sy9djgdvq4yb77";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"typing-extensions" = super.buildPythonPackage rec {
|
||||||
|
pname = "typing-extensions";
|
||||||
|
version = "4.12.2";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl";
|
||||||
|
sha256 = "03bhjivpvdhn4c3x0963z89hv7b5vxr415akd1fgiwz0a41wmr84";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
"urllib3" = super.buildPythonPackage rec {
|
||||||
|
pname = "urllib3";
|
||||||
|
version = "2.3.0";
|
||||||
|
src = fetchurl {
|
||||||
|
url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl";
|
||||||
|
sha256 = "1pz380a93mhdrzx5003inw5pm5n0fh1xazcbnjxzsyw6d79rmvhw";
|
||||||
|
};
|
||||||
|
format = "wheel";
|
||||||
|
doCheck = false;
|
||||||
|
buildInputs = [ ];
|
||||||
|
checkInputs = [ ];
|
||||||
|
nativeBuildInputs = [ ];
|
||||||
|
propagatedBuildInputs = [ ];
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user