329 lines
9.3 KiB
Bash
329 lines
9.3 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
#######################################
|
|
# STATE TRACKING
|
|
#######################################
|
|
STATE_FILE="/var/log/pterodactyl_node_state"
|
|
mkdir -p $(dirname "$STATE_FILE")
|
|
touch "$STATE_FILE"
|
|
|
|
mark_done() {
|
|
echo "$1" >> "$STATE_FILE"
|
|
}
|
|
|
|
is_done() {
|
|
grep -qx "$1" "$STATE_FILE" || false
|
|
}
|
|
|
|
save_var() {
|
|
echo "$1=$2" >> "$STATE_FILE"
|
|
}
|
|
|
|
load_var() {
|
|
grep "^$1=" "$STATE_FILE" | cut -d= -f2
|
|
}
|
|
|
|
#######################################
|
|
# CONFIG
|
|
#######################################
|
|
PANEL_URL="https://panel.amslabs.net"
|
|
PTERO_API_KEY="ptla_1qpEnNZxXFAPAvjJrtRdlX5oRaDWYBImKuizXMzYne1"
|
|
PTERO_LOCATION_ID=1
|
|
|
|
CLOUDFLARE_API_TOKEN="tIrFGHOC40BuhlAsRKJYWLm-k0SH9fsnGFoKEqI4"
|
|
CLOUDFLARE_ZONE_ID="902bc1958422630ff8ce974e84af3a05"
|
|
DOMAIN="amslabs.net"
|
|
PUBLIC_IP="121.99.242.84"
|
|
|
|
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/1460095439930916915/PJiafXkssc4NyLXdCAGGdvGxwpo93z4Gso_LBSGKKX_E9k0CDm8Uq2dbTtYXNJzXcJHN"
|
|
|
|
HOSTS_TEMPLATE="/etc/cloud/templates/hosts.debian.tmpl"
|
|
HOSTS_FILE="/etc/hosts"
|
|
WINGS_BIN="/usr/local/bin/wings"
|
|
WINGS_DIR="/etc/pterodactyl"
|
|
|
|
#######################################
|
|
# INPUTS
|
|
#######################################
|
|
if ! is_done "input_hostname"; then
|
|
read -rp "Enter host name (e.g. node03): " HOSTNAME
|
|
save_var "HOSTNAME" "$HOSTNAME"
|
|
else
|
|
HOSTNAME=$(load_var HOSTNAME)
|
|
fi
|
|
|
|
FQDN="${HOSTNAME}.${DOMAIN}"
|
|
|
|
if ! is_done "daemon_ports"; then
|
|
read -rp "Enter Wings daemon listen port (e.g. 8082): " DAEMON_LISTEN
|
|
read -rp "Enter Wings daemon SFTP port (e.g. 2024): " DAEMON_SFTP
|
|
save_var "DAEMON_LISTEN" "$DAEMON_LISTEN"
|
|
save_var "DAEMON_SFTP" "$DAEMON_SFTP"
|
|
else
|
|
DAEMON_LISTEN=$(load_var DAEMON_LISTEN)
|
|
DAEMON_SFTP=$(load_var DAEMON_SFTP)
|
|
fi
|
|
|
|
if ! is_done "resources"; then
|
|
echo "Select memory option:"
|
|
echo "1) 32768 MB"
|
|
echo "2) 65536 MB"
|
|
echo "3) 92160 MB"
|
|
read -rp "Choice [1-3]: " MEM_CHOICE
|
|
case "$MEM_CHOICE" in
|
|
1) MEMORY=32768 ;;
|
|
2) MEMORY=65536 ;;
|
|
3) MEMORY=92160 ;;
|
|
*) MEMORY=65536 ;;
|
|
esac
|
|
|
|
echo "Select disk option:"
|
|
echo "1) 32768 MB"
|
|
echo "2) 65536 MB"
|
|
echo "3) 92160 MB"
|
|
read -rp "Choice [1-3]: " DISK_CHOICE
|
|
case "$DISK_CHOICE" in
|
|
1) DISK=32768 ;;
|
|
2) DISK=65536 ;;
|
|
3) DISK=92160 ;;
|
|
*) DISK=65536 ;;
|
|
esac
|
|
|
|
save_var "MEMORY" "$MEMORY"
|
|
save_var "DISK" "$DISK"
|
|
else
|
|
MEMORY=$(load_var MEMORY)
|
|
DISK=$(load_var DISK)
|
|
fi
|
|
|
|
echo "🚀 Bootstrapping ${FQDN}"
|
|
|
|
#######################################
|
|
# HOSTS TEMPLATE
|
|
#######################################
|
|
if ! is_done "hosts_template"; then
|
|
sudo tee "${HOSTS_TEMPLATE}" >/dev/null <<EOF
|
|
127.0.0.1 localhost
|
|
|
|
10.30.40.101 node01.amslabs.net
|
|
10.30.40.101 panel.amslabs.net
|
|
10.30.40.102 node02.amslabs.net
|
|
10.30.40.103 ${FQDN}
|
|
|
|
::1 localhost ip6-localhost ip6-loopback
|
|
ff02::1 ip6-allnodes
|
|
ff02::2 ip6-allrouters
|
|
EOF
|
|
sudo cp "${HOSTS_TEMPLATE}" "${HOSTS_FILE}"
|
|
mark_done "hosts_template"
|
|
fi
|
|
|
|
#######################################
|
|
# CLOUDFLARE DNS
|
|
#######################################
|
|
if ! is_done "cloudflare_dns"; then
|
|
echo "🌐 Updating Cloudflare DNS"
|
|
RECORD_ID=$(curl -s \
|
|
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
|
|
"https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/dns_records?type=A&name=${FQDN}" \
|
|
| jq -r '.result[0].id // empty')
|
|
|
|
DNS_PAYLOAD=$(jq -n \
|
|
--arg name "$FQDN" \
|
|
--arg ip "$PUBLIC_IP" \
|
|
'{type:"A",name:$name,content:$ip,ttl:120,proxied:false}')
|
|
|
|
if [[ -n "$RECORD_ID" ]]; then
|
|
curl -s -X PUT \
|
|
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
--data "$DNS_PAYLOAD" \
|
|
"https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/dns_records/${RECORD_ID}" >/dev/null
|
|
else
|
|
curl -s -X POST \
|
|
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
--data "$DNS_PAYLOAD" \
|
|
"https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/dns_records" >/dev/null
|
|
fi
|
|
mark_done "cloudflare_dns"
|
|
fi
|
|
|
|
#######################################
|
|
# PACKAGES
|
|
#######################################
|
|
if ! is_done "packages"; then
|
|
sudo apt update
|
|
sudo apt install -y curl jq docker.io certbot python3-certbot-dns-cloudflare
|
|
sudo systemctl enable docker --now
|
|
mark_done "packages"
|
|
fi
|
|
|
|
#######################################
|
|
# CREATE NODE
|
|
#######################################
|
|
if ! is_done "node_created"; then
|
|
echo "📦 Creating node in panel"
|
|
|
|
NODE_PAYLOAD=$(jq -n \
|
|
--arg name "$HOSTNAME" \
|
|
--arg fqdn "$FQDN" \
|
|
--argjson location "$PTERO_LOCATION_ID" \
|
|
--argjson memory "$MEMORY" \
|
|
--argjson disk "$DISK" \
|
|
--argjson daemon_listen "$DAEMON_LISTEN" \
|
|
--argjson daemon_sftp "$DAEMON_SFTP" \
|
|
'{
|
|
name: $fqdn,
|
|
location_id: $location,
|
|
fqdn: $fqdn,
|
|
scheme: "https",
|
|
memory: $memory,
|
|
memory_overallocate: 0,
|
|
disk: $disk,
|
|
disk_overallocate: 0,
|
|
daemon_listen: $daemon_listen,
|
|
daemon_sftp: $daemon_sftp,
|
|
daemon_base: "/var/lib/pterodactyl/volumes",
|
|
upload_size: 100
|
|
}')
|
|
|
|
NODE_RESPONSE=$(curl -s -X POST \
|
|
"${PANEL_URL}/api/application/nodes" \
|
|
-H "Authorization: Bearer ${PTERO_API_KEY}" \
|
|
-H "Accept: Application/vnd.pterodactyl.v1+json" \
|
|
-H "Content-Type: application/json" \
|
|
--data "$NODE_PAYLOAD")
|
|
|
|
echo "📄 Node creation response:"
|
|
echo "$NODE_RESPONSE" | jq
|
|
|
|
NODE_ID=$(echo "$NODE_RESPONSE" | jq -r '.attributes.id // empty')
|
|
if [[ -z "$NODE_ID" ]]; then
|
|
echo "❌ Node creation failed, check API key, permissions, and values."
|
|
exit 1
|
|
fi
|
|
|
|
save_var "NODE_ID" "$NODE_ID"
|
|
mark_done "node_created"
|
|
fi
|
|
|
|
#######################################
|
|
# INSTALL WINGS
|
|
#######################################
|
|
if ! is_done "wings_installed"; then
|
|
sudo mkdir -p "${WINGS_DIR}"
|
|
curl -Lo "${WINGS_BIN}" https://github.com/pterodactyl/wings/releases/latest/download/wings_linux_amd64
|
|
sudo chmod +x "${WINGS_BIN}"
|
|
|
|
sudo tee /etc/systemd/system/wings.service >/dev/null <<EOF
|
|
[Unit]
|
|
Description=Pterodactyl Wings Daemon
|
|
After=docker.service
|
|
|
|
[Service]
|
|
User=root
|
|
Restart=always
|
|
ExecStart=${WINGS_BIN}
|
|
LimitNOFILE=65536
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
sudo systemctl daemon-reload
|
|
mark_done "wings_installed"
|
|
fi
|
|
|
|
#######################################
|
|
# FETCH CONFIG FROM PANEL
|
|
#######################################
|
|
if ! is_done "node_deployed"; then
|
|
echo "🧠 Fetching Wings config.yml from Panel"
|
|
NODE_ID=$(load_var NODE_ID)
|
|
for i in {1..3}; do
|
|
CONFIG_RESPONSE=$(curl -s -X GET \
|
|
"${PANEL_URL}/api/application/nodes/${NODE_ID}/configuration" \
|
|
-H "Authorization: Bearer ${PTERO_API_KEY}" \
|
|
-H "Accept: Application/vnd.pterodactyl.v1+json")
|
|
|
|
CONFIG=$(echo "$CONFIG_RESPONSE" | jq '.')
|
|
|
|
if [[ -n "$CONFIG" ]]; then
|
|
echo "$CONFIG" | sudo tee "${WINGS_DIR}/config.yml" >/dev/null
|
|
sudo chmod 600 "${WINGS_DIR}/config.yml"
|
|
mark_done "node_deployed"
|
|
echo "✅ Fetched config.yml successfully"
|
|
break
|
|
else
|
|
echo "❌ Failed to fetch config, attempt $i/3"
|
|
sleep 5
|
|
fi
|
|
done
|
|
|
|
if [[ ! -f "${WINGS_DIR}/config.yml" ]]; then
|
|
echo "❌ Could not fetch config.yml from Panel"
|
|
echo "$CONFIG_RESPONSE" | jq
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
#######################################
|
|
# CERTBOT
|
|
#######################################
|
|
if ! is_done "certbot_done"; then
|
|
sudo mkdir -p /root/.secrets/certbot
|
|
sudo tee /root/.secrets/certbot/cloudflare.ini >/dev/null <<EOF
|
|
dns_cloudflare_api_token = ${CLOUDFLARE_API_TOKEN}
|
|
EOF
|
|
sudo chmod 600 /root/.secrets/certbot/cloudflare.ini
|
|
|
|
sudo certbot certonly \
|
|
--dns-cloudflare \
|
|
--dns-cloudflare-credentials /root/.secrets/certbot/cloudflare.ini \
|
|
--dns-cloudflare-propagation-seconds 30 \
|
|
-d "${FQDN}" \
|
|
--agree-tos --non-interactive \
|
|
-m admin@${DOMAIN}
|
|
|
|
mark_done "certbot_done"
|
|
fi
|
|
|
|
#######################################
|
|
# CERT RENEW HOOK
|
|
#######################################
|
|
if ! is_done "cert_hook"; then
|
|
sudo tee /usr/local/bin/wings-cert-renew.sh >/dev/null <<EOF
|
|
#!/usr/bin/env bash
|
|
systemctl restart wings
|
|
curl -X POST -H "Content-Type: application/json" \
|
|
-d "{\"content\":\"🔁 Wings restarted after cert renewal on $(hostname)\"}" \
|
|
"${DISCORD_WEBHOOK_URL}"
|
|
EOF
|
|
sudo chmod +x /usr/local/bin/wings-cert-renew.sh
|
|
sudo ln -sf /usr/local/bin/wings-cert-renew.sh \
|
|
/etc/letsencrypt/renewal-hooks/deploy/wings-restart.sh
|
|
mark_done "cert_hook"
|
|
fi
|
|
|
|
#######################################
|
|
# START WINGS
|
|
#######################################
|
|
if ! is_done "wings_started"; then
|
|
sudo systemctl enable wings
|
|
sudo systemctl restart wings
|
|
mark_done "wings_started"
|
|
fi
|
|
|
|
#######################################
|
|
# FINAL NOTIFY
|
|
#######################################
|
|
if ! is_done "final_notify"; then
|
|
curl -X POST -H "Content-Type: application/json" \
|
|
-d "{\"content\":\"✅ Pterodactyl node ${FQDN} deployed and online\"}" \
|
|
"${DISCORD_WEBHOOK_URL}"
|
|
mark_done "final_notify"
|
|
fi
|
|
|
|
echo "🎉 BOOTSTRAP COMPLETE" |