umorpha-boxes/bin/vultr-install

217 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Copyright (C) 2023-2024 Umorpha Systems
# SPDX-License-Identifier: AGPL-3.0-or-later
set -euE -o pipefail
usage() {
cat <<EOF
Usage: ${0##*/} --name=HOSTNAME --plan=PLAN_ID --region=REGION_ID
or: ${0##*/} --help
Create/wipe a Vultr instance and install an image onto it.
For selecting the appropriate plan ID, determine which type you want
from the following table, then list the plans of that type with
\`vultr-cli plans list --type=TYPE\`
| type | WebUI label | ID format |
|-------+---------------------------------------------+---------------------------------------------|
| vc2 | Cloud Compute : Regular Performance | vc2-${nproc}c-${ram}gb[-sc1] |
| vhf | Cloud Compute : High Frequency | vhf-${nproc}c-${ram}gb[-sc1] |
| vhp | Cloud Compute : High Performance | vhp-${nproc}c-${ram}gb-{amd,intel}[-sc1] |
| voc-g | Optimized Cloud Compute : General Purpose | voc-g-${nproc}c-${ram}gb-${disk}s-amd[-sc1] |
| voc-c | Optimized Cloud Compute : CPU Optimized | voc-c-${nproc}c-${ram}gb-${disk}s-amd[-sc1] |
| voc-m | Optimized Cloud Compute : Memory Optimized | voc-m-${nproc}c-${ram}gb-${disk}s-amd[-sc1] |
| voc-s | Optimized Cloud Compute : Storage Optimized | voc-s-${nproc}c-${ram}gb-${disk}s-amd[-sc1] |
| vcg | Cloud GPU | vcg-${gpu}-{nproc}c-${ram}g-${vram}vram |
(The "-sc1" prefix for the "sao" region (São Paulo, Brazil), which have
separate plan types because it's more expensive there.)
For selecting the appropriate region ID, here lis a list of the
regions in the US:
| ID | Location | HDD | NVMe |
|-----+----------------+-----+------|
| atl | Atlanta | yes | |
| dfw | Dallas | yes | |
| ewr | New Jersey | yes | yes |
| hnl | Honolulu | yes | |
| lax | Los Angeles | yes | yes |
| mia | Miami | yes | |
| ord | Chicago | yes | |
| sea | Seattle | yes | |
| sjc | Silicon Valley | yes | |
EOF
}
run() {
local arg_name arg_plan arg_region arg_backup
arg_name=$1
arg_plan=$2
arg_region=$3
arg_backup=$4
export VULTR_API_KEY=$(cat secrets/vultr-api-key.txt)
# Upload ISO
printf >&2 ':: Uploading bootstrap ISO to Vultr S3...\n'
local iso_file iso_label iso_url
iso_file="images/${arg_name%%.*}.bootstrap.iso"
iso_label=$(xorriso -indev "$iso_file" |& sed -En "s/^Volume id *: *'(.*)'$/\1/p")
iso_url=$(bin/vultr-upload "$iso_file" "s3://umorpha-images/${iso_label}.iso")
# Set ISO as an ISO
printf >&2 ':: Converting from Vultr S3 to Vultr ISO...\n'
local iso_id
iso_id=$(bin/vultr-iso-id "$iso_url")
# Set up instance
local instance_id
instance_id=$(bin/vultr-api "instances?label=${arg_name}" | jq -r '.instances[]|.id')
local secrets
secrets=$(cat "images/${arg_name%%.*}.secrets.sh")
local user_data
user_data="#!/bin/bash
set -e
vultr-api() {
curl \\
--no-progress-meter \\
--fail-with-body \\
-H 'Authorization: Bearer ${VULTR_API_KEY}' \\
-H 'Content-Type: application/json' \\
\"https://api.vultr.com/v2/\$@\"
}
set -x
umorpha-mount \
--root=/dev/mapper/vg_umorpha-lv_root_a:auto:ro \
--overlay=/dev/mapper/vg_umorpha-lv_root_overlay:auto:rw \
--basedir=/run/umorpha-root
systemd-sysusers --root=/run/umorpha-root/mnt
arch-chroot -- /run/umorpha-root/mnt sh -c ${secrets@Q}
umount /run/umorpha-root/mnt
umount /run/umorpha-root/overlay
umount /run/umorpha-root/root
instance_id=\$(curl --fail-with-body --no-progress-meter http://169.254.169.254/v1/instance-v2-id)
vultr-api instances/\${instance_id} -XPATCH --data '{\"user_data\":\"\"}'
# NB: iso/detach halts the VM before the HTTP response comes back, so
# this call never gets to finish.
#
# 2024-02-12: Looks like iso/detach no longer halts the VM?
vultr-api instances/\${instance_id}/iso/detach -XPOST
"
if [[ -z "$instance_id" ]]; then
printf >&2 ':: Creating instance...\n'
vultr-cli instance create \
--host="$arg_name" \
--label="$arg_name" \
\
--plan="$arg_plan" \
--region="$arg_region" \
--ipv6=true \
--auto-backup="$arg_backup" \
\
--iso="$iso_id" \
\
--userdata="$user_data"
else
printf >&2 ':: Updating instance %s...\n' "$instance_id"
# Determine what we need to patch.
instance_data="$(bin/vultr-api instances/$instance_id)"
declare -A patch
patch[user_data]="$(base64 --wrap=0 <<<"$user_data")"
if [[ "$(jq -r .instance.region <<<"$instance_data")" != "$arg_region" ]]; then
patch[region]="$arg_region"
fi
if [[ "$(jq -r .instance.plan <<<"$instance_data")" != "$arg_plan" ]]; then
patch[plan]="$arg_plan"
fi
# Format that patch as JSON.
data='{'
for k in "${!patch[@]}"; do
data+="\"$k\":\"${patch[$k]}\","
done
data="${data%,}}"
# Apply.
printf >&2 ' -> Halting instance...\n'
bin/vultr-api instances/$instance_id/halt -XPOST
printf >&2 ' -> Updating instance...\n'
bin/vultr-api instances/$instance_id -XPATCH --data "$data" | jq
printf >&2 ' -> Attaching ISO...\n'
resp=$(bin/vultr-api instances/$instance_id/iso/attach -XPOST --data "{\"iso_id\":\"${iso_id}\"}")
resp_state=$(jq -r .iso_status.state <<<"$resp")
resp_id=$(jq -r .iso_status.iso_id <<<"$resp")
while ! [[ "$resp_state" == isomounted && "$resp_id" == "$iso_id" ]]; do
printf >&2 ' instance iso: state=%q id=%q\n' "$resp_state" "$resp_id"
sleep 1
resp=$(bin/vultr-api instances/$instance_id/iso)
resp_state=$(jq -r .iso_status.state <<<"$resp")
resp_id=$(jq -r .iso_status.iso_id <<<"$resp")
done
printf >&2 ' instance iso: state=%q id=%q\n' "$resp_state" "$resp_id"
printf >&2 ' -> Starting instance...\n'
bin/vultr-api instances/$instance_id/start -XPOST
fi
}
main() {
local arg_name=
local arg_plan=
local arg_region=
local arg_backup=true
local arg_mode=run
local args
if ! args="$(getopt -n "${0##*/}" -o '' -l 'help,name:,plan:,region:,no-backup' -- "$@")"; then
arg_mode=error
else
eval "set -- $args"
while true; do
case "$1" in
--help) shift; arg_mode=help;;
--name) shift; arg_name=$1; shift;;
--plan) shift; arg_plan=$1; shift;;
--region) shift; arg_region=$1; shift;;
--no-backup) arg_backup=false; shift;;
--) shift; break;;
esac
done
if [[ $@ -gt 0 ]]; then
printf >&2 '%s: error: unexpected positional arguments: %s' "${*@Q}"
arg_mode=error
fi
if [[ -z "$arg_name" ]]; then
printf >&2 '%s: error: --name=HOSTNAME is required\n' "${0##*/}"
arg_mode=error
fi
if [[ -z "$arg_plan" ]]; then
printf >&2 '%s: error: --plan=PLAN_ID is required\n' "${0##*/}"
arg_mode=error
fi
if [[ -z "$arg_region" ]]; then
printf >&2 '%s: error: --region=REGION_ID is required\n' "${0##*/}"
arg_mode=error
fi
case "$arg_mode" in
error) printf >&2 "Try '%q --help' for more information\n" "${0##*/}"; exit 2;;
help) show_help;;
run) run "$arg_name" "$arg_plan" "$arg_region" "$arg_backup";;
esac
fi
}
main "$@"