Newer
Older
skyworks-Nix-infra / hosts / skydick / datapool.nix
# Seagate Mach2 SAS dual-actuator data pool ("dick")
#
# Each Mach2 drive presents TWO LUNs over SAS. CRITICAL: never place both
# LUNs of the same physical drive in the same mirror vdev — a single drive
# failure would kill the entire vdev.
#
# SAS WWN identification:
#   LUN0: wwn-0x6000c500XXXXXXXX0000000000000000
#   LUN1: wwn-0x6000c500XXXXXXXX0001000000000000
#   Same prefix = same physical drive. Pair DIFFERENT drives in each mirror.
#
# Drive inventory (ST14000NM0001, 14TB SAS Mach2):
#
#   drive1: cab9587b  (serial ZKL09S4X...FA4P)
#   drive2: caf74697  (serial ZKL05VPS...FMAC)
#   drive3: cb3613eb  (serial ZKL05VM0...GSP3)
#   drive4: cb3957ab  (serial ZKL09QXG...50KE)
#   drive5: cb9dd2eb  (serial ZKL0EP06...6LLW)  [spare]
#
# Layout (4 active + 1 hot spare, all mirrors for expandability):
#
#   mirror-0:  drive1-LUN0  drive2-LUN0
#   mirror-1:  drive1-LUN1  drive2-LUN1
#   mirror-2:  drive3-LUN0  drive4-LUN0
#   mirror-3:  drive3-LUN1  drive4-LUN1
#   spare:     drive5-LUN0  drive5-LUN1
#
# If drive1 fails: mirror-0 and mirror-1 each lose one member, but stay
# online via drive2. The spare auto-replaces one degraded member.
#
# === Pool creation ===
#
#   zpool create -o ashift=12 -o autotrim=on -o failmode=continue \
#     -O compression=zstd -O relatime=on \
#     -O xattr=sa -O acltype=posixacl -O dnodesize=auto \
#     -O normalization=formD -O redundant_metadata=most \
#     -O mountpoint=none -O canmount=off \
#     dick \
#     mirror wwn-0x6000c500cab9587b0000000000000000 wwn-0x6000c500caf746970000000000000000 \
#     mirror wwn-0x6000c500cab9587b0001000000000000 wwn-0x6000c500caf746970001000000000000 \
#     mirror wwn-0x6000c500cb3613eb0000000000000000 wwn-0x6000c500cb3957ab0000000000000000 \
#     mirror wwn-0x6000c500cb3613eb0001000000000000 wwn-0x6000c500cb3957ab0001000000000000 \
#     spare wwn-0x6000c500cb9dd2eb0000000000000000 wwn-0x6000c500cb9dd2eb0001000000000000
#
# === NVMe acceleration (Intel DC P4600 750GB, PHKE0163008K750BGN) ===
#
#   Partition:
#     sgdisk -Z /dev/disk/by-id/nvme-INTEL_SSDPE21K750GAC_PHKE0163008K750BGN
#     sgdisk -n 1:0:+8G -t 1:bf01 /dev/disk/by-id/nvme-INTEL_SSDPE21K750GAC_PHKE0163008K750BGN
#     sgdisk -n 2:0:0   -t 2:bf01 /dev/disk/by-id/nvme-INTEL_SSDPE21K750GAC_PHKE0163008K750BGN
#
#   Add to pool:
#     zpool add dick \
#       log nvme-INTEL_SSDPE21K750GAC_PHKE0163008K750BGN-part1 \
#       cache nvme-INTEL_SSDPE21K750GAC_PHKE0163008K750BGN-part2
#
# === Dataset hierarchy ===
#
#   SHARED                          mount                    recsize  compress  purpose
#   dick/public                     /srv/public              128K     zstd      collaborative shared files
#   dick/media                      /srv/media               1M       off       shared media (one hardlink domain)
#     → /srv/media/data             (dir)                                       torrent payload (*arr downloads)
#     → /srv/media/library          (dir)                                       organized media (hardlinked from data/)
#
#   PER-USER (template — shown for ldx UID=1000,GID=100; repeat per user)
#   dick/users                      (canmount=off)                              namespace root
#   dick/users/ldx                  /srv/users/ldx           —        —         quota boundary
#   dick/users/ldx/files            /srv/users/ldx/files     128K     zstd      personal files
#   dick/users/ldx/bt-state         /srv/users/ldx/bt-state  16K      zstd      .torrent, resume, *arr DBs
#   dick/users/ldx/vm               /srv/users/ldx/vm        64K      zstd      VM filesystem root / parent for zvol children
#
#   SYSTEM
#   dick/system                     (canmount=off)                              namespace root
#   dick/system/backup              /srv/system/backup       1M       zstd-3    archival backups
#   dick/system/vm                  /srv/system/vm           64K      zstd      central VM filesystem root / parent for zvol children
#   dick/templates/vm               /srv/templates/vm        64K      zstd      shared read-only VM base images
#
# Design rule: dataset boundary = hardlink domain = quota/tuning domain.
#   dick/media keeps payload (data/) and library in ONE dataset so *arr
#   hardlinks work.  Per-user trees hold private state only, not payload —
#   avoids duplicate media across users.  One writer stack (qbittorrent +
#   *arr) manages dick/media; other users get read-only access.
#
# === Dataset creation ===
#
# Shared:
#   zfs create -o mountpoint=/srv/public -o recordsize=128K dick/public
#   chown root:storage /srv/public && chmod 2775 /srv/public
#
#   zfs create -o mountpoint=/srv/media -o recordsize=1M -o compression=off dick/media
#   mkdir -p /srv/media/{data,library}
#   chown qbittorrent:storage /srv/media/{data,library}
#
# Per-user namespace:
#   zfs create -o mountpoint=none -o canmount=off dick/users
#
#   # ldx (UID 1000, primary GID 100 = users)
#   zfs create -o mountpoint=/srv/users/ldx  -o quota=10T                       dick/users/ldx
#   zfs create -o recordsize=128K -o mountpoint=/srv/users/ldx/files            dick/users/ldx/files
#   zfs create -o recordsize=16K  -o mountpoint=/srv/users/ldx/bt-state         dick/users/ldx/bt-state
#   zfs create -o recordsize=64K  -o mountpoint=/srv/users/ldx/vm              dick/users/ldx/vm
#   mkdir -p /srv/users/ldx/vm/files
#   chown ldx:users /srv/users/ldx && chmod 0700 /srv/users/ldx
#   for d in files bt-state vm vm/files; do chown ldx:users /srv/users/ldx/$d && chmod 0750 /srv/users/ldx/$d; done
#   # File-backed VM images live under /srv/users/ldx/vm/files.
#   # Block LUNs are zvol children of dick/users/ldx/vm/<name>.
#
#   # ye-lw21 (local+LDAP account, local UID 1002/GID 100 on skydick)
#   zfs create -o mountpoint=/srv/users/ye-lw21  -o quota=10T                   dick/users/ye-lw21
#   zfs create -o recordsize=128K -o mountpoint=/srv/users/ye-lw21/files        dick/users/ye-lw21/files
#   zfs create -o recordsize=16K  -o mountpoint=/srv/users/ye-lw21/bt-state     dick/users/ye-lw21/bt-state
#   zfs create -o recordsize=64K  -o mountpoint=/srv/users/ye-lw21/vm           dick/users/ye-lw21/vm
#   mkdir -p /srv/users/ye-lw21/vm/files
#   chown ye-lw21:users /srv/users/ye-lw21 && chmod 0700 /srv/users/ye-lw21
#   for d in files bt-state vm vm/files; do chown ye-lw21:users /srv/users/ye-lw21/$d && chmod 0750 /srv/users/ye-lw21/$d; done
#   # File-backed VM images live under /srv/users/ye-lw21/vm/files.
#   # Block LUNs are zvol children of dick/users/ye-lw21/vm/<name>.
#
# System:
#   zfs create -o mountpoint=none -o canmount=off                               dick/system
#   zfs create -o recordsize=1M -o compression=zstd-3 -o mountpoint=/srv/system/backup dick/system/backup
#   zfs create -o recordsize=64K -o mountpoint=/srv/system/vm                   dick/system/vm
#   mkdir -p /srv/system/vm/files
#   zfs create -o recordsize=64K -o readonly=on -o mountpoint=/srv/templates/vm dick/templates/vm
#   chown root:root /srv/system/{backup,vm} /srv/templates/vm && chmod 0700 /srv/system/{backup,vm}
#   chown root:root /srv/system/vm/files && chmod 0700 /srv/system/vm/files
#   # File-backed VM images live under /srv/system/vm/files.
#   # Block LUNs are zvol children of dick/system/vm/<name>.
#
# iSCSI zvols (block service — never the same bytes as SMB/NFS):
#   zfs create -V <size> -o volblocksize=16K dick/users/<user>/vm/<name>
#   zfs create -V <size> -o volblocksize=16K dick/system/vm/<name>
#
# === Expanding the pool ===
#
# Add another pair of Mach2 drives (drive6 + drive7):
#   zpool add dick \
#     mirror <drive6-LUN0> <drive7-LUN0> \
#     mirror <drive6-LUN1> <drive7-LUN1>
#
# === Service model ===
#
# File services (SMB + NFS share the same filesystem datasets):
#   Public:   root:storage 2775, NFS root_squash, Samba [public] @storage
#   Media:    qbittorrent:storage, NFS rw /srv/media all_squash(900),
#             NFS reader /srv/media/library ro, Samba [media] ro @storage
#   Home:     <user>:<user> 0700, explicit per-user NFS exports, Samba [homes]
#   BT-state: <user>:<user> 0750, NFS all_squash(uid), no Samba
#   VM files: <user>:<user> 0750, NFS all_squash(uid), no Samba
#
# Block services (iSCSI — separate zvols, never shared with SMB/NFS):
#   dick/users/<user>/vm/<name>  — user-owned zvols
#   dick/system/vm/<name>        — centrally managed zvols
#
# Quotas:
#   ZFS quota on dick/users/<user> caps total across all child datasets.
#   dick/media is shared — no per-user quota; manage via service-level controls.
#
# Auth:
#   NFS all_squash provides UID mapping, not authentication.  Per-user NFS here
#   uses one explicit export per user, which is acceptable for a small fixed set
#   but scales linearly with user count and client ACL maintenance.
#   Samba [homes] valid users = %S gives real per-user auth via LDAP-backed
#   Samba accounts (ldapsam).  The intended model is LDAP-only SMB users; if a
#   same-name local account exists on skydick, local NSS still wins for the
#   final Unix authorization step.  For stronger NFS isolation: use sec=krb5 or
#   tighter per-client IP restrictions.

