From d0cde973e778f91d79c1db5285250831b8fac879 Mon Sep 17 00:00:00 2001 From: Anish Lakhwara Date: Mon, 19 Jan 2026 22:37:30 -0800 Subject: [PATCH] box zfs --- agents.md | 97 ++++++++++ flake.lock | 10 +- flake.nix | 6 +- home/dev/default.nix | 18 +- home/profiles/beets/default.nix | 53 ++++++ hosts/box/configuration.nix | 114 ++++++------ hosts/box/default.nix | 66 ++++--- hosts/box/disko.nix | 238 ++++++++++++++++++++++++ hosts/box/hardware-configuration.nix | 80 +++----- hosts/curve/default.nix | 87 ++++++--- hosts/profiles/archivebox/default.nix | 23 ++- hosts/profiles/calibre/default.nix | 4 +- hosts/profiles/gonic/default.nix | 15 +- hosts/profiles/headphones/default.nix | 9 +- hosts/profiles/immich/default.nix | 2 +- hosts/profiles/jellyfin/default.nix | 31 ++- hosts/profiles/monitoring/default.nix | 48 +++-- hosts/profiles/nfs/default.nix | 27 ++- hosts/profiles/sync/music/get-music.sh | 8 +- hosts/profiles/transmission/default.nix | 6 +- install-box.sh | 119 ++++++++++++ 21 files changed, 818 insertions(+), 243 deletions(-) create mode 100644 agents.md create mode 100644 home/profiles/beets/default.nix create mode 100644 hosts/box/disko.nix create mode 100755 install-box.sh diff --git a/agents.md b/agents.md new file mode 100644 index 0000000..62bcae4 --- /dev/null +++ b/agents.md @@ -0,0 +1,97 @@ +# Helm Environment Overview + +## Box NAS Server (`box` / `mossnet.lan`) + +### Hardware +- NVMe boot drive (LUKS encrypted) +- 3x 4TB drives in ZFS RAIDZ1 pool (`tank`) - ~7.14TB usable + +### ZFS Datasets +| Dataset | Mountpoint | Purpose | +|---------|------------|---------| +| `tank/data/media` | `/tank/media` | Media library (music, photos, tv, movies) | +| `tank/data/books` | `/tank/books` | Calibre library | +| `tank/data/podcasts` | `/tank/podcasts` | Podcast storage | +| `tank/data/new-music` | `/tank/new-music` | Incoming music from seedbox | +| `tank/data/backup` | `/tank/backup` | PostgreSQL backups | +| `tank/data/archive` | `/tank/archive` | Old data (memories, old-home-dirs, etc.) | + +### Services +- **Immich** - Photo management (`/tank/media/photos`) +- **Gonic** - Music streaming (`/tank/media/music`) +- **Calibre-server/Calibre-web** - Ebook management (`/tank/books`) +- **Jellyfin** - Media streaming +- **Lidarr** - Music management (runs as `headphones:audio`) +- **Radicale** - CalDAV/CardDAV +- **Syncthing** - File sync +- **PostgreSQL** - Database +- **Taskserver** - Taskwarrior sync + +--- + +## Repository Structure + +``` +helm/ +├── flake.nix # Main flake - defines all hosts +├── hosts/ +│ ├── box/ +│ │ ├── default.nix # Box host config, imports profiles +│ │ ├── configuration.nix # Hardware/boot config +│ │ └── disko.nix # Disk/ZFS layout +│ ├── profiles/ # NixOS service profiles +│ │ ├── sync/music/ # get-music-sync service +│ │ ├── headphones/ # Lidarr config +│ │ ├── jellyfin/ +│ │ ├── monitoring/ +│ │ └── ... +├── home/ +│ ├── dev/ +│ │ └── default.nix # Home-manager config for box +│ └── profiles/ +│ ├── beets/ # Beets music library config +│ ├── cli/ +│ ├── nvim/ +│ ├── git/ +│ └── opencode/ +├── secrets/ # Agenix encrypted secrets +└── modules/ # Custom NixOS modules +``` + +--- + +## Deployment + +```bash +# Deploy to a host +deploy .#box +deploy .#curve +deploy .#helix +deploy .#lituus + +# SSH access to box +ssh anish@mossnet.lan +``` + +--- + +## Key Users/Groups + +| User | Group | Purpose | +|------|-------|---------| +| `anish` | `users`, `wheel`, `audio`, `video`, `docker` | Primary user | +| `headphones` | `audio` | Lidarr service | +| `gonic` | `audio` | Gonic music streaming | +| `immich` | `immich` | Photo management | +| `calibre-server` | `calibre-server` | Ebook server | + +--- + +## Hosts + +| Host | Description | +|------|-------------| +| `box` | NAS server (mossnet.lan) | +| `curve` | Workstation | +| `helix` | Workstation | +| `lituus` | VPS/Server | diff --git a/flake.lock b/flake.lock index 651e3c5..03e305d 100644 --- a/flake.lock +++ b/flake.lock @@ -737,15 +737,15 @@ "treefmt-nix": "treefmt-nix_2" }, "locked": { - "lastModified": 1768359433, - "narHash": "sha256-e/6qI81VBJo0lAQsyUG+2jMsL0q3YLz88NZoZOCVFu8=", - "owner": "Chickensoupwithrice", + "lastModified": 1768434130, + "narHash": "sha256-4rBBs7spDuimvUcL3egp2Zh94Lk8pf00VsjkOs59h7E=", + "owner": "numtide", "repo": "llm-agents.nix", - "rev": "596bf03f14e9a54654473a1666b3b274bbc5939e", + "rev": "d0ed3ef68a04b5bd127fecd405baf803eea29c29", "type": "github" }, "original": { - "owner": "Chickensoupwithrice", + "owner": "numtide", "repo": "llm-agents.nix", "type": "github" } diff --git a/flake.nix b/flake.nix index 2bcb31a..e0244d3 100644 --- a/flake.nix +++ b/flake.nix @@ -75,8 +75,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; - # LLM Agents (using fork until chainlink PR is merged) - llm-agents.url = "github:Chickensoupwithrice/llm-agents.nix"; + llm-agents.url = "github:numtide/llm-agents.nix"; # Others nur.url = "github:nix-community/NUR"; @@ -361,6 +360,7 @@ pkgs = nixpkgsFor.${system}; modules = [ ./hosts/box + disko.nixosModules.disko agenix.nixosModules.age self.nixosModules.backup self.nixosModules.wireguard @@ -368,7 +368,7 @@ self.nixosModules.gpodder2go self.nixosModules.wallabag self.nixosModules.ulogger-server - grasp.nixosModule + # grasp.nixosModule # Disabled for initial install - private repo home-manager.nixosModules.home-manager { nix.registry.nixpkgs.flake = nixpkgs; diff --git a/home/dev/default.nix b/home/dev/default.nix index 78a8563..3d2e938 100644 --- a/home/dev/default.nix +++ b/home/dev/default.nix @@ -1,5 +1,17 @@ -{ self, pkgs, inputs, ... }: { - imports = - [ ../profiles/cli ../profiles/nvim ../profiles/direnv ../profiles/git ../profiles/opencode ]; +{ + self, + pkgs, + inputs, + ... +}: +{ + imports = [ + ../profiles/cli + ../profiles/nvim + ../profiles/direnv + ../profiles/git + ../profiles/opencode + ../profiles/beets + ]; home.stateVersion = "22.05"; } diff --git a/home/profiles/beets/default.nix b/home/profiles/beets/default.nix new file mode 100644 index 0000000..dc7a315 --- /dev/null +++ b/home/profiles/beets/default.nix @@ -0,0 +1,53 @@ +{ pkgs, ... }: +{ + programs.beets = { + enable = true; + package = pkgs.beets.override { + pluginOverrides = { + fetchart.enable = true; + embedart.enable = true; + lastgenre.enable = true; + duplicates.enable = true; + missing.enable = true; + }; + }; + settings = { + directory = "/tank/media/music"; + library = "/home/anish/.local/share/beets/library.db"; + + import = { + move = true; # Move files from new-music to library + write = true; # Write tags to files + log = "/tank/new-music/beets-import.log"; + incremental = true; # Skip already-imported directories + }; + + # Path format for organizing music + paths = { + default = "$albumartist/$album%aunique{}/$track $title"; + singleton = "Non-Album/$artist/$title"; + comp = "Compilations/$album%aunique{}/$track $title"; + }; + + plugins = [ + "fetchart" + "embedart" + "lastgenre" + "duplicates" + "missing" + ]; + + fetchart = { + auto = true; + }; + + embedart = { + auto = true; + }; + + lastgenre = { + auto = true; + }; + }; + }; +} diff --git a/hosts/box/configuration.nix b/hosts/box/configuration.nix index 4ff5d22..867c86d 100644 --- a/hosts/box/configuration.nix +++ b/hosts/box/configuration.nix @@ -1,79 +1,73 @@ # Edit this configuration file to define what should be installed on # your system. Help is available in the configuration.nix(5) man page -# and in the NixOS manual (accessible by running ‘nixos-help’). +# and in the NixOS manual (accessible by running 'nixos-help'). { config, pkgs, ... }: { - imports = - [ - # Include the results of the hardware scan. - ./hardware-configuration.nix - ]; + imports = [ + # Include the results of the hardware scan. + ./hardware-configuration.nix + ./disko.nix + ]; # No systemd emergency mode (can't reliably be accessed over SSH) systemd.enableEmergencyMode = false; - # Use the GRUB 2 boot loader. - boot.loader.efi.canTouchEfiVariables = false; - boot.loader.efi.efiSysMountPoint = "/boot/efi"; - boot.loader.grub = { - enable = true; - device = "nodev"; - efiSupport = true; - enableCryptodisk = true; - efiInstallAsRemovable = true; + # ZFS requires a hostId + networking.hostId = "bb7d707a"; + + # Boot configuration for LUKS + ZFS + boot.loader.efi.canTouchEfiVariables = true; + boot.loader.efi.efiSysMountPoint = "/boot"; + boot.loader.systemd-boot.enable = true; + + # ZFS support + boot.supportedFilesystems = [ "zfs" ]; + boot.zfs = { + requestEncryptionCredentials = [ "tank" ]; # Load key for tank pool + forceImportRoot = false; }; - boot.initrd.secrets = { - "/keyfile0.bin" = /etc/secrets/initrd/keyfile0.bin; - "/keyfile1.bin" = /etc/secrets/initrd/keyfile1.bin; - }; - - # Remote SSH unlock - boot.initrd.network.enable = true; - boot.initrd.network.ssh = { - enable = true; - port = 22; - authorizedKeys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM0Zvei46x/yZl/IeBCq6+IYQQ0avulzVyBysF9cPigZMCybWRV7IEU+E3k9t6JrbdbdGfJkcZIWmsWDdKS8W8mBnZpVoT0ffLynu8JQ/TKdGm4Qv6bgUeKNrGsNv0ZPs2CDaGSLj0oJfRF7Ko10tcLP0vW+yujrh+y6TH/vVzJioaV4TGvtCUpn+wEQah9ROwPQLUUofsSWdnRsDJ/gp37zXWs4l5wyjSKtP3O9RZUP7kBekbSqEgSXiTk0oUQSVqIWl9NDiP6onk/gSOjXsR/JPqsSN/XI/c/yj6gyY0f51Ru2D7iBxuMJIJcWV+rU6coIj+ULcQWLzt/7TI8jq5AOOzI/ll4zbL24Eo84Rz+TP9tvMMhDZ0VaMN22AJ8qQEjc5P09tWKsX7Jg39XelyV1jHXncE4yvIE9F4RSCHzWCeKeXakizQNuzSaxTxIExRFYHjNW5bR6+3MTGwVrEIXU+qML+0yFTR86MT+tdY5AreAJQLwbog79O1NupeXJE= anish@curve " ]; - hostKeys = [ "/etc/secrets/initrd/ssh_host_ed25519_key" ]; # create this file with `ssh-keygen -t ed25519 -N "" -f /etc/secrets/initrd/ssh_host_ed25519_key` - }; - boot.initrd.availableKernelModules = [ "igc" "iwlwifi" ]; - - boot.initrd.luks.devices = { - "root" = { - #name = "root"; - device = "/dev/disk/by-uuid/f37f3222-47d7-42d8-b400-363320a31853"; # UUID for /dev/nvme01np2 - preLVM = true; - allowDiscards = true; - keyFile = "/keyfile0.bin"; + # ZFS services + services.zfs = { + autoScrub = { + enable = true; + interval = "weekly"; }; + autoSnapshot = { + enable = true; + frequent = 4; # 15-minute snapshots + hourly = 24; + daily = 7; + weekly = 4; + monthly = 12; + }; + trim.enable = true; }; - # Data mount - # fileSystems."/data" = { - # device = "/dev/disk/by-uuid/3276a297-9ee4-4998-b262-1ed100366c06"; # UUID for /dev/mapper/crypted-data - # encrypted = { - # enable = true; - # label = "crypted-data"; - # blkDev = "/dev/disk/by-uuid/8a317bf4-fe13-4334-a6df-5fe5a5048b5e"; # UUID for /dev/sda1 - # keyFile = "/keyfile1.bin"; - # }; - # }; - networking.interfaces.enp2s0 = { - ipv4.addresses = [{ - address = "192.168.1.240"; - prefixLength = 24; - }]; - ipv6.addresses = [{ - address = "fd7d:587a:4300:1::240"; - prefixLength = 64; - }]; - ipv4.routes = [{ address = "192.168.1.0"; prefixLength = 24; via = "192.168.1.1"; }]; + ipv4.addresses = [ + { + address = "192.168.1.240"; + prefixLength = 24; + } + ]; + ipv6.addresses = [ + { + address = "fd7d:587a:4300:1::240"; + prefixLength = 64; + } + ]; + ipv4.routes = [ + { + address = "192.168.1.0"; + prefixLength = 24; + via = "192.168.1.1"; + } + ]; useDHCP = false; }; - #networking.nameservers = [ "172.16.11.240" ]; networking.nameservers = [ "192.168.1.1" ]; networking.defaultGateway = { address = "192.168.1.1"; @@ -81,11 +75,8 @@ }; networking.hostName = "box"; # Define your hostname. - # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant. # The global useDHCP flag is deprecated, therefore explicitly set to false here. - # Per-interface useDHCP will be mandatory in the future, so this generated config - # replicates the default behaviour. networking.useDHCP = false; networking.interfaces.wlp3s0.useDHCP = true; @@ -103,4 +94,3 @@ system.stateVersion = "19.09"; # Did you read the comment? } - diff --git a/hosts/box/default.nix b/hosts/box/default.nix index 8d0aa11..f664638 100644 --- a/hosts/box/default.nix +++ b/hosts/box/default.nix @@ -1,4 +1,5 @@ -{ self, pkgs, ... }: { +{ self, pkgs, ... }: +{ imports = [ ./configuration.nix ../profiles/core @@ -6,10 +7,10 @@ ../profiles/taskd ../profiles/shaarli ../profiles/dns - # ../profiles/monitoring + ../profiles/monitoring ../profiles/nfs ../profiles/gonic - ../profiles/headphones + ../profiles/headphones ../profiles/radicale # ../profiles/seafile # waiting for https://github.com/NixOS/nixpkgs/pull/249523 to be merged ../profiles/syncthing @@ -19,64 +20,69 @@ ../profiles/finance ../profiles/sync/website ../profiles/sync/music - ../profiles/grasp - # ../profiles/archivebox - # ../profiles/woodpecker-agent - # ../profiles/jellyfin + # ../profiles/grasp # private repo - disabled + # ../profiles/archivebox # requires insecure django - fix in flake.nix permittedInsecurePackages + ../profiles/woodpecker-agent + ../profiles/jellyfin ../profiles/ulogger-server ../profiles/immich ../profiles/jacket ../profiles/gpodder ../profiles/transmission ../profiles/raven - #../profiles/postgres_upgrade_script + # ../profiles/postgres_upgrade_script # one-time use ]; # Backups age.secrets.borg-password.file = "${self}/secrets/borg-password.age"; services.postgresqlBackup = { enable = true; - databases = [ "wallabag" "immich" "ulogger" ]; - location = "/var/backup/postgresql"; + databases = [ + "wallabag" + "immich" + "ulogger" + ]; + location = "/tank/backup/postgresql"; }; mossnet.backup = { enable = true; name = "mossnet"; paths = [ "/var/lib/taskserver" # taskwarrior - "/var/www/shaarli-config" # sharli - "/var/backup/postgresql" # wallabag + "/var/www/shaarli-config" # shaarli + "/tank/backup/postgresql" # postgresql backups "/var/lib/radicale" # radicale - "/home/anish/usr/drawing" # syncthing - "/data/books" # calibre-web - # "/home/anish/usr/nonfiction" # syncthing + "/tank/syncthing/drawing" # syncthing + "/tank/books" # calibre-web "/home/anish/usr/finance" # beancount - "/mnt/two/postgres" # sealight postgres backups TODO remove once moved to capsul - "/mnt/two/photos" - "/mnt/two/music" + "/tank/postgres" # postgres data + "/tank/media/photos" + "/tank/media/music" ]; - # seafile }; # opencode-manager ports networking.firewall = { allowedTCPPorts = [ - 5003 # opencode-manager backend - 5173 # opencode-manager frontend - 5551 # opencode server + 5003 # opencode-manager backend + 5173 # opencode-manager frontend + 5551 # opencode server ]; - allowedTCPPortRanges = [{ - from = 7000; - to = 9000; - }]; # ports for testing user changes + allowedTCPPortRanges = [ + { + from = 7000; + to = 9000; + } + ]; # ports for testing user changes }; environment.systemPackages = with pkgs; [ lm_sensors ]; - hardware.fancontrol = { - enable = false; - config = ''''; - }; + # hardware.fancontrol = { + # enable = false; + # config = ''''; + # }; + # Secrets age.secrets.box-wg.file = "${self}/secrets/box-wg.age"; age.secrets.box-wg.owner = "anish"; age.secrets.borg-key.file = "${self}/secrets/borg-key.age"; diff --git a/hosts/box/disko.nix b/hosts/box/disko.nix new file mode 100644 index 0000000..d61fae8 --- /dev/null +++ b/hosts/box/disko.nix @@ -0,0 +1,238 @@ +# Disko configuration for box NAS +# NVMe boot drive with LUKS + 3x 4TB ZFS RAIDZ1 pool (~8TB usable) +# Unified encryption: LUKS passphrase unlocks root, ZFS uses keyfile inside encrypted root +# +# NOTE: Using RAIDZ1 (3 drives) temporarily due to DOA drive. Can migrate to RAIDZ2 +# with 4 drives later by creating a new pool and copying data. +# +# Installation steps: +# +# 1. Generate the ZFS keyfile and LUKS password: +# dd if=/dev/urandom of=/tmp/tank.key bs=32 count=1 +# echo -n "your-luks-password" > /tmp/luks-password +# +# 2. Run disko-install: +# sudo nix run 'github:nix-community/disko/latest#disko-install' -- \ +# --flake ~/helm#box \ +# --disk nvme /dev/disk/by-id/nvme-CT500P310SSD8_2544543B87C2 \ +# --disk zfs1 /dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX32D954A2J7 \ +# --disk zfs2 /dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX32D95FVZVL \ +# --disk zfs3 /dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX42D95M807R +# +# 3. Copy the keyfile and update keylocation: +# sudo mkdir -p /mnt/etc/zfs +# sudo cp /tmp/tank.key /mnt/etc/zfs/tank.key +# sudo chmod 000 /mnt/etc/zfs/tank.key +# sudo zfs set keylocation=file:///etc/zfs/tank.key tank +{ + disko.devices = { + disk = { + # Boot drive - 500GB NVMe with LUKS encryption + nvme = { + type = "disk"; + device = "/dev/disk/by-id/nvme-placeholder"; # Override with --disk nvme /dev/disk/by-id/... + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + luks = { + size = "100%"; + content = { + type = "luks"; + name = "cryptroot"; + settings = { + allowDiscards = true; + }; + # Passphrase will be prompted during boot + passwordFile = "/tmp/luks-password"; # Only used during install, set this before running disko + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + mountOptions = [ "noatime" ]; + }; + }; + }; + }; + }; + }; + + # ZFS pool drives - 3x 4TB in RAIDZ1 + zfs1 = { + type = "disk"; + device = "/dev/disk/by-id/placeholder-zfs1"; # Override with --disk zfs1 /dev/disk/by-id/... + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "tank"; + }; + }; + }; + }; + }; + zfs2 = { + type = "disk"; + device = "/dev/disk/by-id/placeholder-zfs2"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "tank"; + }; + }; + }; + }; + }; + zfs3 = { + type = "disk"; + device = "/dev/disk/by-id/placeholder-zfs3"; + content = { + type = "gpt"; + partitions = { + zfs = { + size = "100%"; + content = { + type = "zfs"; + pool = "tank"; + }; + }; + }; + }; + }; + }; + + zpool = { + tank = { + type = "zpool"; + mode = "raidz1"; + options = { + ashift = "12"; + cachefile = "none"; # Needed for disko + }; + rootFsOptions = { + compression = "lz4"; + atime = "off"; + xattr = "sa"; + acltype = "posixacl"; + # ZFS native encryption + # During install: keyfile at /tmp/tank.key + # After install: install-box.sh copies to /etc/zfs/tank.key and updates keylocation + encryption = "aes-256-gcm"; + keyformat = "raw"; + keylocation = "file:///tmp/tank.key"; + "com.sun:auto-snapshot" = "false"; + }; + # Don't mount the pool root directly + mountpoint = null; + + datasets = { + # /nix is on the NVMe ext4 root, not on ZFS + # This simplifies boot dependencies - ZFS is purely for data storage + + # Parent dataset for all data - inherits encryption + data = { + type = "zfs_fs"; + options.mountpoint = "none"; + }; + + # Media datasets + "data/media" = { + type = "zfs_fs"; + options.mountpoint = "none"; + }; + "data/media/music" = { + type = "zfs_fs"; + mountpoint = "/tank/media/music"; + options.recordsize = "1M"; # Large files benefit from larger recordsize + }; + "data/media/photos" = { + type = "zfs_fs"; + mountpoint = "/tank/media/photos"; + options.recordsize = "1M"; + }; + "data/media/movies" = { + type = "zfs_fs"; + mountpoint = "/tank/media/movies"; + options.recordsize = "1M"; + }; + "data/media/tv" = { + type = "zfs_fs"; + mountpoint = "/tank/media/tv"; + options.recordsize = "1M"; + }; + + # Other data + "data/books" = { + type = "zfs_fs"; + mountpoint = "/tank/books"; + options."com.sun:auto-snapshot" = "true"; + }; + "data/podcasts" = { + type = "zfs_fs"; + mountpoint = "/tank/podcasts"; + }; + "data/postgres" = { + type = "zfs_fs"; + mountpoint = "/tank/postgres"; + options = { + recordsize = "16K"; # Better for databases + "com.sun:auto-snapshot" = "true"; + }; + }; + "data/syncthing" = { + type = "zfs_fs"; + options.mountpoint = "none"; + options."com.sun:auto-snapshot" = "true"; + }; + "data/syncthing/drawing" = { + type = "zfs_fs"; + mountpoint = "/tank/syncthing/drawing"; + }; + "data/backup" = { + type = "zfs_fs"; + mountpoint = "/tank/backup"; + options."com.sun:auto-snapshot" = "true"; + }; + "data/ftp" = { + type = "zfs_fs"; + mountpoint = "/tank/ftp"; + }; + "data/cache" = { + type = "zfs_fs"; + mountpoint = "/tank/cache"; + options."com.sun:auto-snapshot" = "false"; + }; + "data/playlists" = { + type = "zfs_fs"; + mountpoint = "/tank/playlists"; + }; + "data/new-music" = { + type = "zfs_fs"; + mountpoint = "/tank/new-music"; + }; + "data/archive" = { + type = "zfs_fs"; + mountpoint = "/tank/archive"; + options."com.sun:auto-snapshot" = "true"; + }; + }; + }; + }; + }; +} diff --git a/hosts/box/hardware-configuration.nix b/hosts/box/hardware-configuration.nix index 4d64723..8eb92a9 100644 --- a/hosts/box/hardware-configuration.nix +++ b/hosts/box/hardware-configuration.nix @@ -1,66 +1,34 @@ -# Do not modify this file! It was generated by ‘nixos-generate-config’ -# and may be overwritten by future invocations. Please make changes -# to /etc/nixos/configuration.nix instead. -{ config, lib, pkgs, modulesPath, ... }: +# Hardware configuration for box NAS +# Filesystem mounts are handled by disko.nix +{ + config, + lib, + pkgs, + modulesPath, + ... +}: { - imports = - [ - (modulesPath + "/installer/scan/not-detected.nix") - ]; + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; - boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "thunderbolt" "uas" "usb_storage" "sd_mod" ]; - boot.initrd.kernelModules = [ "dm-snapshot" ]; + boot.initrd.availableKernelModules = [ + "xhci_pci" + "ahci" + "nvme" + "thunderbolt" + "uas" + "usb_storage" + "sd_mod" + ]; + boot.initrd.kernelModules = [ ]; boot.kernelModules = [ "kvm-intel" ]; boot.extraModulePackages = [ ]; - fileSystems."/" = - { - device = "/dev/disk/by-uuid/ade0752d-84d3-4e39-865b-9027ba2d5c67"; - fsType = "ext4"; - }; + # Filesystems are managed by disko - do not define them here - fileSystems."/boot/efi" = - { - device = "/dev/disk/by-uuid/1715-278E"; - fsType = "vfat"; - }; - - fileSystems."/mnt/one" = - { - device = "/dev/disk/by-uuid/0f857c6e-509d-436f-9e78-bc25f1b0d23b"; - fsType = "ext4"; - options = [ - "noatime" - "nodiratime" - "nofail" - ]; - }; - - fileSystems."/mnt/two" = - { - device = "/dev/disk/by-uuid/5bc894bf-ed87-4c30-aab4-87e154e0cd08"; - fsType = "ext4"; - options = [ - "noatime" - "nodiratime" - "nofail" - ]; - }; - - fileSystems."/mnt/three" = - { - device = "/dev/disk/by-uuid/0be3ded1-9c8b-40aa-94ca-dc2297d5988e"; - fsType = "ext4"; - options = [ - "noatime" - "nodiratime" - "nofail" - ]; - }; - - swapDevices = - [{ device = "/dev/disk/by-uuid/b790abb4-ba5f-4476-8f09-b0fc575414aa"; }]; + swapDevices = [ ]; powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; diff --git a/hosts/curve/default.nix b/hosts/curve/default.nix index 9a304db..4522579 100644 --- a/hosts/curve/default.nix +++ b/hosts/curve/default.nix @@ -1,4 +1,5 @@ -{ self, pkgs, ... }: { +{ self, pkgs, ... }: +{ imports = [ ./configuration.nix ../users/anish @@ -26,7 +27,7 @@ programs.gnupg.agent = { enable = true; pinentryPackage = pkgs.pinentry-rofi; - }; + }; hardware.keyboard.qmk.enable = true; services.udev.packages = with pkgs; [ via ]; @@ -36,34 +37,45 @@ virtualisation.docker.enable = true; virtualisation.docker.storageDriver = "btrfs"; - environment.systemPackages = with pkgs; [ docker-compose via ]; + environment.systemPackages = with pkgs; [ + docker-compose + via + ]; - # Speed up boot by removing dependency on network + # Speed up boot by removing dependency on network systemd = { - targets.network-online.wantedBy = - pkgs.lib.mkForce [ ]; # Normally ["multi-user.target"] - services.NetworkManager-wait-online.wantedBy = - pkgs.lib.mkForce [ ]; # Normally ["network-online.target"] + targets.network-online.wantedBy = pkgs.lib.mkForce [ ]; # Normally ["multi-user.target"] + services.NetworkManager-wait-online.wantedBy = pkgs.lib.mkForce [ ]; # Normally ["network-online.target"] }; - - fileSystems."/mnt/ftp" = { - device = "192.168.1.240:/home/ftp"; + device = "192.168.1.240:/tank/ftp"; fsType = "nfs"; - options = [ "x-systemd.automount" "noauto" "x-systemd.idle-timeout=600" ]; + options = [ + "x-systemd.automount" + "noauto" + "x-systemd.idle-timeout=600" + ]; }; fileSystems."/mnt/tv" = { - device = "192.168.1.240:/mnt/three/tv"; + device = "192.168.1.240:/tank/media/tv"; fsType = "nfs"; - options = [ "x-systemd.automount" "noauto" "x-systemd.idle-timeout=600" ]; + options = [ + "x-systemd.automount" + "noauto" + "x-systemd.idle-timeout=600" + ]; }; fileSystems."/mnt/movies" = { - device = "192.168.1.240:/mnt/three/movies"; + device = "192.168.1.240:/tank/media/movies"; fsType = "nfs"; - options = [ "x-systemd.automount" "noauto" "x-systemd.idle-timeout=600" ]; + options = [ + "x-systemd.automount" + "noauto" + "x-systemd.idle-timeout=600" + ]; }; boot.supportedFilesystems = [ "ntfs" ]; @@ -77,15 +89,23 @@ # lazy enable of ports necessary for KDE connect which is installed via cli home profile (for some reason?) networking.firewall = { - allowedTCPPorts = [ 22 4173 3000 ]; # allow ssh and vibekanban - allowedTCPPortRanges = [{ - from = 1714; - to = 1764; - }]; - allowedUDPPortRanges = [{ - from = 1714; - to = 1764; - }]; + allowedTCPPorts = [ + 22 + 4173 + 3000 + ]; # allow ssh and vibekanban + allowedTCPPortRanges = [ + { + from = 1714; + to = 1764; + } + ]; + allowedUDPPortRanges = [ + { + from = 1714; + to = 1764; + } + ]; }; age.secrets.curve-wg.file = "${self}/secrets/curve-wg.age"; @@ -105,17 +125,26 @@ mossnet.backup = { enable = true; name = "curve"; - paths = [ "/home/anish/usr" "/home/anish/.ssh" "/home/anish/.password-store/" ]; + paths = [ + "/home/anish/usr" + "/home/anish/.ssh" + "/home/anish/.password-store/" + ]; }; - # enable adb + # enable adb # TODO move this (it's for KaiOS WebIDE devShell?) programs.adb.enable = true; #virtualisation.docker.enable = true; boot.blacklistedKernelModules = [ "qcserial" ]; # Used for packer Capsul - users.users.anish.extraGroups = - [ "adbusers" "wheel" "plugdev" "libvertd" "docker" ]; + users.users.anish.extraGroups = [ + "adbusers" + "wheel" + "plugdev" + "libvertd" + "docker" + ]; virtualisation.libvirtd.enable = true; hardware.keyboard.zsa.enable = true; services.udev.extraRules = '' diff --git a/hosts/profiles/archivebox/default.nix b/hosts/profiles/archivebox/default.nix index e5ddaaf..92d070a 100644 --- a/hosts/profiles/archivebox/default.nix +++ b/hosts/profiles/archivebox/default.nix @@ -1,4 +1,10 @@ -{ config, options, lib, pkgs, ... }: +{ + config, + options, + lib, + pkgs, + ... +}: with lib; let # cfg = config.services.archivebox; @@ -7,9 +13,8 @@ let port = "8123"; in { - nixpkgs.config.permittedInsecurePackages = [ - "python3.10-django-3.1.14" - ]; + # Note: permittedInsecurePackages must be set in flake.nix nixpkgsFor config + # if archivebox still requires python3.10-django-3.1.14 services.nginx.virtualHosts."archive.mossnet.lan" = { enableACME = false; @@ -26,7 +31,10 @@ in systemd.services.archivebox-install = { description = "archivebox install service"; wantedBy = [ "multi-user.target" ]; - path = with pkgs; [ coreutils archivebox ]; + path = with pkgs; [ + coreutils + archivebox + ]; serviceConfig = { User = user; @@ -51,7 +59,10 @@ in systemd.services.archivebox-server = { description = "archivebox server service"; wantedBy = [ "multi-user.target" ]; - path = with pkgs; [ coreutils archivebox ]; + path = with pkgs; [ + coreutils + archivebox + ]; serviceConfig = { User = user; diff --git a/hosts/profiles/calibre/default.nix b/hosts/profiles/calibre/default.nix index 4112a2a..5108e41 100644 --- a/hosts/profiles/calibre/default.nix +++ b/hosts/profiles/calibre/default.nix @@ -8,7 +8,7 @@ user = "calibre-server"; group = "calibre-server"; options = { - calibreLibrary = "/data/books"; + calibreLibrary = "/tank/books"; enableBookUploading = true; }; }; @@ -20,7 +20,7 @@ services.calibre-server = { enable = true; - libraries = [ "/data/books" ]; + libraries = [ "/tank/books" ]; # Bug in the module puts this in quotes in the systemd file # user = calibre; # group = calibre; diff --git a/hosts/profiles/gonic/default.nix b/hosts/profiles/gonic/default.nix index e035cbb..2de44a8 100644 --- a/hosts/profiles/gonic/default.nix +++ b/hosts/profiles/gonic/default.nix @@ -1,12 +1,17 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: { environment.systemPackages = [ pkgs.ffmpeg ]; mossnet.gonic.enable = true; mossnet.gonic.settings = '' - music-path /mnt/two/music/ - podcast-path /data/podcasts - cache-path /data/cache - playlists-path /data/playlists + music-path /tank/media/music/ + podcast-path /tank/podcasts + cache-path /tank/cache + playlists-path /tank/playlists ''; mossnet.gonic.user = "gonic"; mossnet.gonic.group = "audio"; diff --git a/hosts/profiles/headphones/default.nix b/hosts/profiles/headphones/default.nix index 7af396d..109aaf5 100644 --- a/hosts/profiles/headphones/default.nix +++ b/hosts/profiles/headphones/default.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: { services.headphones = { enable = true; @@ -6,7 +11,7 @@ port = 8181; user = "headphones"; group = "audio"; - dataDir = "/data/music"; + dataDir = "/tank/media/music"; }; services.nginx.virtualHosts."headphones.mossnet.lan" = { enableACME = false; diff --git a/hosts/profiles/immich/default.nix b/hosts/profiles/immich/default.nix index 5c42310..546c669 100644 --- a/hosts/profiles/immich/default.nix +++ b/hosts/profiles/immich/default.nix @@ -8,7 +8,7 @@ }; host = "0.0.0.0"; port = 8567; - mediaLocation = "/mnt/two/photos"; + mediaLocation = "/tank/media/photos"; openFirewall = true; settings.server.externalDomain = "https://photos.sealight.xyz"; }; diff --git a/hosts/profiles/jellyfin/default.nix b/hosts/profiles/jellyfin/default.nix index c111317..dc85ca6 100644 --- a/hosts/profiles/jellyfin/default.nix +++ b/hosts/profiles/jellyfin/default.nix @@ -1,10 +1,13 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: { # Enable Hardware Acceleration for transcoding - nixpkgs.config.packageOverrides = pkgs: { - vaapiIntel = pkgs.vaapiIntel.override { enableHybridCodec = true; }; - }; - hardware.opengl = { + # Note: vaapiIntel override with enableHybridCodec should be in flake.nix overlay if needed + hardware.graphics = { enable = true; extraPackages = with pkgs; [ intel-media-driver @@ -18,10 +21,22 @@ enable = true; user = "jellyfin"; group = "video"; - openFirewall = true; # only for defaults + openFirewall = true; # only for defaults (8096) }; - networking.firewall.allowedTCPPorts = [ 8181 ]; users.users.jellyfin = { - extraGroups = [ "video" "audio" ]; + extraGroups = [ + "video" + "audio" + ]; + }; + services.nginx = { + enable = true; + virtualHosts = { + "jellyfin.mossnet.lan" = { + forceSSL = false; + enableACME = false; + locations."/".proxyPass = "http://localhost:8096/"; + }; + }; }; } diff --git a/hosts/profiles/monitoring/default.nix b/hosts/profiles/monitoring/default.nix index b68e33b..7f920d4 100644 --- a/hosts/profiles/monitoring/default.nix +++ b/hosts/profiles/monitoring/default.nix @@ -1,4 +1,10 @@ -{ self, config, pkgs, ... }: { +{ + self, + config, + pkgs, + ... +}: +{ age.secrets.nullhex-smtp.file = "${self}/secrets/nullhex-smtp.age"; age.secrets.nullhex-smtp.owner = "grafana"; @@ -25,7 +31,7 @@ }; # nginx reverse proxy - # services.nginx.recommendedProxySettings = true; # Needed for new grafana versions + # services.nginx.recommendedProxySettings = true; # Needed for new grafana versions services.nginx.virtualHosts.${config.services.grafana.settings.server.domain} = { locations."/" = { proxyPass = "http://127.0.0.1:2342"; @@ -33,21 +39,23 @@ }; }; - services.postgresql = { - ensureUsers = [{ + services.postgresql = { + ensureUsers = [ + { name = "grafana"; - # TODO this is deprecated - # Need to translate this to - # systemd.services.postgresql.postStart - # or initialScript - ensurePermissions = { - "ALL TABLES IN SCHEMA public" = "SELECT"; - "DATABASE wallabag" = "CONNECT"; - "DATABASE ulogger" = "CONNECT"; - "DATABASE photoprism" = "CONNECT"; - }; - }]; - }; + } + ]; + }; + + # Grant grafana user read access to databases for monitoring + systemd.services.postgresql.postStart = pkgs.lib.mkAfter '' + $PSQL -tAc "GRANT CONNECT ON DATABASE wallabag TO grafana" 2>/dev/null || true + $PSQL -tAc "GRANT CONNECT ON DATABASE ulogger TO grafana" 2>/dev/null || true + $PSQL -tAc "GRANT CONNECT ON DATABASE photoprism TO grafana" 2>/dev/null || true + $PSQL -d wallabag -tAc "GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana" 2>/dev/null || true + $PSQL -d ulogger -tAc "GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana" 2>/dev/null || true + $PSQL -d photoprism -tAc "GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana" 2>/dev/null || true + ''; services.prometheus = { enable = true; @@ -66,11 +74,15 @@ scrapeConfigs = [ { job_name = "box"; - static_configs = [{ targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ]; }]; + static_configs = [ + { targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ]; } + ]; } { job_name = "dns"; - static_configs = [{ targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}" ]; }]; + static_configs = [ + { targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}" ]; } + ]; } ]; }; diff --git a/hosts/profiles/nfs/default.nix b/hosts/profiles/nfs/default.nix index 4fcdc5c..7ee49f4 100644 --- a/hosts/profiles/nfs/default.nix +++ b/hosts/profiles/nfs/default.nix @@ -12,16 +12,31 @@ statdPort = 4000; extraNfsdConfig = ''''; exports = '' - /home/ftp 192.168.1.0/24(rw) - /mnt/one 192.168.1.0/24(rw) - /mnt/two 192.168.1.0/24(rw,async,no_subtree_check) 10.0.69.0/24(rw,async,no_subtree_check) - /mnt/three 192.168.1.0/24(rw) + /tank/ftp 192.168.1.0/24(rw) + /tank/media/music 192.168.1.0/24(rw,async,no_subtree_check) 10.0.69.0/24(rw,async,no_subtree_check) + /tank/media/photos 192.168.1.0/24(rw,async,no_subtree_check) 10.0.69.0/24(rw,async,no_subtree_check) + /tank/media/movies 192.168.1.0/24(rw) + /tank/media/tv 192.168.1.0/24(rw) ''; }; networking.firewall = { - allowedTCPPorts = [ 111 2049 4000 4001 4002 20048 ]; - allowedUDPPorts = [ 111 2049 4000 4001 4002 20048 ]; + allowedTCPPorts = [ + 111 + 2049 + 4000 + 4001 + 4002 + 20048 + ]; + allowedUDPPorts = [ + 111 + 2049 + 4000 + 4001 + 4002 + 20048 + ]; }; #systemd.services.create-mount-dir = { diff --git a/hosts/profiles/sync/music/get-music.sh b/hosts/profiles/sync/music/get-music.sh index 5d67289..ed1751e 100755 --- a/hosts/profiles/sync/music/get-music.sh +++ b/hosts/profiles/sync/music/get-music.sh @@ -4,9 +4,9 @@ set -euo pipefail REMOTE_HOST="aynish@talos.feralhosting.com" REMOTE_PATH="private/transmission/data/" -LOCAL_PATH="/mnt/two/incoming" -TRACKING_FILE="/mnt/two/incoming/.downloaded_albums" -LOG_FILE="/mnt/two/incoming/download-log" +LOCAL_PATH="/tank/new-music" +TRACKING_FILE="/tank/new-music/.downloaded_albums" +LOG_FILE="/tank/new-music/download-log" # Create tracking file if it doesn't exist touch "$TRACKING_FILE" @@ -47,7 +47,7 @@ while IFS= read -r album; do echo "$(date): Importing $album to beets..." >>"$LOG_FILE" # Set umask to allow group read/write access umask 002 - if beet -p fetchart import -m -l /home/anish/music.log -q -g "$LOCAL_PATH/$album"; then + if beet import -q "$LOCAL_PATH/$album"; then echo "$(date): Successfully imported $album to beets" >>"$LOG_FILE" else echo "$(date): Failed to import $album to beets" >>"$LOG_FILE" diff --git a/hosts/profiles/transmission/default.nix b/hosts/profiles/transmission/default.nix index 0b0ac4b..e574f67 100644 --- a/hosts/profiles/transmission/default.nix +++ b/hosts/profiles/transmission/default.nix @@ -11,12 +11,12 @@ # Normally, I would write this into the homedir with home-manager # And explictly set the dir to be the output of the home-manager location script-torrent-done-filename = pkgs.writeShellScript "beet-import.sh" '' -#!/usr/bin/env bash + #!/usr/bin/env bash -beet -p fetchart import -l /home/anish/music.log -q -g "$TR_TORRENT_DIR" + beet -p fetchart import -l /home/anish/music.log -q -g "$TR_TORRENT_DIR" ''; rpc-url = "/transmission/rpc/"; - download-dir = "/mnt/two/new-music"; + download-dir = "/tank/new-music"; }; }; services.nginx.virtualHosts."transmission.mossnet.lan" = { diff --git a/install-box.sh b/install-box.sh new file mode 100755 index 0000000..ae3ce20 --- /dev/null +++ b/install-box.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Install script for box NAS +# Run this from the NixOS installer after rsync'ing the helm repo +# +# Prerequisites: +# - Boot NixOS installer +# - Enable SSH: passwd && sudo systemctl start sshd +# - rsync helm repo: rsync -avz --exclude='.git' /path/to/helm nixos@:~/ +# +# Usage: +# cd ~/helm +# ./install-box.sh + +# Configuration +FLAKE="$HOME/helm#box" +NVME="/dev/disk/by-id/nvme-CT500P310SSD8_2544543B87C2" + +# ZFS drives - update these if drives change +ZFS1="/dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX32D954A2J7" +ZFS2="/dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX32D95FVZVL" +ZFS3="/dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX42D95M807R" + +echo "=== Box NAS Installation ===" +echo "" +echo "This will install NixOS with:" +echo " - NVMe boot drive: $NVME" +echo " - ZFS RAIDZ1 pool with 3x 4TB drives (~8TB usable)" +echo "" + +# Verify drives exist +echo "Verifying drives..." +for disk in "$NVME" "$ZFS1" "$ZFS2" "$ZFS3"; do + if [[ ! -e "$disk" ]]; then + echo "ERROR: Disk not found: $disk" + echo "Available disks:" + ls -la /dev/disk/by-id/ | grep -E '(nvme|ata)' | grep -v part + exit 1 + fi +done +echo "All drives found." +echo "" + +# Generate ZFS keyfile +echo "Generating ZFS keyfile..." +dd if=/dev/urandom of=/tmp/tank.key bs=32 count=1 2>/dev/null +echo "ZFS keyfile created at /tmp/tank.key" +echo "" + +# Get LUKS password +echo "Enter LUKS password for boot drive encryption:" +read -s LUKS_PASSWORD +echo "" +echo "Confirm LUKS password:" +read -s LUKS_PASSWORD_CONFIRM +echo "" + +if [[ "$LUKS_PASSWORD" != "$LUKS_PASSWORD_CONFIRM" ]]; then + echo "ERROR: Passwords do not match" + exit 1 +fi + +echo -n "$LUKS_PASSWORD" > /tmp/luks-password +echo "LUKS password saved." +echo "" + +# Confirm before proceeding +echo "WARNING: This will DESTROY all data on the following drives:" +echo " - $NVME" +echo " - $ZFS1" +echo " - $ZFS2" +echo " - $ZFS3" +echo "" +read -p "Type 'yes' to continue: " CONFIRM +if [[ "$CONFIRM" != "yes" ]]; then + echo "Aborted." + exit 1 +fi + +echo "" +echo "Running disko-install..." +sudo nix \ + --extra-experimental-features nix-command \ + --extra-experimental-features flakes \ + run 'github:nix-community/disko/latest#disko-install' -- \ + --flake "$FLAKE" \ + --disk nvme "$NVME" \ + --disk zfs1 "$ZFS1" \ + --disk zfs2 "$ZFS2" \ + --disk zfs3 "$ZFS3" + +echo "" +echo "Copying ZFS keyfile to installed system..." +# disko-install mounts the root filesystem at /mnt +if [[ ! -d /mnt/etc ]]; then + echo "ERROR: /mnt/etc does not exist. Is the root filesystem mounted?" + exit 1 +fi +sudo mkdir -p /mnt/etc/zfs +sudo cp /tmp/tank.key /mnt/etc/zfs/tank.key +sudo chmod 000 /mnt/etc/zfs/tank.key + +echo "Updating ZFS keylocation to permanent path..." +# Update keylocation so ZFS looks for the key in the installed system +sudo zfs set keylocation=file:///etc/zfs/tank.key tank + +echo "" +echo "Cleaning up..." +rm -f /tmp/luks-password /tmp/tank.key + +echo "" +echo "=== Installation complete! ===" +echo "" +echo "Next steps:" +echo " 1. Reboot: sudo reboot" +echo " 2. Enter LUKS password at boot prompt" +echo " 3. SSH to box at 192.168.1.240" +echo ""