1{
2 config,
3 pkgs,
4 lib,
5 modulesPath,
6 ...
7}:
8with lib;
9{
10 imports = [
11 (modulesPath + "/profiles/qemu-guest.nix")
12 (modulesPath + "/virtualisation/digital-ocean-init.nix")
13 ];
14 options.virtualisation.digitalOcean = with types; {
15 setRootPassword = mkOption {
16 type = bool;
17 default = false;
18 example = true;
19 description = "Whether to set the root password from the Digital Ocean metadata";
20 };
21 setSshKeys = mkOption {
22 type = bool;
23 default = true;
24 example = true;
25 description = "Whether to fetch ssh keys from Digital Ocean";
26 };
27 seedEntropy = mkOption {
28 type = bool;
29 default = true;
30 example = true;
31 description = "Whether to run the kernel RNG entropy seeding script from the Digital Ocean vendor data";
32 };
33 };
34 config =
35 let
36 cfg = config.virtualisation.digitalOcean;
37 hostName = config.networking.hostName;
38 doMetadataFile = "/run/do-metadata/v1.json";
39 in
40 mkMerge [
41 {
42 fileSystems."/" = lib.mkDefault {
43 device = "/dev/disk/by-label/nixos";
44 autoResize = true;
45 fsType = "ext4";
46 };
47 boot = {
48 growPartition = true;
49 kernelParams = [
50 "console=ttyS0"
51 "panic=1"
52 "boot.panic_on_fail"
53 ];
54 initrd.kernelModules = [ "virtio_scsi" ];
55 kernelModules = [
56 "virtio_pci"
57 "virtio_net"
58 ];
59 loader.grub.devices = [ "/dev/vda" ];
60 };
61 services.openssh = {
62 enable = mkDefault true;
63 settings.PasswordAuthentication = mkDefault false;
64 };
65 services.do-agent.enable = mkDefault true;
66 networking = {
67 hostName = mkDefault ""; # use Digital Ocean metadata server
68 };
69
70 /*
71 Check for and wait for the metadata server to become reachable.
72 This serves as a dependency for all the other metadata services.
73 */
74 systemd.services.digitalocean-metadata = {
75 path = [ pkgs.curl ];
76 description = "Get host metadata provided by Digitalocean";
77 script = ''
78 set -eu
79 DO_DELAY_ATTEMPTS=0
80 while ! curl -fsSL -o $RUNTIME_DIRECTORY/v1.json http://169.254.169.254/metadata/v1.json; do
81 DO_DELAY_ATTEMPTS=$((DO_DELAY_ATTEMPTS + 1))
82 if (( $DO_DELAY_ATTEMPTS >= $DO_DELAY_ATTEMPTS_MAX )); then
83 echo "giving up"
84 exit 1
85 fi
86
87 echo "metadata unavailable, trying again in 1s..."
88 sleep 1
89 done
90 chmod 600 $RUNTIME_DIRECTORY/v1.json
91 '';
92 environment = {
93 DO_DELAY_ATTEMPTS_MAX = "10";
94 };
95 serviceConfig = {
96 Type = "oneshot";
97 RemainAfterExit = true;
98 RuntimeDirectory = "do-metadata";
99 RuntimeDirectoryPreserve = "yes";
100 };
101 unitConfig = {
102 ConditionPathExists = "!${doMetadataFile}";
103 After =
104 [ "network-pre.target" ]
105 ++ optional config.networking.dhcpcd.enable "dhcpcd.service"
106 ++ optional config.systemd.network.enable "systemd-networkd.service";
107 };
108 };
109
110 /*
111 Fetch the root password from the digital ocean metadata.
112 There is no specific route for this, so we use jq to get
113 it from the One Big JSON metadata blob
114 */
115 systemd.services.digitalocean-set-root-password = mkIf cfg.setRootPassword {
116 path = [
117 pkgs.shadow
118 pkgs.jq
119 ];
120 description = "Set root password provided by Digitalocean";
121 wantedBy = [ "multi-user.target" ];
122 script = ''
123 set -eo pipefail
124 ROOT_PASSWORD=$(jq -er '.auth_key' ${doMetadataFile})
125 echo "root:$ROOT_PASSWORD" | chpasswd
126 mkdir -p /etc/do-metadata/set-root-password
127 '';
128 unitConfig = {
129 ConditionPathExists = "!/etc/do-metadata/set-root-password";
130 Before = optional config.services.openssh.enable "sshd.service";
131 After = [ "digitalocean-metadata.service" ];
132 Requires = [ "digitalocean-metadata.service" ];
133 };
134 serviceConfig = {
135 Type = "oneshot";
136 };
137 };
138
139 /*
140 Set the hostname from Digital Ocean, unless the user configured it in
141 the NixOS configuration. The cached metadata file isn't used here
142 because the hostname is a mutable part of the droplet.
143 */
144 systemd.services.digitalocean-set-hostname = mkIf (hostName == "") {
145 path = [
146 pkgs.curl
147 pkgs.nettools
148 ];
149 description = "Set hostname provided by Digitalocean";
150 wantedBy = [ "network.target" ];
151 script = ''
152 set -e
153 DIGITALOCEAN_HOSTNAME=$(curl -fsSL http://169.254.169.254/metadata/v1/hostname)
154 hostname "$DIGITALOCEAN_HOSTNAME"
155 if [[ ! -e /etc/hostname || -w /etc/hostname ]]; then
156 printf "%s\n" "$DIGITALOCEAN_HOSTNAME" > /etc/hostname
157 fi
158 '';
159 unitConfig = {
160 Before = [ "network.target" ];
161 After = [ "digitalocean-metadata.service" ];
162 Wants = [ "digitalocean-metadata.service" ];
163 };
164 serviceConfig = {
165 Type = "oneshot";
166 };
167 };
168
169 # Fetch the ssh keys for root from Digital Ocean
170 systemd.services.digitalocean-ssh-keys = mkIf cfg.setSshKeys {
171 description = "Set root ssh keys provided by Digital Ocean";
172 wantedBy = [ "multi-user.target" ];
173 path = [ pkgs.jq ];
174 script = ''
175 set -e
176 mkdir -m 0700 -p /root/.ssh
177 jq -er '.public_keys[]' ${doMetadataFile} > /root/.ssh/authorized_keys
178 chmod 600 /root/.ssh/authorized_keys
179 '';
180 serviceConfig = {
181 Type = "oneshot";
182 RemainAfterExit = true;
183 };
184 unitConfig = {
185 ConditionPathExists = "!/root/.ssh/authorized_keys";
186 Before = optional config.services.openssh.enable "sshd.service";
187 After = [ "digitalocean-metadata.service" ];
188 Requires = [ "digitalocean-metadata.service" ];
189 };
190 };
191
192 /*
193 Initialize the RNG by running the entropy-seed script from the
194 Digital Ocean metadata
195 */
196 systemd.services.digitalocean-entropy-seed = mkIf cfg.seedEntropy {
197 description = "Run the kernel RNG entropy seeding script from the Digital Ocean vendor data";
198 wantedBy = [ "network.target" ];
199 path = [
200 pkgs.jq
201 pkgs.mpack
202 ];
203 script = ''
204 set -eo pipefail
205 TEMPDIR=$(mktemp -d)
206 jq -er '.vendor_data' ${doMetadataFile} | munpack -tC $TEMPDIR
207 ENTROPY_SEED=$(grep -rl "DigitalOcean Entropy Seed script" $TEMPDIR)
208 ${pkgs.runtimeShell} $ENTROPY_SEED
209 rm -rf $TEMPDIR
210 '';
211 unitConfig = {
212 Before = [ "network.target" ];
213 After = [ "digitalocean-metadata.service" ];
214 Requires = [ "digitalocean-metadata.service" ];
215 };
216 serviceConfig = {
217 Type = "oneshot";
218 };
219 };
220
221 }
222 ];
223 meta.maintainers = with maintainers; [
224 arianvp
225 eamsden
226 ];
227}