Fediversity/deployment/proxmox/provision.sh

340 lines
8.4 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euC
################################################################################
## Constants
## FIXME: There seems to be a problem with file upload where the task is
## registered to `node051` no matter what node we are actually uploading to? For
## now, let us just use `node051` everywhere.
readonly node=node051
readonly tmpdir=/tmp/proxmox-provision-$RANDOM
mkdir $tmpdir
################################################################################
## Parse arguments
api_url=
username=
password=
sockets=1
cores=1
memory=2048
vm_ids=
debug=false
help () {
cat <<EOF
Usage: $0 [OPTION...] ID [ID...]
ID can be either:
- of the form INT:STR, in which case the integer will be taken as the Proxmox
id for the machine, and the string as its name in Proxmox and the name of
the NixOS configuration to install,
- an integer, in which case it will be taken as the Proxmox id for the
machine, and the name will be the id prefixed by 'fedi'.
Options:
--api-url STR Base URL of the Proxmox API (required)
--username STR Username, with provider (eg. niols@pve; required)
--password STR Password (required)
--cores INT Number of cores (default: $cores)
--memory INT Memory (default: $memory)
--sockets INT Number of sockets (default: $sockets)
--debug Run this script in debug mode (default: $debug)
-h|-?|--help Show this help and exit
Options can also be provided by adding assignments to a '.proxmox' file in the
current working directory. For instance, it could contain:
api_url=https://192.168.51.81:8006/api2/json
cores=7
username=mireille@pve
debug=true
Command line options take precedence over options found in the '.proxmox' file.
EOF
}
# shellcheck disable=SC2059
die () { printf '\033[31m'; printf "$@"; printf '\033[0m\n'; exit 2; }
# shellcheck disable=SC2059
die_with_help () { printf '\033[31m'; printf "$@"; printf '\033[0m\n\n'; help; exit 2; }
# shellcheck disable=SC2059
debug () { if $debug; then printf >&2 '\033[37m'; printf >&2 "$@"; printf >&2 '\033[0m\n'; fi }
if [ -f .proxmox ]; then
. "$PWD"/.proxmox
fi
while [ $# -gt 0 ]; do
argument=$1
shift
case $argument in
--api-url|--api_url) readonly api_url="$1"; shift ;;
--username) readonly username="$1"; shift ;;
--password) readonly password="$1"; shift ;;
--sockets) sockets=$1; shift ;;
--cores) cores=$1; shift ;;
--memory) memory=$1; shift ;;
--debug) debug=true ;;
-h|-\?|--help) help; exit 0 ;;
-*) die_with_help "Unknown argument: '%s'." "$argument" ;;
*) vm_ids="$vm_ids $argument" ;;
esac
done
if [ -z "$vm_ids" ]; then
die_with_help "Required: at least one VM id."
fi
if [ -z "$api_url" ] || [ -z "$username" ] || [ -z "$password" ]; then
die_with_help "Required: '--api-url', '--username' and '--password'."
fi
readonly sockets
readonly cores
readonly memory
## FIXME: When we figure out how to use other nodes than node051.
# if [ -z "$node" ]; then
# printf 'Picking random node...'
# proxmox GET "$api_url/nodes"
# node=$(from_response .data[].node | sort -R | head -n 1)
# printf " done. Picked '%s'.\n" "$node"
# fi
# readonly node
readonly debug
################################################################################
## Getting started
printf 'Authenticating...'
response=$(
http \
--verify no \
POST "$api_url/access/ticket" \
"username=$username" \
"password=$password"
)
ticket=$(echo "$response" | jq -r .data.ticket)
readonly ticket
csrf_token=$(echo "$response" | jq -r .data.CSRFPreventionToken)
readonly csrf_token
printf ' done.\n'
acquire_lock () {
until mkdir "$tmpdir/lock-$1" 2>/dev/null; do sleep 1; done
}
release_lock () {
rmdir "$tmpdir/lock-$1"
}
proxmox () {
acquire_lock proxmox
debug 'request %s' "$*"
response=$(
http \
--form \
--verify no \
--ignore-stdin \
"$@" \
"Cookie:PVEAuthCookie=$ticket" \
"CSRFPreventionToken:$csrf_token"
)
debug 'response to request %s:\n %s' "$*" "$response"
release_lock proxmox
echo "$response"
}
## Synchronous variant for when the `proxmox` function would just respond an
## UPID in the `data` JSON field.
proxmox_sync () (
response=$(proxmox "$@")
upid=$(echo "$response" | jq -r .data)
while :; do
response=$(proxmox GET "$api_url/nodes/$node/tasks/$upid/status")
status=$(echo "$response" | jq -r .data.status)
case $status in
running) sleep 1 ;;
stopped) break ;;
*) die "unexpected status: '%s'" "$status" ;;
esac
done
)
################################################################################
## Build ISO
build_iso () {
acquire_lock build
printf 'Building ISO for VM %s...\n' "$2"
nix build \
--impure --expr "
let flake = builtins.getFlake (builtins.toString ./.); in
flake.lib.makeInstallerIso {
nixosConfiguration = flake.nixosConfigurations.$2;
nixpkgs = flake.inputs.nixpkgs;
hostKeys = {
ed25519 = {
private = ./deployment/hostKeys/$2/ssh_host_ed25519_key;
public = ./deployment/hostKeys/$2/ssh_host_ed25519_key.pub;
};
};
}
" \
--log-format raw --quiet \
--out-link "$tmpdir/installer-$2"
ln -sf "$tmpdir/installer-$2/iso/installer.iso" "$tmpdir/installer-$2.iso"
printf 'done building ISO for VM %s.\n' "$2"
release_lock build
}
################################################################################
## Upload ISO
upload_iso () {
acquire_lock upload
printf 'Uploading ISO for VM %s...\n' "$2"
proxmox_sync POST "$api_url/nodes/$node/storage/local/upload" \
"filename@$tmpdir/installer-$2.iso" \
content==iso
printf 'done uploading ISO for VM %s.\n' "$2"
release_lock upload
}
################################################################################
## Remove ISO
remove_iso () {
printf 'Removing ISO for VM %s...\n' "$2"
proxmox_sync DELETE "$api_url/nodes/$node/storage/local/content/local:iso/installer-$2.iso"
printf 'done removing ISO for VM %s.\n' "$2"
}
################################################################################
## Create VM
create_vm () {
printf 'Creating VM %s with id %d...\n' "$2" "$1"
proxmox_sync POST "$api_url/nodes/$node/qemu" \
\
vmid=="$1" \
name=="$2" \
pool==Fediversity \
\
ide2=="local:iso/installer-$2.iso,media=cdrom" \
ostype==l26 \
\
bios==ovmf \
efidisk0=='linstor_storage:1,efitype=4m' \
agent==1 \
\
scsihw==virtio-scsi-single \
scsi0=='linstor_storage:32,discard=on,ssd=on,iothread=on' \
\
sockets=="$sockets" \
cores=="$cores" \
cpu==x86-64-v2-AES \
numa==1 \
\
memory=="$memory" \
\
net0=='virtio,bridge=vnet1306'
printf 'done creating VM %s.\n' "$2"
}
################################################################################
## Install VM
install_vm () (
printf 'Installing VM %s...\n' "$2"
proxmox_sync POST "$api_url/nodes/$node/qemu/$1/status/start"
while :; do
response=$(proxmox GET "$api_url/nodes/$node/qemu/$1/status/current")
status=$(echo "$response" | jq -r .data.status)
case $status in
running) sleep 1 ;;
stopped) break ;;
*) die " unexpected status: '%s'\n" "$status" ;;
esac
done
printf 'done installing VM %s.\n' "$2"
)
################################################################################
## Start VM
start_vm () {
printf 'Starting VM %s...\n' "$2"
proxmox_sync POST "$api_url/nodes/$node/qemu/$1/config" \
ide2=='none,media=cdrom' \
net0=='virtio,bridge=vnet1305'
proxmox_sync POST "$api_url/nodes/$node/qemu/$1/status/start"
printf 'done starting VM %s.\n' "$2"
}
################################################################################
## Main loop
printf 'Provisioning VMs%s with:\n' "$vm_ids"
printf ' sockets: %d\n' "$sockets"
printf ' cores: %d\n' "$cores"
printf ' memory: %d\n' "$memory"
provision_vm () {
build_iso "$@"
upload_iso "$@"
create_vm "$@"
install_vm "$@"
start_vm "$@"
remove_iso "$@"
}
for vm_id in $vm_ids; do
vm_name=${vm_id#*:}
vm_id=${vm_id%:*}
if [ "$vm_id" = "$vm_name" ]; then
vm_name=$(printf 'fedi%03d' "$vm_id")
fi
provision_vm "$vm_id" "$vm_name" &
done
wait
printf 'done provisioning VMs%s.\n' "$vm_ids"
################################################################################
## Cleanup
rm -Rf $tmpdir