232 lines
9.3 KiB
Bash
Executable File
232 lines
9.3 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# For my use, debian for the little one, ubuntu for the big ones
|
|
|
|
# From .env, following values:
|
|
# LINODE_API_KEY (used for all Linode API calls below, via curl)
|
|
# WHICH_DOMAIN
|
|
# ROOT_PASS
|
|
source .env
|
|
# All Linode API requests use the API key sourced from .env above:
|
|
# curl -H "Authorization: Bearer $LINODE_API_KEY" https://api.linode.com/v4/...
|
|
#ROOT_PASS=$(for w in {1..3}; do echo -n $(cat /usr/share/dict/words | grep -v "'" | sed -n $(echo "$(cat /usr/share/dict/words | grep -v "'" | wc -l) * $RANDOM / 32768 + 1" | bc)p); done)
|
|
#ROOT_PASS="StartRootPass5678"
|
|
SCRIPT_ACTION=$1
|
|
LINODE_LABEL=$2
|
|
LOGFILE="./linodes.log"
|
|
|
|
# Pre-existing Valheim volume, attached to every Linode at creation time.
|
|
# The filesystem (mkfs.ext4) was already created; see voldirections.txt.
|
|
VOLUME_ID="18073448"
|
|
VOLUME_NAME="ValVol202609"
|
|
VOLUME_DEVICE="/dev/disk/by-id/scsi-0Linode_Volume_$VOLUME_NAME"
|
|
VOLUME_MOUNT="/mnt/$VOLUME_NAME"
|
|
|
|
# Cloud Firewall applied to every Linode at creation time. It must allow the
|
|
# Valheim ports (2456-2457/udp) and, if you still need SSH, port 22.
|
|
FIREWALL_ID="165767433"
|
|
|
|
# StackScript that mounts the volume on first boot (see stackscript.md).
|
|
# Enter the numeric StackScript ID manually after creating it in the Linode UI.
|
|
#STACKSCRIPT_ID="2225110"
|
|
# Normal:
|
|
#STACKSCRIPT_ID="2225140"
|
|
# More resources (1.5):
|
|
#STACKSCRIPT_ID="2225166"
|
|
# Final, 1.5 resources:
|
|
STACKSCRIPT_ID="2225201"
|
|
|
|
if [[ "$SCRIPT_ACTION" == "" || "$SCRIPT_ACTION" == "--help" || "$SCRIPT_ACTION" == "-?" ]]; then
|
|
echo "Linode helper script: create-linode.sh <action> <label> [args]"
|
|
echo "Create: create-linode.sh c <new label>"
|
|
echo " (fixed size: 8GB shared, newest Debian image;"
|
|
echo " attaches and mounts volume $VOLUME_NAME)"
|
|
echo "Destroy: create-linode.sh d <label>"
|
|
echo "Connect: create-linode.sh x <label>"
|
|
echo " create-linode.sh x <label> get <remote source> <local dest>"
|
|
echo " create-linode.sh x <label> put <local source> <remote dest>"
|
|
echo "Update record: create-linode.sh a <label> <subdomain>"
|
|
echo " (updates the existing A record, creates one if missing)"
|
|
echo "List: create-linode.sh l"
|
|
echo " (lists all Linodes, no label needed)"
|
|
fi
|
|
|
|
# Create
|
|
if [[ "$SCRIPT_ACTION" == "c" ]]; then
|
|
|
|
# Fixed size: 8GB shared, newest Debian image
|
|
INSTANCE_SIZE="g6-standard-4"
|
|
INSTANCE_IMAGE="linode/debian13"
|
|
|
|
# The StackScript (see stackscript.md) runs as root on first boot and mounts
|
|
# the volume, so no SSH session is needed to configure it.
|
|
curl -s -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $LINODE_API_KEY" \
|
|
-X POST -d '{
|
|
"label": "'$LINODE_LABEL'",
|
|
"region": "us-central",
|
|
"type": "'$INSTANCE_SIZE'",
|
|
"image": "'$INSTANCE_IMAGE'",
|
|
"root_pass": "'$ROOT_PASS'",
|
|
"authorized_users": ["ni7ne_midpriv"],
|
|
"stackscript_id": '$STACKSCRIPT_ID',
|
|
"firewall_id": '$FIREWALL_ID'
|
|
}' \
|
|
https://api.linode.com/v4/linode/instances | jq .
|
|
|
|
# Wait for the Linode to finish provisioning instead of blindly sleeping.
|
|
# Poll the status until it reports "running", which is the state required
|
|
# before the volume can be attached. 60 attempts x 10s = 10 minute ceiling.
|
|
INSTANCE_STATUS=""
|
|
for _ in $(seq 1 60); do
|
|
INSTANCE_STATUS=$(curl -s -H "Authorization: Bearer $LINODE_API_KEY" https://api.linode.com/v4/linode/instances | jq -r '.data[] | select(.label == "'$LINODE_LABEL'") | .status')
|
|
if [[ "$INSTANCE_STATUS" == "running" ]]; then
|
|
break
|
|
fi
|
|
echo "Waiting for $LINODE_LABEL to reach running (status: ${INSTANCE_STATUS:-unknown})..."
|
|
sleep 10
|
|
done
|
|
|
|
if [[ "$INSTANCE_STATUS" != "running" ]]; then
|
|
echo "ERROR: $LINODE_LABEL did not reach running (last status: ${INSTANCE_STATUS:-unknown})." >&2
|
|
exit 1
|
|
fi
|
|
|
|
INSTANCE_IP=$(curl -s -H "Authorization: Bearer $LINODE_API_KEY" https://api.linode.com/v4/linode/instances | jq '.data[] | select(.label == "'$LINODE_LABEL'") | .ipv4[]' | sed -e "s/\"//g")
|
|
INSTANCE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_KEY" https://api.linode.com/v4/linode/instances | jq '.data[] | select(.label == "'$LINODE_LABEL'") | .id' | sed -e "s/\"//g")
|
|
echo "Instance IP: $INSTANCE_IP"
|
|
echo "Instance ID: $INSTANCE_ID"
|
|
|
|
# Attach the pre-existing Valheim volume (see voldirections.txt).
|
|
# The volume must be attached to a running Linode; the image boots on create.
|
|
curl -s -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $LINODE_API_KEY" \
|
|
-X POST -d '{
|
|
"linode_id": '$INSTANCE_ID',
|
|
"persist_across_boots": true
|
|
}' \
|
|
https://api.linode.com/v4/volumes/$VOLUME_ID/attach | jq .
|
|
|
|
# Give the volume a moment to appear on the instance before the StackScript
|
|
# (which runs on first boot) mounts it.
|
|
sleep 15
|
|
|
|
echo "$(date -Iminutes) --- Id:$INSTANCE_ID --- IP:$INSTANCE_IP Summoned $LINODE_LABEL with volume $VOLUME_ID." >>$LOGFILE
|
|
echo "Started with root pass $ROOT_PASS"
|
|
|
|
fi
|
|
|
|
# List
|
|
if [[ "$SCRIPT_ACTION" == "l" ]]; then
|
|
curl -s -H "Authorization: Bearer $LINODE_API_KEY" \
|
|
https://api.linode.com/v4/linode/instances | jq .
|
|
fi
|
|
|
|
# Destroy
|
|
if [[ "$SCRIPT_ACTION" == "d" ]]; then
|
|
INSTANCE_IP=$(curl -s -H "Authorization: Bearer $LINODE_API_KEY" https://api.linode.com/v4/linode/instances | jq '.data[] | select(.label == "'$LINODE_LABEL'") | .ipv4[]' | sed -e "s/\"//g")
|
|
INSTANCE_ID=$(curl -s -H "Authorization: Bearer $LINODE_API_KEY" https://api.linode.com/v4/linode/instances | jq '.data[] | select(.label == "'$LINODE_LABEL'") | .id' | sed -e "s/\"//g")
|
|
echo "Destroying ID:$INSTANCE_ID Hit Ctrl-C in the next 10 seconds to cancel."
|
|
sleep 15
|
|
|
|
# Shut the Linode down cleanly before deleting it, so systemd stops the
|
|
# Docker container and the Valheim server gets the chance to save the world
|
|
# rather than being cut off mid-write.
|
|
echo "Shutting down ID:$INSTANCE_ID..."
|
|
curl -s -H "Authorization: Bearer $LINODE_API_KEY" \
|
|
-X POST https://api.linode.com/v4/linode/instances/$INSTANCE_ID/shutdown | jq .
|
|
|
|
# Give the shutdown time to finish before the instance is deleted.
|
|
sleep 30
|
|
|
|
curl -s -H "Authorization: Bearer $LINODE_API_KEY" \
|
|
-X DELETE https://api.linode.com/v4/linode/instances/$INSTANCE_ID
|
|
echo "$(date -Iminutes) --- Id:$INSTANCE_ID --- IP:$INSTANCE_IP Destroyed $LINODE_LABEL." >>$LOGFILE
|
|
fi
|
|
|
|
# Connect
|
|
if [[ "$SCRIPT_ACTION" == "x" ]]; then
|
|
|
|
REMOTE_ACTION=$3
|
|
SCP_SOURCE=$4
|
|
SCP_DEST=$5
|
|
|
|
INSTANCE_IP=$(curl -s -H "Authorization: Bearer $LINODE_API_KEY" https://api.linode.com/v4/linode/instances | jq '.data[] | select(.label == "'$LINODE_LABEL'") | .ipv4[]' | sed -e "s/\"//g")
|
|
if [[ "$REMOTE_ACTION" == "get" ]]; then
|
|
echo "get </~/source> <localdest>"
|
|
scp $INSTANCE_IP:/home/finn/$SCP_SOURCE $SCP_DEST
|
|
fi
|
|
|
|
if [[ "$REMOTE_ACTION" == "put" ]]; then
|
|
echo "put <localsource> </~/dest>"
|
|
scp $SCP_SOURCE $INSTANCE_IP:/home/finn/$SCP_DEST
|
|
fi
|
|
|
|
if [[ "$REMOTE_ACTION" == "" ]]; then
|
|
echo "Connecting: $INSTANCE_IP"
|
|
ssh -o "StrictHostKeyChecking no" root@$INSTANCE_IP
|
|
fi
|
|
|
|
fi
|
|
|
|
# Update record
|
|
if [[ "$SCRIPT_ACTION" == "a" ]]; then
|
|
|
|
INSTANCE_IP=$(curl -s -H "Authorization: Bearer $LINODE_API_KEY" https://api.linode.com/v4/linode/instances | jq '.data[] | select(.label == "'$LINODE_LABEL'") | .ipv4[]' | sed -e "s/\"//g")
|
|
|
|
NI_SUBDOMAIN=$3
|
|
DOMAIN_RECORDS=$(curl -s -H "Authorization: Bearer $LINODE_API_KEY" \
|
|
https://api.linode.com/v4/domains/$WHICH_DOMAIN/records)
|
|
|
|
# Only A records are managed by this action, so filter out the rest
|
|
# (CNAME, MX, TXT, ...) when showing the current state.
|
|
echo "Previous value:"
|
|
echo "$DOMAIN_RECORDS" | jq '.data[] | select(.type == "A")'
|
|
|
|
sleep 3
|
|
|
|
# Update the A record that already exists for this subdomain rather than
|
|
# adding another one; a record is only created when there is nothing to
|
|
# update. DNS names are case-insensitive, so compare them that way.
|
|
RECORD_IDS=$(echo "$DOMAIN_RECORDS" | jq -r --arg name "$NI_SUBDOMAIN" \
|
|
'.data[] | select(.type == "A" and ((.name // "") | ascii_downcase) == ($name | ascii_downcase)) | .id')
|
|
RECORD_ID=$(echo "$RECORD_IDS" | head -n 1)
|
|
DUPLICATE_IDS=$(echo "$RECORD_IDS" | tail -n +2)
|
|
|
|
echo "Updating records with ip $INSTANCE_IP..."
|
|
|
|
#ni7ne:
|
|
if [[ -n "$RECORD_ID" ]]; then
|
|
curl -s -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $LINODE_API_KEY" \
|
|
-X PUT -d '{
|
|
"type": "A",
|
|
"name": "'$NI_SUBDOMAIN'",
|
|
"target": "'$INSTANCE_IP'",
|
|
"ttl_sec": 14400
|
|
}' \
|
|
https://api.linode.com/v4/domains/$WHICH_DOMAIN/records/$RECORD_ID | jq .
|
|
|
|
# Duplicates left behind by earlier runs would keep resolving to an old
|
|
# IP, so remove everything except the record that was just updated.
|
|
for DUPLICATE_ID in $DUPLICATE_IDS; do
|
|
echo "Deleting duplicate A record $DUPLICATE_ID for $NI_SUBDOMAIN..."
|
|
curl -s -H "Authorization: Bearer $LINODE_API_KEY" \
|
|
-X DELETE https://api.linode.com/v4/domains/$WHICH_DOMAIN/records/$DUPLICATE_ID
|
|
done
|
|
else
|
|
curl -s -H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer $LINODE_API_KEY" \
|
|
-X POST -d '{
|
|
"type": "A",
|
|
"name": "'$NI_SUBDOMAIN'",
|
|
"target": "'$INSTANCE_IP'",
|
|
"ttl_sec": 14400
|
|
}' \
|
|
https://api.linode.com/v4/domains/$WHICH_DOMAIN/records | jq .
|
|
fi
|
|
|
|
echo "Update complete."
|
|
|
|
fi
|