# 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; }];
};
}