#!/usr/bin/env bash # Copyright (C) 2023 Umorpha Systems # SPDX-License-Identifier: AGPL-3.0-or-later set -euE -o pipefail usage() { cat <&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. 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 "$@"