1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8# openafsMod, openafsBin, mkCellServDB
9with import ./lib.nix { inherit config lib pkgs; };
10
11let
12 inherit (lib)
13 getBin
14 literalExpression
15 mkOption
16 mkIf
17 optionalString
18 singleton
19 types
20 ;
21
22 cfg = config.services.openafsClient;
23
24 cellServDB = pkgs.fetchurl {
25 url = "http://dl.central.org/dl/cellservdb/CellServDB.2018-05-14";
26 sha256 = "1wmjn6mmyy2r8p10nlbdzs4nrqxy8a9pjyrdciy5nmppg4053rk2";
27 };
28
29 clientServDB = pkgs.writeText "client-cellServDB-${cfg.cellName}" (
30 mkCellServDB cfg.cellName cfg.cellServDB
31 );
32
33 afsConfig = pkgs.runCommand "afsconfig" { preferLocalBuild = true; } ''
34 mkdir -p $out
35 echo ${cfg.cellName} > $out/ThisCell
36 cat ${cellServDB} ${clientServDB} > $out/CellServDB
37 echo "${cfg.mountPoint}:${cfg.cache.directory}:${toString cfg.cache.blocks}" > $out/cacheinfo
38 '';
39
40in
41{
42 ###### interface
43
44 options = {
45
46 services.openafsClient = {
47
48 enable = mkOption {
49 default = false;
50 type = types.bool;
51 description = "Whether to enable the OpenAFS client.";
52 };
53
54 afsdb = mkOption {
55 default = true;
56 type = types.bool;
57 description = "Resolve cells via AFSDB DNS records.";
58 };
59
60 cellName = mkOption {
61 default = "";
62 type = types.str;
63 description = "Cell name.";
64 example = "grand.central.org";
65 };
66
67 cellServDB = mkOption {
68 default = [ ];
69 type =
70 with types;
71 listOf (submodule {
72 options = cellServDBConfig;
73 });
74 description = ''
75 This cell's database server records, added to the global
76 CellServDB. See {manpage}`CellServDB(5)` man page for syntax. Ignored when
77 `afsdb` is set to `true`.
78 '';
79 example = [
80 {
81 ip = "1.2.3.4";
82 dnsname = "first.afsdb.server.dns.fqdn.org";
83 }
84 {
85 ip = "2.3.4.5";
86 dnsname = "second.afsdb.server.dns.fqdn.org";
87 }
88 ];
89 };
90
91 cache = {
92 blocks = mkOption {
93 default = 100000;
94 type = types.int;
95 description = "Cache size in 1KB blocks.";
96 };
97
98 chunksize = mkOption {
99 default = 0;
100 type = types.ints.between 0 30;
101 description = ''
102 Size of each cache chunk given in powers of
103 2. `0` resets the chunk size to its default
104 values (13 (8 KB) for memcache, 18-20 (256 KB to 1 MB) for
105 diskcache). Maximum value is 30. Important performance
106 parameter. Set to higher values when dealing with large files.
107 '';
108 };
109
110 directory = mkOption {
111 default = "/var/cache/openafs";
112 type = types.str;
113 description = "Cache directory.";
114 };
115
116 diskless = mkOption {
117 default = false;
118 type = types.bool;
119 description = ''
120 Use in-memory cache for diskless machines. Has no real
121 performance benefit anymore.
122 '';
123 };
124 };
125
126 crypt = mkOption {
127 default = true;
128 type = types.bool;
129 description = "Whether to enable (weak) protocol encryption.";
130 };
131
132 daemons = mkOption {
133 default = 2;
134 type = types.int;
135 description = ''
136 Number of daemons to serve user requests. Numbers higher than 6
137 usually do no increase performance. Default is sufficient for up
138 to five concurrent users.
139 '';
140 };
141
142 fakestat = mkOption {
143 default = false;
144 type = types.bool;
145 description = ''
146 Return fake data on stat() calls. If `true`,
147 always do so. If `false`, only do so for
148 cross-cell mounts (as these are potentially expensive).
149 '';
150 };
151
152 inumcalc = mkOption {
153 default = "compat";
154 type = types.strMatching "compat|md5";
155 description = ''
156 Inode calculation method. `compat` is
157 computationally less expensive, but `md5` greatly
158 reduces the likelihood of inode collisions in larger scenarios
159 involving multiple cells mounted into one AFS space.
160 '';
161 };
162
163 mountPoint = mkOption {
164 default = "/afs";
165 type = types.str;
166 description = ''
167 Mountpoint of the AFS file tree, conventionally
168 `/afs`. When set to a different value, only
169 cross-cells that use the same value can be accessed.
170 '';
171 };
172
173 packages = {
174 module = mkOption {
175 default = config.boot.kernelPackages.openafs;
176 defaultText = literalExpression "config.boot.kernelPackages.openafs";
177 type = types.package;
178 description = "OpenAFS kernel module package. MUST match the userland package!";
179 };
180 programs = mkOption {
181 default = getBin pkgs.openafs;
182 defaultText = literalExpression "getBin pkgs.openafs";
183 type = types.package;
184 description = "OpenAFS programs package. MUST match the kernel module package!";
185 };
186 };
187
188 sparse = mkOption {
189 default = true;
190 type = types.bool;
191 description = "Minimal cell list in /afs.";
192 };
193
194 startDisconnected = mkOption {
195 default = false;
196 type = types.bool;
197 description = ''
198 Start up in disconnected mode. You need to execute
199 `fs disco online` (as root) to switch to
200 connected mode. Useful for roaming devices.
201 '';
202 };
203
204 };
205 };
206
207 ###### implementation
208
209 config = mkIf cfg.enable {
210
211 assertions = [
212 {
213 assertion = cfg.afsdb || cfg.cellServDB != [ ];
214 message = "You should specify all cell-local database servers in config.services.openafsClient.cellServDB or set config.services.openafsClient.afsdb.";
215 }
216 {
217 assertion = cfg.cellName != "";
218 message = "You must specify the local cell name in config.services.openafsClient.cellName.";
219 }
220 ];
221
222 environment.systemPackages = [ openafsBin ];
223
224 environment.etc = {
225 clientCellServDB = {
226 source = pkgs.runCommand "CellServDB" { preferLocalBuild = true; } ''
227 cat ${cellServDB} ${clientServDB} > $out
228 '';
229 target = "openafs/CellServDB";
230 mode = "0644";
231 };
232 clientCell = {
233 text = ''
234 ${cfg.cellName}
235 '';
236 target = "openafs/ThisCell";
237 mode = "0644";
238 };
239 };
240
241 systemd.services.afsd = {
242 description = "AFS client";
243 wantedBy = [ "multi-user.target" ];
244 wants = lib.optional (!cfg.startDisconnected) "network-online.target";
245 after = singleton (if cfg.startDisconnected then "network.target" else "network-online.target");
246 serviceConfig = {
247 RemainAfterExit = true;
248 };
249 restartIfChanged = false;
250
251 preStart = ''
252 mkdir -p -m 0755 ${cfg.mountPoint}
253 mkdir -m 0700 -p ${cfg.cache.directory}
254 ${pkgs.kmod}/bin/insmod ${openafsMod}/lib/modules/*/extra/openafs/libafs.ko.xz
255 ${openafsBin}/sbin/afsd \
256 -mountdir ${cfg.mountPoint} \
257 -confdir ${afsConfig} \
258 ${optionalString (!cfg.cache.diskless) "-cachedir ${cfg.cache.directory}"} \
259 -blocks ${toString cfg.cache.blocks} \
260 -chunksize ${toString cfg.cache.chunksize} \
261 ${optionalString cfg.cache.diskless "-memcache"} \
262 -inumcalc ${cfg.inumcalc} \
263 ${if cfg.fakestat then "-fakestat-all" else "-fakestat"} \
264 ${if cfg.sparse then "-dynroot-sparse" else "-dynroot"} \
265 ${optionalString cfg.afsdb "-afsdb"}
266 ${openafsBin}/bin/fs setcrypt ${if cfg.crypt then "on" else "off"}
267 ${optionalString cfg.startDisconnected "${openafsBin}/bin/fs discon offline"}
268 '';
269
270 # Doing this in preStop, because after these commands AFS is basically
271 # stopped, so systemd has nothing to do, just noticing it. If done in
272 # postStop, then we get a hang + kernel oops, because AFS can't be
273 # stopped simply by sending signals to processes.
274 preStop = ''
275 ${pkgs.util-linux}/bin/umount ${cfg.mountPoint}
276 ${openafsBin}/sbin/afsd -shutdown
277 ${pkgs.kmod}/sbin/rmmod libafs
278 '';
279 };
280 };
281}