1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9let
10 cfg = config.services.waagent;
11
12 # Format for waagent.conf
13 settingsFormat = {
14 type =
15 with types;
16 let
17 singleAtom =
18 (nullOr (oneOf [
19 bool
20 str
21 int
22 float
23 ]))
24 // {
25 description = "atom (bool, string, int or float) or null";
26 };
27 atom = either singleAtom (listOf singleAtom) // {
28 description = singleAtom.description + " or a list of them";
29 };
30 in
31 attrsOf (
32 either atom (attrsOf atom)
33 // {
34 description = atom.description + " or an attribute set of them";
35 }
36 );
37 generate =
38 name: value:
39 let
40 # Transform non-attribute values
41 transform =
42 x:
43 # Transform bool to "y" or "n"
44 if (isBool x) then
45 (if x then "y" else "n")
46 # Concatenate list items with comma
47 else if (isList x) then
48 concatStringsSep "," (map transform x)
49 else
50 toString x;
51
52 # Convert to format of waagent.conf
53 recurse =
54 path: value:
55 if builtins.isAttrs value then
56 pipe value [
57 (mapAttrsToList (k: v: recurse (path ++ [ k ]) v))
58 concatLists
59 ]
60 else
61 [
62 {
63 name = concatStringsSep "." path;
64 inherit value;
65 }
66 ];
67 convert =
68 attrs:
69 pipe (recurse [ ] attrs) [
70 # Filter out null values and emoty lists
71 (filter (kv: kv.value != null && kv.value != [ ]))
72 # Transform to Key=Value form, then concatenate
73 (map (kv: "${kv.name}=${transform kv.value}"))
74 (concatStringsSep "\n")
75 ];
76 in
77 pkgs.writeText name (convert value);
78 };
79
80 settingsType = types.submodule {
81 freeformType = settingsFormat.type;
82 options = {
83 Provisioning = {
84 Enable = mkOption {
85 type = types.bool;
86 default = !config.services.cloud-init.enable;
87 defaultText = literalExpression "!config.services.cloud-init.enable";
88 description = ''
89 Whether to enable provisioning functionality in the agent.
90
91 If provisioning is disabled, SSH host and user keys in the image are preserved
92 and configuration in the Azure provisioning API is ignored.
93
94 Set to `false` if cloud-init is used for provisioning tasks.
95 '';
96 };
97
98 Agent = mkOption {
99 type = types.enum [
100 "auto"
101 "waagent"
102 "cloud-init"
103 "disabled"
104 ];
105 default = "auto";
106 description = ''
107 Which provisioning agent to use.
108 '';
109 };
110 };
111
112 ResourceDisk = {
113 Format = mkOption {
114 type = types.bool;
115 default = false;
116 description = ''
117 If set to `true`, waagent formats and mounts the resource disk that the platform provides,
118 unless the file system type in `ResourceDisk.FileSystem` is set to `ntfs`.
119 The agent makes a single Linux partition (ID 83) available on the disk.
120 This partition isn't formatted if it can be successfully mounted.
121
122 This configuration has no effect if resource disk is managed by cloud-init.
123 '';
124 };
125
126 FileSystem = mkOption {
127 type = types.str;
128 default = "ext4";
129 description = ''
130 The file system type for the resource disk.
131 If the string is `X`, then `mkfs.X` should be present in the environment.
132 You can add additional filesystem packages using `services.waagent.extraPackages`.
133
134 This configuration has no effect if resource disk is managed by cloud-init.
135 '';
136 };
137
138 MountPoint = mkOption {
139 type = types.str;
140 default = "/mnt/resource";
141 description = ''
142 This option specifies the path at which the resource disk is mounted.
143 The resource disk is a temporary disk and might be emptied when the VM is deprovisioned.
144
145 This configuration has no effect if resource disk is managed by cloud-init.
146 '';
147 };
148
149 MountOptions = mkOption {
150 type = with types; listOf str;
151 default = [ ];
152 example = [
153 "nodev"
154 "nosuid"
155 ];
156 description = ''
157 This option specifies disk mount options to be passed to the `mount -o` command.
158 For more information, see the {manpage}`mount(8)` manual page.
159 '';
160 };
161
162 EnableSwap = mkOption {
163 type = types.bool;
164 default = false;
165 description = ''
166 If enabled, the agent creates a swap file (`/swapfile`) on the resource disk
167 and adds it to the system swap space.
168
169 This configuration has no effect if resource disk is managed by cloud-init.
170 '';
171 };
172
173 SwapSizeMB = mkOption {
174 type = types.int;
175 default = 0;
176 description = ''
177 Specifies the size of the swap file in megabytes.
178
179 This configuration has no effect if resource disk is managed by cloud-init.
180 '';
181 };
182 };
183
184 Logs.Verbose = lib.mkOption {
185 type = types.bool;
186 default = false;
187 description = ''
188 If you set this option, log verbosity is boosted.
189 Waagent logs to `/var/log/waagent.log` and uses the system logrotate functionality to rotate logs.
190 '';
191 };
192
193 OS = {
194 EnableRDMA = lib.mkOption {
195 type = types.bool;
196 default = false;
197 description = ''
198 If enabled, the agent attempts to install and then load an RDMA kernel driver
199 that matches the version of the firmware on the underlying hardware.
200 '';
201 };
202
203 RootDeviceScsiTimeout = lib.mkOption {
204 type = types.nullOr types.int;
205 default = 300;
206 description = ''
207 Configures the SCSI timeout in seconds on the OS disk and data drives.
208 If set to `null`, the system defaults are used.
209 '';
210 };
211 };
212
213 HttpProxy = {
214 Host = lib.mkOption {
215 type = types.nullOr types.str;
216 default = null;
217 description = ''
218 If you set http proxy, waagent will use is proxy to access the Internet.
219 '';
220 };
221
222 Port = lib.mkOption {
223 type = types.nullOr types.int;
224 default = null;
225 description = ''
226 If you set http proxy, waagent will use this proxy to access the Internet.
227 '';
228 };
229 };
230
231 AutoUpdate.UpdateToLatestVersion = lib.mkOption {
232 type = types.bool;
233 default = false;
234 description = ''
235 Whether or not to enable auto-update of the Extension Handler.
236 '';
237 };
238 };
239 };
240in
241{
242 options.services.waagent = {
243 enable = lib.mkEnableOption "Windows Azure Linux Agent";
244
245 package = lib.mkPackageOption pkgs "waagent" { };
246
247 extraPackages = lib.mkOption {
248 default = [ ];
249 description = ''
250 Additional packages to add to the waagent {env}`PATH`.
251 '';
252 example = lib.literalExpression "[ pkgs.powershell ]";
253 type = lib.types.listOf lib.types.package;
254 };
255
256 settings = lib.mkOption {
257 type = settingsType;
258 default = { };
259 description = ''
260 The waagent.conf configuration, see https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux for documentation.
261 '';
262 };
263 };
264
265 config = lib.mkIf cfg.enable {
266 assertions = [
267 {
268 assertion = (cfg.settings.HttpProxy.Host != null) -> (cfg.settings.HttpProxy.Port != null);
269 message = "Option services.waagent.settings.HttpProxy.Port must be set if services.waagent.settings.HttpProxy.Host is set.";
270 }
271 ];
272
273 boot.initrd.kernelModules = [ "ata_piix" ];
274 networking.firewall.allowedUDPPorts = [ 68 ];
275
276 services.udev.packages = with pkgs; [ waagent ];
277
278 boot.initrd.services.udev = with pkgs; {
279 # Provide waagent-shipped udev rules in initrd too.
280 packages = [ waagent ];
281 # udev rules shell out to chmod, cut and readlink, which are all
282 # provided by pkgs.coreutils, which is in services.udev.path, but not
283 # boot.initrd.services.udev.binPackages.
284 binPackages = [ coreutils ];
285 };
286
287 networking.dhcpcd.persistent = true;
288
289 services.logrotate = {
290 enable = true;
291 settings."/var/log/waagent.log" = {
292 compress = true;
293 frequency = "monthly";
294 rotate = 6;
295 };
296 };
297
298 # Write settings to /etc/waagent.conf
299 environment.etc."waagent.conf".source = settingsFormat.generate "waagent.conf" cfg.settings;
300
301 systemd.targets.provisioned = {
302 description = "Services Requiring Azure VM provisioning to have finished";
303 };
304
305 systemd.services.consume-hypervisor-entropy = {
306 description = "Consume entropy in ACPI table provided by Hyper-V";
307
308 wantedBy = [
309 "sshd.service"
310 "waagent.service"
311 ];
312 before = [
313 "sshd.service"
314 "waagent.service"
315 ];
316
317 path = [ pkgs.coreutils ];
318 script = ''
319 echo "Fetching entropy..."
320 cat /sys/firmware/acpi/tables/OEM0 > /dev/random
321 '';
322 serviceConfig.Type = "oneshot";
323 serviceConfig.RemainAfterExit = true;
324 serviceConfig.StandardError = "journal+console";
325 serviceConfig.StandardOutput = "journal+console";
326 };
327
328 systemd.services.waagent = {
329 wantedBy = [ "multi-user.target" ];
330 after = [
331 "network-online.target"
332 ] ++ lib.optionals config.services.cloud-init.enable [ "cloud-init.service" ];
333 wants = [
334 "network-online.target"
335 "sshd.service"
336 "sshd-keygen.service"
337 ];
338
339 path =
340 with pkgs;
341 [
342 e2fsprogs
343 bash
344 findutils
345 gnugrep
346 gnused
347 iproute2
348 iptables
349 openssh
350 openssl
351 parted
352
353 # for hostname
354 nettools
355 # for pidof
356 procps
357 # for useradd, usermod
358 shadow
359
360 util-linux # for (u)mount, fdisk, sfdisk, mkswap
361 # waagent's Microsoft.CPlat.Core.RunCommandLinux needs lsof
362 lsof
363 ]
364 ++ cfg.extraPackages;
365 description = "Windows Azure Agent Service";
366 unitConfig.ConditionPathExists = "/etc/waagent.conf";
367 serviceConfig = {
368 ExecStart = "${lib.getExe cfg.package} -daemon";
369 Type = "simple";
370 Restart = "always";
371 Slice = "azure.slice";
372 CPUAccounting = true;
373 MemoryAccounting = true;
374 };
375 };
376
377 # waagent will generate files under /etc/sudoers.d during provisioning
378 security.sudo.extraConfig = ''
379 #includedir /etc/sudoers.d
380 '';
381 };
382}