parent
7cb0bc30e6
commit
8bba28260a
@ -0,0 +1,197 @@ |
||||
{ config, pkgs, lib, modulesPath, ... }: |
||||
with lib; |
||||
{ |
||||
imports = [ |
||||
(modulesPath + "/profiles/qemu-guest.nix") |
||||
(modulesPath + "/virtualisation/digital-ocean-init.nix") |
||||
]; |
||||
options.virtualisation.digitalOcean = with types; { |
||||
setRootPassword = mkOption { |
||||
type = bool; |
||||
default = false; |
||||
example = true; |
||||
description = "Whether to set the root password from the Digital Ocean metadata"; |
||||
}; |
||||
setSshKeys = mkOption { |
||||
type = bool; |
||||
default = true; |
||||
example = true; |
||||
description = "Whether to fetch ssh keys from Digital Ocean"; |
||||
}; |
||||
seedEntropy = mkOption { |
||||
type = bool; |
||||
default = true; |
||||
example = true; |
||||
description = "Whether to run the kernel RNG entropy seeding script from the Digital Ocean vendor data"; |
||||
}; |
||||
}; |
||||
config = |
||||
let |
||||
cfg = config.virtualisation.digitalOcean; |
||||
hostName = config.networking.hostName; |
||||
doMetadataFile = "/run/do-metadata/v1.json"; |
||||
in mkMerge [{ |
||||
fileSystems."/" = { |
||||
device = "/dev/disk/by-label/nixos"; |
||||
autoResize = true; |
||||
fsType = "ext4"; |
||||
}; |
||||
boot = { |
||||
growPartition = true; |
||||
kernelParams = [ "console=ttyS0" "panic=1" "boot.panic_on_fail" ]; |
||||
initrd.kernelModules = [ "virtio_scsi" ]; |
||||
kernelModules = [ "virtio_pci" "virtio_net" ]; |
||||
loader = { |
||||
grub.device = "/dev/vda"; |
||||
timeout = 0; |
||||
grub.configurationLimit = 0; |
||||
}; |
||||
}; |
||||
services.openssh = { |
||||
enable = mkDefault true; |
||||
passwordAuthentication = mkDefault false; |
||||
}; |
||||
services.do-agent.enable = mkDefault true; |
||||
networking = { |
||||
hostName = mkDefault ""; # use Digital Ocean metadata server |
||||
}; |
||||
|
||||
/* Check for and wait for the metadata server to become reachable. |
||||
* This serves as a dependency for all the other metadata services. */ |
||||
systemd.services.digitalocean-metadata = { |
||||
path = [ pkgs.curl ]; |
||||
description = "Get host metadata provided by Digitalocean"; |
||||
script = '' |
||||
set -eu |
||||
DO_DELAY_ATTEMPTS=0 |
||||
while ! curl -fsSL -o $RUNTIME_DIRECTORY/v1.json http://169.254.169.254/metadata/v1.json; do |
||||
DO_DELAY_ATTEMPTS=$((DO_DELAY_ATTEMPTS + 1)) |
||||
if (( $DO_DELAY_ATTEMPTS >= $DO_DELAY_ATTEMPTS_MAX )); then |
||||
echo "giving up" |
||||
exit 1 |
||||
fi |
||||
|
||||
echo "metadata unavailable, trying again in 1s..." |
||||
sleep 1 |
||||
done |
||||
chmod 600 $RUNTIME_DIRECTORY/v1.json |
||||
''; |
||||
environment = { |
||||
DO_DELAY_ATTEMPTS_MAX = "10"; |
||||
}; |
||||
serviceConfig = { |
||||
Type = "oneshot"; |
||||
RemainAfterExit = true; |
||||
RuntimeDirectory = "do-metadata"; |
||||
RuntimeDirectoryPreserve = "yes"; |
||||
}; |
||||
unitConfig = { |
||||
ConditionPathExists = "!${doMetadataFile}"; |
||||
After = [ "network-pre.target" ] ++ |
||||
optional config.networking.dhcpcd.enable "dhcpcd.service" ++ |
||||
optional config.systemd.network.enable "systemd-networkd.service"; |
||||
}; |
||||
}; |
||||
|
||||
/* Fetch the root password from the digital ocean metadata. |
||||
* There is no specific route for this, so we use jq to get |
||||
* it from the One Big JSON metadata blob */ |
||||
systemd.services.digitalocean-set-root-password = mkIf cfg.setRootPassword { |
||||
path = [ pkgs.shadow pkgs.jq ]; |
||||
description = "Set root password provided by Digitalocean"; |
||||
wantedBy = [ "multi-user.target" ]; |
||||
script = '' |
||||
set -eo pipefail |
||||
ROOT_PASSWORD=$(jq -er '.auth_key' ${doMetadataFile}) |
||||
echo "root:$ROOT_PASSWORD" | chpasswd |
||||
mkdir -p /etc/do-metadata/set-root-password |
||||
''; |
||||
unitConfig = { |
||||
ConditionPathExists = "!/etc/do-metadata/set-root-password"; |
||||
Before = optional config.services.openssh.enable "sshd.service"; |
||||
After = [ "digitalocean-metadata.service" ]; |
||||
Requires = [ "digitalocean-metadata.service" ]; |
||||
}; |
||||
serviceConfig = { |
||||
Type = "oneshot"; |
||||
}; |
||||
}; |
||||
|
||||
/* Set the hostname from Digital Ocean, unless the user configured it in |
||||
* the NixOS configuration. The cached metadata file isn't used here |
||||
* because the hostname is a mutable part of the droplet. */ |
||||
systemd.services.digitalocean-set-hostname = mkIf (hostName == "") { |
||||
path = [ pkgs.curl pkgs.nettools ]; |
||||
description = "Set hostname provided by Digitalocean"; |
||||
wantedBy = [ "network.target" ]; |
||||
script = '' |
||||
set -e |
||||
DIGITALOCEAN_HOSTNAME=$(curl -fsSL http://169.254.169.254/metadata/v1/hostname) |
||||
hostname "$DIGITALOCEAN_HOSTNAME" |
||||
if [[ ! -e /etc/hostname || -w /etc/hostname ]]; then |
||||
printf "%s\n" "$DIGITALOCEAN_HOSTNAME" > /etc/hostname |
||||
fi |
||||
''; |
||||
unitConfig = { |
||||
Before = [ "network.target" ]; |
||||
After = [ "digitalocean-metadata.service" ]; |
||||
Wants = [ "digitalocean-metadata.service" ]; |
||||
}; |
||||
serviceConfig = { |
||||
Type = "oneshot"; |
||||
}; |
||||
}; |
||||
|
||||
/* Fetch the ssh keys for root from Digital Ocean */ |
||||
systemd.services.digitalocean-ssh-keys = mkIf cfg.setSshKeys { |
||||
description = "Set root ssh keys provided by Digital Ocean"; |
||||
wantedBy = [ "multi-user.target" ]; |
||||
path = [ pkgs.jq ]; |
||||
script = '' |
||||
set -e |
||||
mkdir -m 0700 -p /root/.ssh |
||||
jq -er '.public_keys[]' ${doMetadataFile} > /root/.ssh/authorized_keys |
||||
chmod 600 /root/.ssh/authorized_keys |
||||
''; |
||||
serviceConfig = { |
||||
Type = "oneshot"; |
||||
RemainAfterExit = true; |
||||
}; |
||||
unitConfig = { |
||||
ConditionPathExists = "!/root/.ssh/authorized_keys"; |
||||
Before = optional config.services.openssh.enable "sshd.service"; |
||||
After = [ "digitalocean-metadata.service" ]; |
||||
Requires = [ "digitalocean-metadata.service" ]; |
||||
}; |
||||
}; |
||||
|
||||
/* Initialize the RNG by running the entropy-seed script from the |
||||
* Digital Ocean metadata |
||||
*/ |
||||
systemd.services.digitalocean-entropy-seed = mkIf cfg.seedEntropy { |
||||
description = "Run the kernel RNG entropy seeding script from the Digital Ocean vendor data"; |
||||
wantedBy = [ "network.target" ]; |
||||
path = [ pkgs.jq pkgs.mpack ]; |
||||
script = '' |
||||
set -eo pipefail |
||||
TEMPDIR=$(mktemp -d) |
||||
jq -er '.vendor_data' ${doMetadataFile} | munpack -tC $TEMPDIR |
||||
ENTROPY_SEED=$(grep -rl "DigitalOcean Entropy Seed script" $TEMPDIR) |
||||
${pkgs.runtimeShell} $ENTROPY_SEED |
||||
rm -rf $TEMPDIR |
||||
''; |
||||
unitConfig = { |
||||
Before = [ "network.target" ]; |
||||
After = [ "digitalocean-metadata.service" ]; |
||||
Requires = [ "digitalocean-metadata.service" ]; |
||||
}; |
||||
serviceConfig = { |
||||
Type = "oneshot"; |
||||
}; |
||||
}; |
||||
|
||||
} |
||||
]; |
||||
meta.maintainers = with maintainers; [ arianvp eamsden ]; |
||||
} |
||||
|
@ -0,0 +1,69 @@ |
||||
{ config, lib, pkgs, ... }: |
||||
|
||||
with lib; |
||||
let |
||||
cfg = config.virtualisation.digitalOceanImage; |
||||
in |
||||
{ |
||||
|
||||
imports = [ ./digital-ocean-config.nix ]; |
||||
|
||||
options = { |
||||
virtualisation.digitalOceanImage.diskSize = mkOption { |
||||
type = with types; int; |
||||
default = 4096; |
||||
description = '' |
||||
Size of disk image. Unit is MB. |
||||
''; |
||||
}; |
||||
|
||||
virtualisation.digitalOceanImage.configFile = mkOption { |
||||
type = with types; nullOr path; |
||||
default = null; |
||||
description = '' |
||||
A path to a configuration file which will be placed at |
||||
<literal>/etc/nixos/configuration.nix</literal> and be used when switching |
||||
to a new configuration. If set to <literal>null</literal>, a default |
||||
configuration is used that imports |
||||
<literal>(modulesPath + "/virtualisation/digital-ocean-config.nix")</literal>. |
||||
''; |
||||
}; |
||||
|
||||
virtualisation.digitalOceanImage.compressionMethod = mkOption { |
||||
type = types.enum [ "gzip" "bzip2" ]; |
||||
default = "gzip"; |
||||
example = "bzip2"; |
||||
description = '' |
||||
Disk image compression method. Choose bzip2 to generate smaller images that |
||||
take longer to generate but will consume less metered storage space on your |
||||
Digital Ocean account. |
||||
''; |
||||
}; |
||||
}; |
||||
|
||||
#### implementation |
||||
config = { |
||||
|
||||
system.build.digitalOceanImage = import ../../lib/make-disk-image.nix { |
||||
name = "digital-ocean-image"; |
||||
format = "qcow2"; |
||||
postVM = let |
||||
compress = { |
||||
"gzip" = "${pkgs.gzip}/bin/gzip"; |
||||
"bzip2" = "${pkgs.bzip2}/bin/bzip2"; |
||||
}.${cfg.compressionMethod}; |
||||
in '' |
||||
${compress} $diskImage |
||||
''; |
||||
configFile = if cfg.configFile == null |
||||
then config.virtualisation.digitalOcean.defaultConfigFile |
||||
else cfg.configFile; |
||||
inherit (cfg) diskSize; |
||||
inherit config lib pkgs; |
||||
}; |
||||
|
||||
}; |
||||
|
||||
meta.maintainers = with maintainers; [ arianvp eamsden ]; |
||||
|
||||
} |
@ -0,0 +1,95 @@ |
||||
{ config, pkgs, lib, ... }: |
||||
with lib; |
||||
let |
||||
cfg = config.virtualisation.digitalOcean; |
||||
defaultConfigFile = pkgs.writeText "digitalocean-configuration.nix" '' |
||||
{ modulesPath, lib, ... }: |
||||
{ |
||||
imports = lib.optional (builtins.pathExists ./do-userdata.nix) ./do-userdata.nix ++ [ |
||||
(modulesPath + "/virtualisation/digital-ocean-config.nix") |
||||
]; |
||||
} |
||||
''; |
||||
in { |
||||
options.virtualisation.digitalOcean.rebuildFromUserData = mkOption { |
||||
type = types.bool; |
||||
default = true; |
||||
example = true; |
||||
description = "Whether to reconfigure the system from Digital Ocean user data"; |
||||
}; |
||||
options.virtualisation.digitalOcean.defaultConfigFile = mkOption { |
||||
type = types.path; |
||||
default = defaultConfigFile; |
||||
defaultText = '' |
||||
The default configuration imports user-data if applicable and |
||||
<literal>(modulesPath + "/virtualisation/digital-ocean-config.nix")</literal>. |
||||
''; |
||||
description = '' |
||||
A path to a configuration file which will be placed at |
||||
<literal>/etc/nixos/configuration.nix</literal> and be used when switching to |
||||
a new configuration. |
||||
''; |
||||
}; |
||||
|
||||
config = { |
||||
systemd.services.digitalocean-init = mkIf cfg.rebuildFromUserData { |
||||
description = "Reconfigure the system from Digital Ocean userdata on startup"; |
||||
wantedBy = [ "network-online.target" ]; |
||||
unitConfig = { |
||||
ConditionPathExists = "!/etc/nixos/do-userdata.nix"; |
||||
After = [ "digitalocean-metadata.service" "network-online.target" ]; |
||||
Requires = [ "digitalocean-metadata.service" ]; |
||||
X-StopOnRemoval = false; |
||||
}; |
||||
serviceConfig = { |
||||
Type = "oneshot"; |
||||
RemainAfterExit = true; |
||||
}; |
||||
restartIfChanged = false; |
||||
path = [ pkgs.jq pkgs.gnused pkgs.gnugrep pkgs.systemd config.nix.package config.system.build.nixos-rebuild ]; |
||||
environment = { |
||||
HOME = "/root"; |
||||
NIX_PATH = concatStringsSep ":" [ |
||||
"/nix/var/nix/profiles/per-user/root/channels/nixos" |
||||
"nixos-config=/etc/nixos/configuration.nix" |
||||
"/nix/var/nix/profiles/per-user/root/channels" |
||||
]; |
||||
}; |
||||
script = '' |
||||
set -e |
||||
echo "attempting to fetch configuration from Digital Ocean user data..." |
||||
userData=$(mktemp) |
||||
if jq -er '.user_data' /run/do-metadata/v1.json > $userData; then |
||||
# If the user-data looks like it could be a nix expression, |
||||
# copy it over. Also, look for a magic three-hash comment and set |
||||
# that as the channel. |
||||
if nix-instantiate --parse $userData > /dev/null; then |
||||
channels="$(grep '^###' "$userData" | sed 's|###\s*||')" |
||||
printf "%s" "$channels" | while read channel; do |
||||
echo "writing channel: $channel" |
||||
done |
||||
|
||||
if [[ -n "$channels" ]]; then |
||||
printf "%s" "$channels" > /root/.nix-channels |
||||
nix-channel --update |
||||
fi |
||||
|
||||
echo "setting configuration from Digital Ocean user data" |
||||
cp "$userData" /etc/nixos/do-userdata.nix |
||||
if [[ ! -e /etc/nixos/configuration.nix ]]; then |
||||
install -m0644 ${cfg.defaultConfigFile} /etc/nixos/configuration.nix |
||||
fi |
||||
else |
||||
echo "user data does not appear to be a Nix expression; ignoring" |
||||
exit |
||||
fi |
||||
|
||||
nixos-rebuild switch |
||||
else |
||||
echo "no user data is available" |
||||
fi |
||||
''; |
||||
}; |
||||
}; |
||||
meta.maintainers = with maintainers; [ arianvp eamsden ]; |
||||
} |
Loading…
Reference in new issue