nixos/digital-ocean-image: init
This commit is contained in:
parent
7cb0bc30e6
commit
8bba28260a
3 changed files with 361 additions and 0 deletions
197
nixos/modules/virtualisation/digital-ocean-config.nix
Normal file
197
nixos/modules/virtualisation/digital-ocean-config.nix
Normal file
|
@ -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 ];
|
||||
}
|
||||
|
69
nixos/modules/virtualisation/digital-ocean-image.nix
Normal file
69
nixos/modules/virtualisation/digital-ocean-image.nix
Normal file
|
@ -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 ];
|
||||
|
||||
}
|
95
nixos/modules/virtualisation/digital-ocean-init.nix
Normal file
95
nixos/modules/virtualisation/digital-ocean-init.nix
Normal file
|
@ -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 a new issue