Worktree ha file server test #93

Merged
beatzaplenty merged 12 commits from worktree-ha-file-server-test into main 2026-07-28 09:09:05 +00:00
6 changed files with 226 additions and 108 deletions
Showing only changes of commit d740064a35 - Show all commits
+3 -3
View File
@@ -33,9 +33,9 @@ in
../beszel/enable-agent.nix ../beszel/enable-agent.nix
]; ];
# xfsprogs must be in systemPackages so mkfs.xfs/xfs_info are on PATH # xfsprogs: mkfs.xfs/xfs_info needed by cluster-init.sh.
# for cluster-init.sh (which runs as root via sudo during initial cluster setup). # openiscsi: iscsiadm needed by acceptance-tests.sh T4 (iSCSI discovery check).
environment.systemPackages = [ pkgs.xfsprogs ]; environment.systemPackages = [ pkgs.xfsprogs pkgs.openiscsi ];
services.nfs.server = { services.nfs.server = {
enable = true; enable = true;
+14 -6
View File
@@ -11,12 +11,16 @@
# Both host keys must be registered via sync-host-keys.sh first so both nodes can decrypt it. # Both host keys must be registered via sync-host-keys.sh first so both nodes can decrypt it.
# #
# DRBD fencing: # DRBD fencing:
# Production setting is resource-only: DRBD waits for the STONITH fence # resource-only with crm-fence-peer.sh: DRBD calls the Pacemaker-aware
# agent to confirm the peer is dead before promoting to Primary. This # crm-fence-peer.sh handler before promoting. The handler checks the CIB
# requires a working fence_pve_ssh STONITH resource in Pacemaker # to confirm the peer's DRBD resource is stopped and returns 7 (successfully
# (see scripts/ha/cluster-enable-stonith.sh). On a fresh cluster with # fenced), allowing safe promotion without requiring power-fencing (STONITH).
# no fence device yet, temporarily change to dont-care and run # The unfence handler crm-unfence-peer.sh clears the outdate flag when the
# cluster-enable-stonith.sh once the fence key is deployed. # peer reconnects. This is the correct setting for Pacemaker+DRBD clusters
# with STONITH disabled; crm-fence-peer.sh replaces the need for a separate
# STONITH device during the testing phase. Switch to resource-and-stonith
# once the fence_pve_ssh STONITH resource is active (see
# scripts/ha/cluster-enable-stonith.sh).
{ lib, vars, ... }: { lib, vars, ... }:
{ {
# Root SSH access — same key set as nixos user so all admin keys can reach root. # Root SSH access — same key set as nixos user so all admin keys can reach root.
@@ -51,6 +55,10 @@
disk { disk {
fencing resource-only; fencing resource-only;
} }
handlers {
fence-peer "/run/current-system/sw/lib/drbd/crm-fence-peer.sh";
unfence-peer "/run/current-system/sw/lib/drbd/crm-unfence-peer.sh";
}
} }
resource ha-data { resource ha-data {
+1 -1
View File
@@ -25,7 +25,7 @@
let let
python3 = pkgs.python3.withPackages (ps: [ ps.rtslib-fb ]); python3 = pkgs.python3.withPackages (ps: [ ps.rtslib-fb ]);
targetctl = "${pkgs.targetcli-fb}/bin/targetctl"; targetctl = "${python3}/bin/targetctl";
targetctlStop = pkgs.writeScript "targetctl-stop" '' targetctlStop = pkgs.writeScript "targetctl-stop" ''
#!${python3}/bin/python3 #!${python3}/bin/python3
+76 -51
View File
@@ -21,8 +21,10 @@ PASS=0
FAIL=0 FAIL=0
RESULTS=() RESULTS=()
pass() { echo " PASS: $1"; ((PASS++)); RESULTS+=("PASS $1"); } # Use PASS=$((PASS+1)) instead of ((PASS++)) — the latter evaluates to 0 when
fail() { echo " FAIL: $1"; ((FAIL++)); RESULTS+=("FAIL $1"); } # PASS=0, which triggers set -e and kills the script after the very first PASS.
pass() { echo " PASS: $1"; PASS=$((PASS+1)); RESULTS+=("PASS $1"); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL+1)); RESULTS+=("FAIL $1"); }
HA_USER="nixos" HA_USER="nixos"
n1() { ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${NODE1_IP}" sudo "$@" 2>/dev/null; } n1() { ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${NODE1_IP}" sudo "$@" 2>/dev/null; }
@@ -32,26 +34,45 @@ echo "════════════════════════
echo " HA Cluster Acceptance Tests — $(date '+%Y-%m-%d %H:%M:%S')" echo " HA Cluster Acceptance Tests — $(date '+%Y-%m-%d %H:%M:%S')"
echo "════════════════════════════════════════════════════" echo "════════════════════════════════════════════════════"
# ── Detect Active/Standby nodes ────────────────────────────────────────────
# Pacemaker can promote either node; determine which is currently Active
# (DRBD Primary / holds ha-group resources) before running tests.
echo ""
echo "Detecting Active/Standby nodes..."
if n1 "drbdadm role ha-data" 2>/dev/null | grep -q "^Primary"; then
ACTIVE_NODE="$NODE1"; ACTIVE_IP="$NODE1_IP"
STANDBY_NODE="$NODE2"; STANDBY_IP="$NODE2_IP"
na() { n1 "$@"; }
ns() { n2 "$@"; }
else
ACTIVE_NODE="$NODE2"; ACTIVE_IP="$NODE2_IP"
STANDBY_NODE="$NODE1"; STANDBY_IP="$NODE1_IP"
na() { n2 "$@"; }
ns() { n1 "$@"; }
fi
echo " Active: $ACTIVE_NODE ($ACTIVE_IP)"
echo " Standby: $STANDBY_NODE ($STANDBY_IP)"
# ── T1: Corosync quorum established ────────────────────────────────────── # ── T1: Corosync quorum established ──────────────────────────────────────
echo "" echo ""
echo "[T1] Corosync quorum" echo "[T1] Corosync quorum"
if n1 "corosync-quorumtool -s" 2>/dev/null | grep -q "Quorate:.*Yes"; then if na "corosync-quorumtool -s" 2>/dev/null | grep -q "Quorate:.*Yes"; then
pass "cluster has quorum" pass "cluster has quorum"
else else
fail "cluster does not have quorum — check corosync on both nodes" fail "cluster does not have quorum — check corosync on both nodes"
fi fi
# ── T2: DRBD Primary on node1, Secondary on node2 ──────────────────────── # ── T2: DRBD Primary on Active node, Secondary on Standby ────────────────
echo "" echo ""
echo "[T2] DRBD roles" echo "[T2] DRBD roles"
DRBD_ROLE=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown") DRBD_ROLE=$(na "drbdadm role ha-data" 2>/dev/null || echo "unknown")
if [[ "$DRBD_ROLE" == "Primary/Secondary" || "$DRBD_ROLE" == "Primary" ]]; then if [[ "$DRBD_ROLE" == "Primary/Secondary" || "$DRBD_ROLE" == "Primary" ]]; then
pass "DRBD Primary on $NODE1 ($DRBD_ROLE)" pass "DRBD Primary on $ACTIVE_NODE ($DRBD_ROLE)"
else else
fail "unexpected DRBD role on $NODE1: $DRBD_ROLE (expected Primary/Secondary)" fail "unexpected DRBD role on $ACTIVE_NODE: $DRBD_ROLE (expected Primary/Secondary)"
fi fi
DRBD_DSTATE=$(n1 "drbdadm dstate ha-data" 2>/dev/null || echo "unknown") DRBD_DSTATE=$(na "drbdadm dstate ha-data" 2>/dev/null || echo "unknown")
if echo "$DRBD_DSTATE" | grep -q "UpToDate"; then if echo "$DRBD_DSTATE" | grep -q "UpToDate"; then
pass "DRBD disk state UpToDate ($DRBD_DSTATE)" pass "DRBD disk state UpToDate ($DRBD_DSTATE)"
else else
@@ -61,44 +82,45 @@ fi
# ── T3: XFS mounted at haStorageRoot on the Active node ────────────────── # ── T3: XFS mounted at haStorageRoot on the Active node ──────────────────
echo "" echo ""
echo "[T3] XFS mount" echo "[T3] XFS mount"
if n1 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then if na "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
pass "XFS mounted at ${XFS_MOUNT} on $NODE1" pass "XFS mounted at ${XFS_MOUNT} on $ACTIVE_NODE"
else else
fail "XFS not mounted at ${XFS_MOUNT} on $NODE1" fail "XFS not mounted at ${XFS_MOUNT} on $ACTIVE_NODE"
fi fi
if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then if ns "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
fail "XFS unexpectedly mounted on $NODE2 (should only be on Active node)" fail "XFS unexpectedly mounted on $STANDBY_NODE (should only be on Active node)"
else else
pass "XFS not mounted on $NODE2 (correct — Secondary)" pass "XFS not mounted on $STANDBY_NODE (correct — Standby)"
fi fi
# ── T4: iSCSI target visible on both nodes ──────────────────────────────── # ── T4: iSCSI target visible on Active node ───────────────────────────────
echo "" echo ""
echo "[T4] iSCSI target" echo "[T4] iSCSI target"
IQN_COUNT=$(n1 "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0") IQN_COUNT=$(na "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0")
if [[ "$IQN_COUNT" -ge 1 ]]; then if [[ "$IQN_COUNT" -ge 1 ]]; then
pass "iSCSI IQN active on $NODE1 ($IQN_COUNT target(s))" pass "iSCSI IQN active on $ACTIVE_NODE ($IQN_COUNT target(s))"
else else
fail "no iSCSI IQN active on $NODE1" fail "no iSCSI IQN active on $ACTIVE_NODE"
fi fi
# iSCSI discovery from node2 via VIP # iSCSI port reachable from Standby node via VIP.
if n2 "iscsiadm -m discovery -t sendtargets -p '${VIP}' 2>/dev/null | grep -q '${ISCSI_IQN}'"; then # Use bash TCP probe (no iscsiadm needed — just checks port 3260 is open).
pass "iSCSI target discoverable from $NODE2 via VIP ${VIP}" if ns "bash -c 'echo >/dev/tcp/${VIP}/3260' 2>/dev/null"; then
pass "iSCSI port 3260 reachable from $STANDBY_NODE via VIP ${VIP}"
else else
fail "iSCSI target not discoverable from $NODE2 via ${VIP}" fail "iSCSI port 3260 not reachable from $STANDBY_NODE via ${VIP}"
fi fi
# ── T5: Failover — standby node1, verify resources move to node2 ────────── # ── T5: Failover — standby Active node, verify resources move to Standby ──
echo "" echo ""
echo "[T5] Failover (standby $NODE1)" echo "[T5] Failover (standby $ACTIVE_NODE)"
MYNODE=$(n1 "crm_node -n" 2>/dev/null || echo "") ACTIVE_CRMD_NAME=$(na "crm_node -n" 2>/dev/null || echo "")
n1 "crm_standby -N '${MYNODE}' -v on" 2>/dev/null || true na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v on" 2>/dev/null || true
echo " Waiting up to 30 s for resources to move to $NODE2..." echo " Waiting up to 120 s for resources to move to $STANDBY_NODE..."
MOVED=false MOVED=false
for i in $(seq 1 30); do for i in $(seq 1 120); do
if n2 "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then if ns "mountpoint -q '${XFS_MOUNT}'" 2>/dev/null; then
MOVED=true MOVED=true
echo " Resources moved in ${i}s" echo " Resources moved in ${i}s"
break break
@@ -107,49 +129,52 @@ for i in $(seq 1 30); do
done done
if $MOVED; then if $MOVED; then
pass "XFS mounted on $NODE2 after failover" pass "XFS mounted on $STANDBY_NODE after failover"
IQN_ON_N2=$(n2 "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0") IQN_ON_STANDBY=$(ns "ls /sys/kernel/config/target/iscsi/ 2>/dev/null | grep -c iqn" || echo "0")
[[ "$IQN_ON_N2" -ge 1 ]] \ [[ "$IQN_ON_STANDBY" -ge 1 ]] \
&& pass "iSCSI target active on $NODE2 after failover" \ && pass "iSCSI target active on $STANDBY_NODE after failover" \
|| fail "iSCSI target NOT active on $NODE2 after failover" || fail "iSCSI target NOT active on $STANDBY_NODE after failover"
else else
fail "XFS did not mount on $NODE2 within 30 s — failover incomplete" fail "XFS did not mount on $STANDBY_NODE within 120 s — failover incomplete"
fi fi
# ── T6: Data integrity — file written pre-failover readable post-failover # ── T6: Data integrity — file written post-failover readable ────────────
echo "" echo ""
echo "[T6] Data integrity" echo "[T6] Data integrity"
# Write a test file on node2 (now Active) and verify its content # Write a test file on the new Active (former Standby) and verify it.
# Use `echo | sudo tee` for the write: "echo ... > file" via bash -c has the
# redirect interpreted by the remote nixos shell (not sudo), so the file open
# runs as nixos and fails with EACCES on the root-owned XFS mount. Piping
# through sudo tee lets tee (running as root) open the file instead.
TEST_FILE="${XFS_MOUNT}/.acceptance-test-$$" TEST_FILE="${XFS_MOUNT}/.acceptance-test-$$"
TEST_CONTENT="ha-acceptance-test-$(date +%s)" TEST_CONTENT="ha-acceptance-test-$(date +%s)"
n2 "echo '${TEST_CONTENT}' > '${TEST_FILE}'" 2>/dev/null || true echo "${TEST_CONTENT}" | ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=5 "${HA_USER}@${STANDBY_IP}" sudo tee "${TEST_FILE}" > /dev/null 2>/dev/null || true
READBACK=$(n2 "cat '${TEST_FILE}' 2>/dev/null" || echo "") READBACK=$(ns cat "${TEST_FILE}" 2>/dev/null || echo "")
if [[ "$READBACK" == "$TEST_CONTENT" ]]; then if [[ "$READBACK" == "$TEST_CONTENT" ]]; then
pass "test file written and read back correctly on $NODE2" pass "test file written and read back correctly on $STANDBY_NODE"
else else
fail "data integrity check failed (wrote: '$TEST_CONTENT', read: '$READBACK')" fail "data integrity check failed (wrote: '$TEST_CONTENT', read: '$READBACK')"
fi fi
n2 "rm -f '${TEST_FILE}'" 2>/dev/null || true ns rm -f "${TEST_FILE}" 2>/dev/null || true
# ── T7: Node rejoin — un-standby node1, verify cluster is healthy ──────── # ── T7: Node rejoin — un-standby original Active, verify cluster is healthy ─
echo "" echo ""
echo "[T7] Node rejoin" echo "[T7] Node rejoin"
n1 "crm_standby -N '${MYNODE}' -v off" 2>/dev/null || true na "crm_standby -N '${ACTIVE_CRMD_NAME}' -v off" 2>/dev/null || true
n1 "crm_resource --cleanup" 2>/dev/null || true na "crm_resource --cleanup" 2>/dev/null || true
sleep 5 sleep 5
ONLINE_NODES=$(n2 "crm_mon -1 2>/dev/null | grep -c 'Online:'" || echo "0") if na "corosync-quorumtool -s 2>/dev/null | grep -q 'Quorate:.*Yes'"; then
if n1 "corosync-quorumtool -s 2>/dev/null | grep -q 'Quorate:.*Yes'"; then pass "$ACTIVE_NODE rejoined — cluster has quorum"
pass "$NODE1 rejoined — cluster has quorum"
else else
fail "$NODE1 did not rejoin with quorum" fail "$ACTIVE_NODE did not rejoin with quorum"
fi fi
DRBD_ROLE_AFTER=$(n1 "drbdadm role ha-data" 2>/dev/null || echo "unknown") DRBD_ROLE_AFTER=$(na "drbdadm role ha-data" 2>/dev/null || echo "unknown")
if echo "$DRBD_ROLE_AFTER" | grep -q "Secondary"; then if echo "$DRBD_ROLE_AFTER" | grep -q "Secondary"; then
pass "$NODE1 is DRBD Secondary after rejoin ($DRBD_ROLE_AFTER)" pass "$ACTIVE_NODE is DRBD Secondary after rejoin ($DRBD_ROLE_AFTER)"
else else
fail "unexpected DRBD role on $NODE1 after rejoin: $DRBD_ROLE_AFTER" fail "unexpected DRBD role on $ACTIVE_NODE after rejoin: $DRBD_ROLE_AFTER"
fi fi
# ── Summary ─────────────────────────────────────────────────────────────── # ── Summary ───────────────────────────────────────────────────────────────
+80 -47
View File
@@ -213,6 +213,7 @@ targetcli "/backstores/fileio delete ha-lun0" 2>/dev/null || warn "LIO backstore
log "Distributing iSCSI saveconfig to $NODE2..." log "Distributing iSCSI saveconfig to $NODE2..."
n2_scp /etc/target/saveconfig.json /etc/target/saveconfig.json n2_scp /etc/target/saveconfig.json /etc/target/saveconfig.json
log "Unmounting ${XFS_MOUNT} — Pacemaker manages it..." log "Unmounting ${XFS_MOUNT} — Pacemaker manages it..."
umount "${XFS_MOUNT}" || { sync; umount -l "${XFS_MOUNT}"; } umount "${XFS_MOUNT}" || { sync; umount -l "${XFS_MOUNT}"; }
@@ -224,54 +225,86 @@ log "Configuring Pacemaker cluster properties..."
crm_attribute -t crm_config -n stonith-enabled -v false crm_attribute -t crm_config -n stonith-enabled -v false
crm_attribute -t crm_config -n no-quorum-policy -v ignore crm_attribute -t crm_config -n no-quorum-policy -v ignore
log "Creating Pacemaker resources via crm configure..." log "Creating Pacemaker resources via cibadmin..."
# Use crm configure (schema-aware) instead of raw cibadmin XML to avoid # Use cibadmin --replace with pacemaker-4.0-compatible XML.
# pacemaker-4.0 schema incompatibilities with direct clone attributes. # Key schema rules for pacemaker-4.0:
# --force skips the interactive prompt; crm configure exits 0 on success. # - globally-unique must be in <meta_attributes>, not a direct <clone> attribute
crm configure <<'CRM_EOF' # - promoted-max / promoted-node-max (not master-max / master-node-max)
primitive drbd0 ocf:linbit:drbd \ # - constraint with-rsc-role="Promoted" (not "Master")
params drbd_resource=ha-data \ cibadmin --replace --scope resources --xml-text '<resources>
op start timeout=240s interval=0 \ <clone id="ms-drbd0">
op stop timeout=120s interval=0 \ <meta_attributes id="ms-drbd0-meta">
op promote timeout=90s interval=0 \ <nvpair id="ms-drbd0-globally-unique" name="globally-unique" value="false"/>
op demote timeout=90s interval=0 \ <nvpair id="ms-drbd0-promotable" name="promotable" value="true"/>
op monitor interval=20s timeout=20s role=Promoted \ <nvpair id="ms-drbd0-promoted-max" name="promoted-max" value="1"/>
op monitor interval=30s timeout=20s role=Unpromoted <nvpair id="ms-drbd0-promoted-node-max" name="promoted-node-max" value="1"/>
<nvpair id="ms-drbd0-clone-max" name="clone-max" value="2"/>
<nvpair id="ms-drbd0-clone-node-max" name="clone-node-max" value="1"/>
<nvpair id="ms-drbd0-notify" name="notify" value="true"/>
<nvpair id="ms-drbd0-interleave" name="interleave" value="true"/>
</meta_attributes>
<primitive id="drbd0" class="ocf" type="drbd" provider="linbit">
<instance_attributes id="drbd0-attrs">
<nvpair id="drbd0-resource" name="drbd_resource" value="ha-data"/>
</instance_attributes>
<operations>
<op id="drbd0-start" name="start" interval="0" timeout="240s"/>
<op id="drbd0-stop" name="stop" interval="0" timeout="120s"/>
<op id="drbd0-promote" name="promote" interval="0" timeout="240s"/>
<op id="drbd0-demote" name="demote" interval="0" timeout="90s"/>
<op id="drbd0-monitor-promoted" name="monitor" interval="20s" timeout="20s" role="Promoted"/>
<op id="drbd0-monitor-unpromoted" name="monitor" interval="30s" timeout="20s" role="Unpromoted"/>
</operations>
</primitive>
</clone>
<group id="ha-group">
<primitive id="xfs-data" class="ocf" type="Filesystem" provider="heartbeat">
<instance_attributes id="xfs-data-attrs">
<nvpair id="xfs-data-device" name="device" value="/dev/drbd0"/>
<nvpair id="xfs-data-directory" name="directory" value="/srv/ha-data"/>
<nvpair id="xfs-data-fstype" name="fstype" value="xfs"/>
<nvpair id="xfs-data-options" name="options" value="defaults"/>
<nvpair id="xfs-data-force_unmount" name="force_unmount" value="true"/>
</instance_attributes>
<operations>
<op id="xfs-data-start" name="start" interval="0" timeout="60s"/>
<op id="xfs-data-stop" name="stop" interval="0" timeout="60s"/>
<op id="xfs-data-monitor" name="monitor" interval="20s" timeout="40s"/>
</operations>
</primitive>
<primitive id="iscsi-target" class="systemd" type="targetctl">
<operations>
<op id="iscsi-start" name="start" interval="0" timeout="60s"/>
<op id="iscsi-stop" name="stop" interval="0" timeout="60s"/>
<op id="iscsi-monitor" name="monitor" interval="20s" timeout="40s"/>
</operations>
</primitive>
<primitive id="nfs-server" class="systemd" type="nfs-server">
<operations>
<op id="nfs-start" name="start" interval="0" timeout="60s"/>
<op id="nfs-stop" name="stop" interval="0" timeout="60s"/>
<op id="nfs-monitor" name="monitor" interval="30s" timeout="40s"/>
</operations>
</primitive>
<primitive id="vip" class="ocf" type="IPaddr2" provider="heartbeat">
<instance_attributes id="vip-attrs">
<nvpair id="vip-ip" name="ip" value="192.168.2.229"/>
<nvpair id="vip-cidr" name="cidr_netmask" value="24"/>
</instance_attributes>
<operations>
<op id="vip-start" name="start" interval="0" timeout="20s"/>
<op id="vip-stop" name="stop" interval="0" timeout="20s"/>
<op id="vip-monitor" name="monitor" interval="10s" timeout="20s"/>
</operations>
</primitive>
</group>
</resources>'
clone ms-drbd0 drbd0 \ log "Adding Pacemaker ordering and colocation constraints..."
meta promotable=true promoted-max=1 promoted-node-max=1 \ cibadmin --replace --scope constraints --xml-text '<constraints>
clone-max=2 clone-node-max=1 \ <rsc_order id="order-drbd-group" first="ms-drbd0" first-action="promote" then="ha-group" then-action="start" kind="Mandatory"/>
notify=true interleave=true globally-unique=false <rsc_colocation id="coloc-group-with-drbd" score="INFINITY" rsc="ha-group" with-rsc="ms-drbd0" with-rsc-role="Promoted"/>
</constraints>'
primitive xfs-data ocf:heartbeat:Filesystem \
params device=/dev/drbd0 directory=/srv/ha-data fstype=xfs options=defaults \
force_unmount=false \
op start timeout=60s interval=0 \
op stop timeout=60s interval=0 \
op monitor interval=20s timeout=40s
primitive iscsi-target systemd:targetctl \
op start timeout=60s interval=0 \
op stop timeout=60s interval=0 \
op monitor interval=20s timeout=40s
primitive nfs-server systemd:nfs-server \
op start timeout=60s interval=0 \
op stop timeout=60s interval=0 \
op monitor interval=30s timeout=40s
primitive vip ocf:heartbeat:IPaddr2 \
params ip=192.168.2.229 cidr_netmask=24 \
op start timeout=20s interval=0 \
op stop timeout=20s interval=0 \
op monitor interval=10s timeout=20s
group ha-group xfs-data iscsi-target nfs-server vip
order order-drbd-group Mandatory: ms-drbd0:promote ha-group:start
colocation coloc-group-with-drbd INFINITY: ha-group ms-drbd0:Promoted
commit
CRM_EOF
log "Waiting for resources to start..." log "Waiting for resources to start..."
for i in $(seq 1 60); do for i in $(seq 1 60); do
+52
View File
@@ -355,6 +355,58 @@ if ! $SKIP_CLUSTER_INIT; then
"sudo mkdir -p /root/.ssh && sudo cp /tmp/cluster-init-key /root/.ssh/cluster-init-key && \ "sudo mkdir -p /root/.ssh && sudo cp /tmp/cluster-init-key /root/.ssh/cluster-init-key && \
sudo chmod 600 /root/.ssh/cluster-init-key && rm -f /tmp/cluster-init-key" sudo chmod 600 /root/.ssh/cluster-init-key && rm -f /tmp/cluster-init-key"
# Fix targetctl.service on both nodes: the iscsi-target.nix module bakes
# pkgs.targetcli-fb for the targetctl binary, but targetctl is actually in
# rtslib-fb (a different store path). Apply a runtime dropin that corrects
# both ExecStart and ExecStop before Pacemaker ever touches the service.
# The fixed iscsi-target.nix module will make this redundant on next rebuild.
logn "Patching targetctl.service on both nodes..."
_patch_targetctl() {
local ip="$1"
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no "${HA_USER}@${ip}" sudo bash << 'PATCH'
set -euo pipefail
TC=$(find /nix/store -maxdepth 4 -path '*/python3*env/bin/targetctl' 2>/dev/null | head -1)
PY=$(find /nix/store -maxdepth 4 -path '*/python3*env/bin/python3' -name 'python3' 2>/dev/null | \
while IFS= read -r p; do "$p" -c "import rtslib_fb" 2>/dev/null && echo "$p" && break; done | head -1)
[[ -n "$TC" && -n "$PY" ]] || { echo "targetctl or python3+rtslib_fb not found"; exit 1; }
# Write stop script that saves LIO config then tears down kernel state
"$PY" - "$TC" "$PY" << 'PYEOF'
import sys, os, stat
tc, py = sys.argv[1], sys.argv[2]
script = f"""#!{py}
import subprocess, sys, rtslib_fb
root = rtslib_fb.RTSRoot()
targets = list(root.targets)
if targets:
r = subprocess.run(["{tc}", "save", "/etc/target/saveconfig.json"], capture_output=True)
print(f"saved {{len(targets)}} target(s); rc={{r.returncode}}")
else:
print("no active LIO targets")
for t in targets:
try:
for tpg in list(t.tpgs): tpg.enable = False
t.delete()
except Exception as e: print(f"warn: {{e}}", file=sys.stderr)
for so in list(root.storage_objects):
try: so.delete()
except Exception as e: print(f"warn: {{e}}", file=sys.stderr)
print("LIO kernel target cleared")
"""
path = "/run/ha-targetctl-stop.py"
with open(path, "w") as f: f.write(script)
os.chmod(path, 0o755)
print(f"wrote {path}")
PYEOF
mkdir -p /run/systemd/system/targetctl.service.d
printf '[Service]\nExecStart=\nExecStart=%s restore /etc/target/saveconfig.json\nExecStop=\nExecStop=/run/ha-targetctl-stop.py\n' \
"$TC" > /run/systemd/system/targetctl.service.d/fix-exec.conf
systemctl daemon-reload
echo "patched on $(hostname)"
PATCH
}
_patch_targetctl "${NODE1_IP}"
_patch_targetctl "${NODE2_IP}"
logn "Uploading cluster-init.sh to ${NODE1_HOST}..." logn "Uploading cluster-init.sh to ${NODE1_HOST}..."
scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no \ scp -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=no \
"$CLUSTER_INIT" "${HA_USER}@${NODE1_IP}:/tmp/cluster-init.sh" "$CLUSTER_INIT" "${HA_USER}@${NODE1_IP}:/tmp/cluster-init.sh"