{ config, pkgs, ... }:

{
  # Keep a fixed local GID for on-disk ownership and the qbittorrent service
  # account. Shared-user membership policy is carried by the matching LDAP
  # posixGroup cn=storage,ou=posix_groups,dc=skyw,dc=top.
  users.groups.storage = {
    gid = 997;
  };

  # Service account for the shared media writer (qbittorrent + *arr stack)
  users.users.qbittorrent = {
    uid = 900;
    group = "storage";
    isSystemUser = true;
    shell = "/run/current-system/sw/bin/nologin";
  };

  systemd.tmpfiles.rules = [
    "d /srv 0755 root root -"

    # Shared
    "d /srv/public 2775 root storage -"
    "d /srv/media 2775 root storage -"
    "d /srv/media/data 2775 qbittorrent storage -"
    "d /srv/media/library 2775 qbittorrent storage -"

    # Per-user trees
    "d /srv/users 0755 root root -"
    "d /srv/users/ldx 0700 ldx users -"
    "d /srv/users/ldx/files 0750 ldx users -"
    "d /srv/users/ldx/bt-state 0750 ldx users -"
    "d /srv/users/ldx/vm 0750 ldx users -"
    "d /srv/users/ldx/vm/files 0750 ldx users -"
    "d /srv/users/ye-lw21 0700 ye-lw21 users -"
    "d /srv/users/ye-lw21/files 0750 ye-lw21 users -"
    "d /srv/users/ye-lw21/bt-state 0750 ye-lw21 users -"
    "d /srv/users/ye-lw21/vm 0750 ye-lw21 users -"
    "d /srv/users/ye-lw21/vm/files 0750 ye-lw21 users -"

    # System
    "d /srv/system 0700 root root -"
    "d /srv/system/backup 0700 root root -"
    "d /srv/system/vm 0700 root root -"
    "d /srv/system/vm/files 0700 root root -"
    "d /srv/templates 0755 root root -"
    "d /srv/templates/vm 0755 root root -"

  ];

  systemd.services.samba-ldap-admin-password = {
    description = "Seed Samba LDAP admin password into secrets.tdb";
    wants = [ "network-online.target" ];
    after = [ "network-online.target" ];
    before = [ "samba-nmbd.service" "samba-winbindd.service" "samba-smbd.service" ];
    requiredBy = [ "samba-nmbd.service" "samba-winbindd.service" "samba-smbd.service" ];
    restartTriggers = [ config.environment.etc."samba/smb.conf".source ];

    serviceConfig = {
      Type = "oneshot";
      RemainAfterExit = true;
      ConditionPathExists = config.age.secrets.skydick-samba-ldap-admin.path;
    };

    script = ''
      set -euo pipefail
      password="$(${pkgs.coreutils}/bin/cat ${config.age.secrets.skydick-samba-ldap-admin.path})"
      ${pkgs.coreutils}/bin/printf '%s\n%s\n' "$password" "$password" | ${config.services.samba.package}/bin/smbpasswd -s -W
    '';
  };

  # NFS — primary protocol for all datasets
  services.rpcbind.enable = true;

  services.nfs.server = {
    enable = true;
    nproc = 64;
    statdPort = 20001;
    lockdPort = 20002;
    mountdPort = 20003;

    exports = ''
      /srv                10.0.0.0/16(rw,sync,fsid=0,crossmnt,no_subtree_check,root_squash)

      # Shared
      /srv/public         10.0.0.0/16(rw,sync,no_subtree_check,root_squash)
      /srv/media          10.0.0.0/16(rw,sync,no_subtree_check,all_squash,anonuid=900,anongid=997)
      /srv/media/library  10.0.0.0/16(ro,sync,no_subtree_check,root_squash)

      # Per-user — explicit exports; all_squash maps every client UID to the owner
      /srv/users/ldx      10.0.0.0/16(rw,sync,no_subtree_check,all_squash,anonuid=1000,anongid=100)
      /srv/users/ye-lw21  10.0.0.0/16(rw,sync,no_subtree_check,all_squash,anonuid=1002,anongid=100)

      # System
      /srv/system/backup  10.0.0.0/16(rw,sync,no_subtree_check,no_root_squash)
      /srv/system/vm      10.0.0.0/16(rw,sync,no_subtree_check,no_root_squash)
      /srv/templates/vm   10.0.0.0/16(ro,sync,no_subtree_check,root_squash)

    '';
  };

  services.nfs.idmapd.settings = {
    General.Domain = "skydick.local";
    Mapping = {
      Nobody-User = "nobody";
      Nobody-Group = "nogroup";
    };
  };

  # Samba — file-level access (SMB + NFS share the same datasets)
  services.samba = {
    enable = true;
    package = pkgs.sambaFull;
    openFirewall = false;

    settings = {
      global = {
        workgroup = "WORKGROUP";
        "server string" = "Skydick Storage";
        "netbios name" = "SKYDICK";
        security = "user";
        "passdb backend" = "ldapsam:ldap://10.0.0.1";
        "ldap admin dn" = "cn=admin,dc=skyw,dc=top";
        "ldap suffix" = "dc=skyw,dc=top";
        "ldap user suffix" = "ou=people";
        "ldap group suffix" = "ou=posix_groups";
        "ldap machine suffix" = "ou=machines";
        "ldap delete dn" = "no";
        "ldap passwd sync" = "only";
        "ldap ssl" = "off";
        "ldap server require strong auth" = "no";
        "ldap connection timeout" = "5";
        "ldap timeout" = "5";
        "hosts allow" = "10.0. 127.";
        "hosts deny" = "ALL";

        "socket options" = "TCP_NODELAY IPTOS_LOWDELAY SO_RCVBUF=131072 SO_SNDBUF=131072";
        "use sendfile" = "yes";
        "aio read size" = "16384";
        "aio write size" = "16384";

        "map to guest" = "never";
        "server min protocol" = "SMB2_10";

        "load printers" = "no";
      };

      # Shared datasets
      public = {
        path = "/srv/public";
        browseable = "yes";
        "read only" = "no";
        "guest ok" = "no";
        "valid users" = "@storage";
        "create mask" = "0664";
        "directory mask" = "2775";
      };

      media = {
        path = "/srv/media/library";
        browseable = "yes";
        "read only" = "yes";
        "valid users" = "@storage";
      };

      # Per-user homes — Samba auto-creates \\SKYDICK\<user> from this template
      homes = {
        path = "/srv/users/%S/files";
        browseable = "no";
        "read only" = "no";
        "valid users" = "%S";
        "create mask" = "0640";
        "directory mask" = "0750";
      };
    };
  };

  services.samba-wsdd = {
    enable = true;
    openFirewall = false;
  };

  # iSCSI — vm zvols only
  services.target.enable = true;

  # Firewall: storage service ports
  networking.firewall = {
    allowedTCPPorts = [
      111   # RPC (NFS)
      2049  # NFS
      445   # SMB
      139   # NetBIOS (SMB)
      3260  # iSCSI
    ];
    allowedUDPPorts = [
      111   # RPC (NFS)
      2049  # NFS (NFSv4.1+)
      137   # NetBIOS Name Service
      138   # NetBIOS Datagram
    ];
    allowedTCPPortRanges = [{ from = 20000; to = 20005; }];
    allowedUDPPortRanges = [{ from = 20000; to = 20005; }];
  };
}