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 ]
106 ++ optional config.networking.dhcpcd.enable "dhcpcd.service"
107 ++ optional config.systemd.network.enable "systemd-networkd.service";
108 };
109 };
110
111 /*
112 Fetch the root password from the digital ocean metadata.
113 There is no specific route for this, so we use jq to get
114 it from the One Big JSON metadata blob
115 */
116 systemd.services.digitalocean-set-root-password = mkIf cfg.setRootPassword {
117 path = [
118 pkgs.shadow
119 pkgs.jq
120 ];
121 description = "Set root password provided by Digitalocean";
122 wantedBy = [ "multi-user.target" ];
123 script = ''
124 set -eo pipefail
125 ROOT_PASSWORD=$(jq -er '.auth_key' ${doMetadataFile})
126 echo "root:$ROOT_PASSWORD" | chpasswd
127 mkdir -p /etc/do-metadata/set-root-password
128 '';
129 unitConfig = {
130 ConditionPathExists = "!/etc/do-metadata/set-root-password";
131 Before = optional config.services.openssh.enable "sshd.service";
132 After = [ "digitalocean-metadata.service" ];
133 Requires = [ "digitalocean-metadata.service" ];
134 };
135 serviceConfig = {
136 Type = "oneshot";
137 };
138 };
139
140 /*
141 Set the hostname from Digital Ocean, unless the user configured it in
142 the NixOS configuration. The cached metadata file isn't used here
143 because the hostname is a mutable part of the droplet.
144 */
145 systemd.services.digitalocean-set-hostname = mkIf (hostName == "") {
146 path = [
147 pkgs.curl
148 pkgs.net-tools
149 ];
150 description = "Set hostname provided by Digitalocean";
151 wantedBy = [ "network.target" ];
152 script = ''
153 set -e
154 DIGITALOCEAN_HOSTNAME=$(curl -fsSL http://169.254.169.254/metadata/v1/hostname)
155 hostname "$DIGITALOCEAN_HOSTNAME"
156 if [[ ! -e /etc/hostname || -w /etc/hostname ]]; then
157 printf "%s\n" "$DIGITALOCEAN_HOSTNAME" > /etc/hostname
158 fi
159 '';
160 unitConfig = {
161 Before = [ "network.target" ];
162 After = [ "digitalocean-metadata.service" ];
163 Wants = [ "digitalocean-metadata.service" ];
164 };
165 serviceConfig = {
166 Type = "oneshot";
167 };
168 };
169
170 # Fetch the ssh keys for root from Digital Ocean
171 systemd.services.digitalocean-ssh-keys = mkIf cfg.setSshKeys {
172 description = "Set root ssh keys provided by Digital Ocean";
173 wantedBy = [ "multi-user.target" ];
174 path = [ pkgs.jq ];
175 script = ''
176 set -e
177 mkdir -m 0700 -p /root/.ssh
178 jq -er '.public_keys[]' ${doMetadataFile} > /root/.ssh/authorized_keys
179 chmod 600 /root/.ssh/authorized_keys
180 '';
181 serviceConfig = {
182 Type = "oneshot";
183 RemainAfterExit = true;
184 };
185 unitConfig = {
186 ConditionPathExists = "!/root/.ssh/authorized_keys";
187 Before = optional config.services.openssh.enable "sshd.service";
188 After = [ "digitalocean-metadata.service" ];
189 Requires = [ "digitalocean-metadata.service" ];
190 };
191 };
192
193 /*
194 Initialize the RNG by running the entropy-seed script from the
195 Digital Ocean metadata
196 */
197 systemd.services.digitalocean-entropy-seed = mkIf cfg.seedEntropy {
198 description = "Run the kernel RNG entropy seeding script from the Digital Ocean vendor data";
199 wantedBy = [ "network.target" ];
200 path = [
201 pkgs.jq
202 pkgs.mpack
203 ];
204 script = ''
205 set -eo pipefail
206 TEMPDIR=$(mktemp -d)
207 jq -er '.vendor_data' ${doMetadataFile} | munpack -tC $TEMPDIR
208 ENTROPY_SEED=$(grep -rl "DigitalOcean Entropy Seed script" $TEMPDIR)
209 ${pkgs.runtimeShell} $ENTROPY_SEED
210 rm -rf $TEMPDIR
211 '';
212 unitConfig = {
213 Before = [ "network.target" ];
214 After = [ "digitalocean-metadata.service" ];
215 Requires = [ "digitalocean-metadata.service" ];
216 };
217 serviceConfig = {
218 Type = "oneshot";
219 };
220 };
221
222 }
223 ];
224 meta.maintainers = with maintainers; [
225 arianvp
226 eamsden
227 ];
228}