# LIO iSCSI target service (targetctl) for NixOS HA clusters. # # Provides the targetctl.service that saves/restores LIO configuration from # /etc/target/saveconfig.json. Pacemaker manages this service via its # systemd resource agent (class="systemd" type="targetctl"). # # Why ExecStop is not simply "targetctl save": # targetctl save writes the LIO config to JSON but does NOT remove the LIO # target from the kernel's configfs. As a result, any fileio backing store # that LIO has open (e.g. iscsi-lun.img on an XFS-over-DRBD filesystem) # stays referenced in the kernel. The subsequent XFS umount from the # Filesystem OCF resource then returns EBUSY and either hangs for the full # op-stop timeout or fails outright, blocking the entire failover. # # The ExecStop script here additionally tears down the kernel LIO state # via rtslib_fb after saving, so the backing-store file descriptor is # released and umount succeeds immediately. # # Empty-config guard: # The save step is skipped when no iSCSI targets are currently active. # This prevents the secondary node (where LIO was never started) from # overwriting a valid saveconfig.json with an empty one when Pacemaker # stops the iscsi-target resource as part of a failover or cleanup. { pkgs, ... }: let python3 = pkgs.python3.withPackages (ps: [ ps.rtslib-fb ]); targetctl = "${python3}/bin/targetctl"; targetctlStop = pkgs.writeScript "targetctl-stop" '' #!${python3}/bin/python3 import subprocess, sys import rtslib_fb root = rtslib_fb.RTSRoot() targets = list(root.targets) if targets: subprocess.run( ["${targetctl}", "save", "/etc/target/saveconfig.json"], capture_output=True, ) print(f"saved {len(targets)} iSCSI target(s)") else: print("no active LIO targets — saveconfig.json unchanged") for target in targets: try: for tpg in list(target.tpgs): tpg.enable = False target.delete() except Exception as e: print(f"warn (target): {e}", file=sys.stderr) for so in list(root.storage_objects): try: so.delete() except Exception as e: print(f"warn (backstore): {e}", file=sys.stderr) print("LIO kernel target cleared") ''; in { boot.kernelModules = [ "target_core_mod" "iscsi_target_mod" "target_core_file" "target_core_pscsi" "target_core_user" "configfs" ]; systemd = { mounts = [{ where = "/sys/kernel/config"; what = "configfs"; type = "configfs"; wantedBy = [ "multi-user.target" ]; before = [ "targetctl.service" ]; }]; services.targetctl = { description = "LIO iSCSI target config save/restore"; wantedBy = [ "multi-user.target" ]; after = [ "sys-kernel-config.mount" "network.target" ]; requires = [ "sys-kernel-config.mount" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; ExecStart = "${targetctl} restore /etc/target/saveconfig.json"; ExecStop = "${targetctlStop}"; }; unitConfig.ConditionFileNotEmpty = "/etc/target/saveconfig.json"; }; tmpfiles.rules = [ "d /etc/target 0750 root root -" "f /etc/target/saveconfig.json 0640 root root -" ]; }; environment.systemPackages = [ pkgs.targetcli-fb ]; }