Archived
All 7 acceptance tests pass on live NixOS 25.11 VMs (VMIDs 200/201 on
pve1). Failover completes in ~5 s with data integrity verified.
modules/ha/pacemaker-stack.nix — fixes four NixOS-specific breakages:
- systemd StateDirectory resets /var/lib/pacemaker to root:root; removed
and replaced with ExecStartPre to create/chown dirs as hacluster
- HA_SBIN_DIR points to a non-existent Nix store path; overridden to
/run/current-system/sw/bin so crm_master resolves correctly
- OCF agents need an explicit broad PATH (iproute2, util-linux, xfsprogs,
drbd, bash, etc.) — NixOS services have no implicit PATH
- FUSER=true bypasses the psmisc fuser check_binary call in the
Filesystem OCF agent (psmisc not installed on minimal hosts)
modules/ha/iscsi-target.nix — LIO iSCSI target via targetctl with a
Python/rtslib_fb ExecStop that explicitly clears the kernel LIO state
(not just saves JSON), so the XFS backing store's file descriptor is
released before umount — preventing EBUSY stop timeouts on failover.
Includes an empty-config guard so the secondary node never overwrites
the primary's saveconfig.json with an empty one.
test-lab/ha/common.nix — updated to import both modules, use fencing
dont-care (no STONITH in test lab), omit LVM handlers (non-existent on
NixOS paths), and merge repeated services/networking attr sets to satisfy
statix W20. test-lab/ha/acceptance-tests.sh — final v4 with crm_standby
fix (pacemaker 3.x API).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HaH1cSGvhogRP5ExoF6nD8
164 lines
5.5 KiB
Nix
164 lines
5.5 KiB
Nix
# Shared HA stack config for both test nodes.
|
|
# These are throwaway test VMs — not production hosts.
|
|
# No sops-nix, no clan, no home-manager.
|
|
{ lib, pkgs, vars, ... }:
|
|
|
|
let
|
|
node1Ip = "192.168.2.200";
|
|
node2Ip = "192.168.2.201";
|
|
drbdPort = 7789;
|
|
|
|
# Test-only corosync authkey (128 bytes = 1024 bits minimum for corosync).
|
|
# Not secret — this is a disposable test cluster, not production.
|
|
testAuthKey = "ha-test-cluster-auth-key-NOT-FOR-PRODUCTION-use-corosync-keygen-for-real-clusters-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
|
in
|
|
{
|
|
system.stateVersion = "26.05";
|
|
|
|
# ── Hardware (Proxmox VM) ──────────────────────────────────────────────
|
|
imports = [
|
|
../../modules/hardware-configuration/vm/proxmox.nix
|
|
../../modules/boot/efi.nix
|
|
../../modules/ha/pacemaker-stack.nix
|
|
../../modules/ha/iscsi-target.nix
|
|
];
|
|
|
|
# ── Nix settings ──────────────────────────────────────────────────────
|
|
nix.settings.experimental-features = [ "nix-command" "flakes" ];
|
|
|
|
users.users.root.openssh.authorizedKeys.keys = [
|
|
vars.adminSshKey
|
|
# Claude Code session key (this machine) — test-lab only
|
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGygkCljN6uKpdJbHTOQtn8ZnH+wKXDLAwrDFbLrE/65 nixos@nixos"
|
|
];
|
|
|
|
# ── Services ──────────────────────────────────────────────────────────
|
|
services = {
|
|
openssh = {
|
|
enable = true;
|
|
settings.PermitRootLogin = "yes";
|
|
};
|
|
|
|
# Allow QEMU guest exec for key injection fallback
|
|
qemuGuest.enable = true;
|
|
|
|
drbd = {
|
|
enable = true;
|
|
config = ''
|
|
global {
|
|
usage-count yes;
|
|
}
|
|
|
|
common {
|
|
net {
|
|
protocol C;
|
|
ping-int 1;
|
|
verify-alg sha256;
|
|
after-sb-0pri discard-zero-changes;
|
|
after-sb-1pri discard-secondary;
|
|
}
|
|
disk {
|
|
# dont-care: DRBD itself won't fence before promoting. Production
|
|
# clusters should use resource-only here and configure a STONITH
|
|
# fence agent (e.g. fence_pve_ssh) in Pacemaker so DRBD can safely
|
|
# protect against split-brain without risking dual-Primary.
|
|
# For this test cluster (no fence device) dont-care lets promotion
|
|
# proceed; the DRBD kernel module still refuses dual-Primary without
|
|
# allow-two-primaries in net {}.
|
|
fencing dont-care;
|
|
# LVM before/after-resync-target handlers omitted: the LVM snapshot
|
|
# scripts (/usr/lib/drbd/snapshot-resync-target-lvm.sh) don't exist
|
|
# on NixOS paths. If present, DRBD calls them on resync and exits 127,
|
|
# dropping the peer connection and leaving the secondary Outdated.
|
|
}
|
|
}
|
|
|
|
resource ha-data {
|
|
volume 0 {
|
|
device /dev/drbd0;
|
|
disk /dev/sdb; # scsi1 in Proxmox VM → sdb
|
|
meta-disk internal;
|
|
}
|
|
|
|
on ha-test-node1 {
|
|
address ${node1Ip}:${toString drbdPort};
|
|
}
|
|
|
|
on ha-test-node2 {
|
|
address ${node2Ip}:${toString drbdPort};
|
|
}
|
|
}
|
|
'';
|
|
};
|
|
|
|
# services.corosync.enable = true is set by modules/ha/pacemaker-stack.nix
|
|
corosync = {
|
|
clusterName = "ha-test";
|
|
nodelist = [
|
|
{ nodeid = 1; name = "ha-test-node1"; ring_addrs = [ node1Ip ]; }
|
|
{ nodeid = 2; name = "ha-test-node2"; ring_addrs = [ node2Ip ]; }
|
|
];
|
|
};
|
|
};
|
|
|
|
# Corosync authkey (test-only, not secret — generated with
|
|
# `corosync-keygen` for production).
|
|
environment.etc."corosync/authkey" = {
|
|
source = builtins.toFile "authkey" testAuthKey;
|
|
mode = "0400";
|
|
};
|
|
|
|
# ── Packages ──────────────────────────────────────────────────────────
|
|
# corosync, pacemaker, ocf-resource-agents, targetcli-fb already added
|
|
# by the ha/ modules; add the remaining stack-specific tools here.
|
|
environment.systemPackages = with pkgs; [
|
|
# Storage
|
|
drbd # drbdadm, drbdsetup, drbdmon
|
|
xfsprogs # mkfs.xfs, xfs_admin, xfs_info
|
|
|
|
# Networking / debug
|
|
iproute2 # ip, ss
|
|
iputils # ping
|
|
tcpdump
|
|
lsof
|
|
|
|
# Scripting / config
|
|
python3
|
|
curl
|
|
jq
|
|
vim
|
|
htop
|
|
];
|
|
|
|
# ── Networking ────────────────────────────────────────────────────────
|
|
networking = {
|
|
useDHCP = false;
|
|
defaultGateway = "192.168.2.1";
|
|
nameservers = [ "192.168.2.1" "8.8.8.8" ];
|
|
|
|
firewall = {
|
|
enable = true;
|
|
allowedTCPPorts = [
|
|
22 # SSH
|
|
3260 # iSCSI
|
|
3121 # pacemaker-remoted
|
|
2224 # pcsd
|
|
drbdPort
|
|
];
|
|
allowedUDPPorts = [
|
|
5404 # corosync
|
|
5405 # corosync
|
|
5407 # corosync crypto
|
|
];
|
|
extraCommands = ''
|
|
iptables -A INPUT -s ${node1Ip}/32 -j ACCEPT
|
|
iptables -A INPUT -s ${node2Ip}/32 -j ACCEPT
|
|
'';
|
|
};
|
|
};
|
|
|
|
# ── Locale / time ─────────────────────────────────────────────────────
|
|
time.timeZone = vars.timeZone;
|
|
i18n.defaultLocale = "en_AU.UTF-8";
|
|
}
|