1{ config, lib, pkgs, ... }: with lib;
2let
3 cfg = config.boot.iscsi-initiator;
4in
5{
6 # If you're booting entirely off another machine you may want to add
7 # this snippet to always boot the latest "system" version. It is not
8 # enabled by default in case you have an initrd on a local disk:
9 #
10 # boot.initrd.postMountCommands = ''
11 # ln -sfn /nix/var/nix/profiles/system/init /mnt-root/init
12 # stage2Init=/init
13 # '';
14 #
15 # Note: Theoretically you might want to connect to multiple portals and
16 # log in to multiple targets, however the authors of this module so far
17 # don't have the need or expertise to reasonably implement it. Also,
18 # consider carefully before making your boot chain depend on multiple
19 # machines to be up.
20 options.boot.iscsi-initiator = with types; {
21 name = mkOption {
22 description = lib.mdDoc ''
23 Name of the iSCSI initiator to boot from. Note, booting from iscsi
24 requires networkd based networking.
25 '';
26 default = null;
27 example = "iqn.2020-08.org.linux-iscsi.initiatorhost:example";
28 type = nullOr str;
29 };
30
31 discoverPortal = mkOption {
32 description = lib.mdDoc ''
33 iSCSI portal to boot from.
34 '';
35 default = null;
36 example = "192.168.1.1:3260";
37 type = nullOr str;
38 };
39
40 target = mkOption {
41 description = lib.mdDoc ''
42 Name of the iSCSI target to boot from.
43 '';
44 default = null;
45 example = "iqn.2020-08.org.linux-iscsi.targethost:example";
46 type = nullOr str;
47 };
48
49 logLevel = mkOption {
50 description = lib.mdDoc ''
51 Higher numbers elicits more logs.
52 '';
53 default = 1;
54 example = 8;
55 type = int;
56 };
57
58 loginAll = mkOption {
59 description = lib.mdDoc ''
60 Do not log into a specific target on the portal, but to all that we discover.
61 This overrides setting target.
62 '';
63 type = bool;
64 default = false;
65 };
66
67 extraIscsiCommands = mkOption {
68 description = lib.mdDoc "Extra iscsi commands to run in the initrd.";
69 default = "";
70 type = lines;
71 };
72
73 extraConfig = mkOption {
74 description = lib.mdDoc "Extra lines to append to /etc/iscsid.conf";
75 default = null;
76 type = nullOr lines;
77 };
78
79 extraConfigFile = mkOption {
80 description = lib.mdDoc ''
81 Append an additional file's contents to `/etc/iscsid.conf`. Use a non-store path
82 and store passwords in this file. Note: the file specified here must be available
83 in the initrd, see: `boot.initrd.secrets`.
84 '';
85 default = null;
86 type = nullOr str;
87 };
88 };
89
90 config = mkIf (cfg.name != null) {
91 # The "scripted" networking configuration (ie: non-networkd)
92 # doesn't properly order the start and stop of the interfaces, and the
93 # network interfaces are torn down before unmounting disks. Since this
94 # module is specifically for very-early-boot network mounts, we need
95 # the network to stay on.
96 #
97 # We could probably fix the scripted options to properly order, but I'm
98 # not inclined to invest that time today. Hopefully this gets users far
99 # enough along and they can just use networkd.
100 networking.useNetworkd = true;
101 networking.useDHCP = false; # Required to set useNetworkd = true
102
103 boot.initrd = {
104 network.enable = true;
105
106 # By default, the stage-1 disables the network and resets the interfaces
107 # on startup. Since our startup disks are on the network, we can't let
108 # the network not work.
109 network.flushBeforeStage2 = false;
110
111 kernelModules = [ "iscsi_tcp" ];
112
113 extraUtilsCommands = ''
114 copy_bin_and_libs ${pkgs.openiscsi}/bin/iscsid
115 copy_bin_and_libs ${pkgs.openiscsi}/bin/iscsiadm
116 ${optionalString (!config.boot.initrd.network.ssh.enable) "cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib"}
117
118 mkdir -p $out/etc/iscsi
119 cp ${config.environment.etc.hosts.source} $out/etc/hosts
120 cp ${pkgs.openiscsi}/etc/iscsi/iscsid.conf $out/etc/iscsi/iscsid.fragment.conf
121 chmod +w $out/etc/iscsi/iscsid.fragment.conf
122 cat << 'EOF' >> $out/etc/iscsi/iscsid.fragment.conf
123 ${optionalString (cfg.extraConfig != null) cfg.extraConfig}
124 EOF
125 '';
126
127 extraUtilsCommandsTest = ''
128 $out/bin/iscsiadm --version
129 '';
130
131 preLVMCommands = let
132 extraCfgDumper = optionalString (cfg.extraConfigFile != null) ''
133 if [ -f "${cfg.extraConfigFile}" ]; then
134 printf "\n# The following is from ${cfg.extraConfigFile}:\n"
135 cat "${cfg.extraConfigFile}"
136 else
137 echo "Warning: boot.iscsi-initiator.extraConfigFile ${cfg.extraConfigFile} does not exist!" >&2
138 fi
139 '';
140 in ''
141 ${optionalString (!config.boot.initrd.network.ssh.enable) ''
142 # stolen from initrd-ssh.nix
143 echo 'root:x:0:0:root:/root:/bin/ash' > /etc/passwd
144 echo 'passwd: files' > /etc/nsswitch.conf
145 ''}
146
147 cp -f $extraUtils/etc/hosts /etc/hosts
148
149 mkdir -p /etc/iscsi /run/lock/iscsi
150 echo "InitiatorName=${cfg.name}" > /etc/iscsi/initiatorname.iscsi
151
152 (
153 cat "$extraUtils/etc/iscsi/iscsid.fragment.conf"
154 printf "\n"
155 ${optionalString cfg.loginAll ''echo "node.startup = automatic"''}
156 ${extraCfgDumper}
157 ) > /etc/iscsi/iscsid.conf
158
159 iscsid --foreground --no-pid-file --debug ${toString cfg.logLevel} &
160 iscsiadm --mode discoverydb \
161 --type sendtargets \
162 --discover \
163 --portal ${escapeShellArg cfg.discoverPortal} \
164 --debug ${toString cfg.logLevel}
165
166 ${if cfg.loginAll then ''
167 iscsiadm --mode node --loginall all
168 '' else ''
169 iscsiadm --mode node --targetname ${escapeShellArg cfg.target} --login
170 ''}
171
172 ${cfg.extraIscsiCommands}
173
174 pkill -9 iscsid
175 '';
176 };
177
178 services.openiscsi = {
179 enable = true;
180 inherit (cfg) name;
181 };
182
183 assertions = [
184 {
185 assertion = cfg.loginAll -> cfg.target == null;
186 message = "iSCSI target name is set while login on all portals is enabled.";
187 }
188 {
189 assertion = !config.boot.initrd.systemd.enable;
190 message = "systemd stage 1 does not support iscsi yet.";
191 }
192 ];
193 };
194}