Skip to content

Instantly share code, notes, and snippets.

@PhrozenByte
Last active May 14, 2022 21:50
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save PhrozenByte/9d72a89f271bbbb5f2c81add9e8af1b1 to your computer and use it in GitHub Desktop.
Save PhrozenByte/9d72a89f271bbbb5f2c81add9e8af1b1 to your computer and use it in GitHub Desktop.
Shell script to create a snapshot of a Hetzner CX line virtual server

hetzner-snapshot

Shell script to create a snapshot of a Hetzner CX line virtual server.

Install

# script
mv hetzner-snapshot.sh /usr/local/sbin/hetzner-snapshot
chmod +x /usr/local/sbin/hetzner-snapshot

# lib
mkdir /usr/local/lib/hetzner_api
mv lib.sh /usr/local/lib/hetzner_api/

# config file (don't forget to set strict permissions)
( umask 077 && touch /etc/hetzner_api )
cat hetzner_api.conf >> /etc/hetzner_api
#!/bin/bash
# Create Hetzner virtual server snapshots
# Version 1.2 (build 20190723)
#
# Copyright (C) 2017-2019 Daniel Rudolf <www.daniel-rudolf.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# See <http://www.gnu.org/licenses/> to receive a full-text-copy of
# the GNU General Public License.
APP_NAME="$(basename "$0")"
set -e
VERSION="1.2"
BUILD="20190723"
if [ ! -e /etc/hetzner_api ]; then
echo "$APP_NAME: Unable to read Hetzner API credentials from '/etc/hetzner_api': No such file or directory" >&2
exit 1
elif [ ! -f /etc/hetzner_api ]; then
echo "$APP_NAME: Unable to read Hetzner API credentials from '/etc/hetzner_api': Not a regular file" >&2
exit 1
elif [ ! -r /etc/hetzner_api ]; then
echo "$APP_NAME: Unable to read Hetzner API credentials from '/etc/hetzner_api': Permission denied" >&2
exit 1
fi
. /etc/hetzner_api
. /usr/local/lib/hetzner_api/lib.sh
print_usage() {
echo "Usage:"
echo " $APP_NAME [--latest|--oldest] IP_ADDRESS"
}
# read parameters
SERVER_IP=""
SNAPSHOT_SELECT="latest"
while [ $# -gt 0 ]; do
if [ "$1" == "--oldest" ] || [ "$1" == "-o" ]; then
SNAPSHOT_SELECT="oldest"
elif [ "$1" == "--latest" ] || [ "$1" == "-l" ]; then
SNAPSHOT_SELECT="latest"
elif [ "$1" == "--help" ] || [ "$1" == "-h" ]; then
print_usage
echo
echo "Application options:"
echo " -l, --latest replace the server's latest snapshot (default)"
echo " -o, --oldest replace the server's oldest snapshot"
echo
echo "Help options:"
echo " -h, --help display this help and exit"
echo " --version output version information and exit"
exit 0
elif [ "$1" == "--version" ]; then
echo "hetzner-snapshot.sh $VERSION ($BUILD)"
echo "Copyright (C) 2017-2019 Daniel Rudolf"
echo "License GPLv3: GNU GPL version 3 only <http://gnu.org/licenses/gpl.html>."
echo "This is free software: you are free to change and redistribute it."
echo "There is NO WARRANTY, to the extent permitted by law."
echo
echo "Written by Daniel Rudolf <http://www.daniel-rudolf.de/>"
exit 0
elif [ -z "$SERVER_IP" ]; then
if ! [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "$APP_NAME: Invalid IP address specified" >&2
exit 1
fi
SERVER_IP="$1"
else
print_usage >&2
exit 1
fi
shift
done
if [ -z "$SERVER_IP" ]; then
print_usage >&2
exit 1
fi
# function: returns the latest or oldest snapshot
get_snapshot() {
local SNAPSHOT=""
local SNAPSHOT_RESPOSNE=""
hetzner_api_call --output-var SNAPSHOT_RESPONSE --show-error "GET" "/snapshot/$SERVER_IP" >&2
SNAPSHOT="$(echo "$SNAPSHOT_RESPONSE" | php -r '
$json = json_decode(stream_get_contents(STDIN), true);
if (is_array($json)) {
if (empty($json)) {
exit(0);
}
$json = (isset($_SERVER["argv"]) && ($_SERVER["argv"] === "--latest")) ? array_reverse($json) : $json;
foreach ($json as $item) {
if (isset($item["snapshot"]) && ($item["snapshot"]["type"] === "snapshot")) {
echo $item["snapshot"]["id"];
exit(0);
}
}
}
exit(1);
' -- "--$SNAPSHOT_SELECT")"
if [ $? -ne 0 ]; then
echo "$APP_NAME: Hetzner API call 'GET /snapshot/$SERVER_IP' failed: Invalid JSON response: $SNAPSHOT_RESPONSE" >&2
exit 1
fi
if [ -n "$SNAPSHOT" ]; then
echo "$SNAPSHOT"
return 0
fi
return 1
}
# get list of existing snapshots
echo "Retrieving list of snapshots..."
SNAPSHOT="$(get_snapshot || true)"
# delete the latest snapshot
if [ -n "$SNAPSHOT" ]; then
echo -n "Deleting $SNAPSHOT_SELECT snapshot"
hetzner_api_call --show-error "DELETE" "/snapshot/$SERVER_IP/$SNAPSHOT"
# deleting a snapshot might take some time
for (( i=1 ; i < 100 ; i++ )); do
echo -n "."
if [ $(($i % 3)) -ne 0 ]; then
sleep 3
elif [ "$(get_snapshot || true)" != "$SNAPSHOT" ]; then
break
fi
done
echo
if [ $i -eq 100 ]; then
echo "$APP_NAME: Unable to delete latest snapshot" >&2
exit 1
fi
fi
# create a new snapshot
echo "Creating snapshot..."
hetzner_api_call --show-error "POST" "/snapshot/$SERVER_IP"
HETZNER_API="https://robot-ws.your-server.de"
HETZNER_LOGIN="#ws+XXXXXXXX"
HETZNER_PASSWORD="password"
# Hetzner API library
# Version 1.2 (build 20190723)
#
# Copyright (C) 2017-2019 Daniel Rudolf <www.daniel-rudolf.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# See <http://www.gnu.org/licenses/> to receive a full-text-copy of
# the GNU General Public License.
if [ -z "$APP_NAME" ]; then
APP_NAME="$(basename "$0")"
fi
if [ -z "$HETZNER_API" ]; then
HETZNER_API="http://hetzner.invalid/"
fi
if [ -z "$HETZNER_LOGIN" ]; then
HETZNER_LOGIN="anonymous"
fi
if [ -z "$HETZNER_PASSWORD" ]; then
HETZNER_PASSWORD=""
fi
hetzner_api_call() {
local __METHOD=""
local __URL=""
local __SHOW_ERROR="no"
local __PRINT_STATUS="no"
local __RESULT_STATUS=""
local __PRINT_DOCUMENT="no"
local __RESULT_DOCUMENT=""
local __DUMP_HEADER="$(mktemp)"
local __DUMP_DOCUMENT="$(mktemp)"
local __CURL_ARGS=()
local __CURL_STATUS=0
while [ $# -gt 0 ]; do
if [ "$1" == "--show-error" ]; then
__SHOW_ERROR="yes"
elif [ "$1" == "--status" ]; then
__PRINT_STATUS="yes"
elif [ "$1" == "--status-var" ]; then
if ! [[ "$2" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
echo "$APP_NAME: Invalid Hetzner API status variable name '$2'" >&2
exit 1
fi
__RESULT_STATUS="$2"
shift
elif [ "$1" == "--output" ]; then
__PRINT_DOCUMENT="yes"
elif [ "$1" == "--output-var" ]; then
if ! [[ "$2" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
echo "$APP_NAME: Invalid Hetzner API document variable name '$2'" >&2
exit 1
fi
__RESULT_DOCUMENT="$2"
shift
elif [ -z "$__METHOD" ]; then
__METHOD="$1"
elif [ -z "$__URL" ]; then
__URL="${1#/}"
else
__CURL_ARGS+=( "$1" )
fi
shift
done
curl --silent --show-error \
--dump-header "$__DUMP_HEADER" \
--output "$__DUMP_DOCUMENT" \
--user "$HETZNER_LOGIN:$HETZNER_PASSWORD" \
"${__CURL_ARGS[@]}" \
--request "$__METHOD" "$HETZNER_API/$__URL" \
|| { __CURL_STATUS=$?; true; }
if [ $__CURL_STATUS -ne 0 ]; then
echo "$APP_NAME: Hetzner API call '$__METHOD /$__URL' failed: Execution of cURL failed" >&2
exit 1
fi
local __HTTP_STATUS="$(head -n1 "$__DUMP_HEADER")"
if ! [[ "$__HTTP_STATUS" =~ ^HTTP/[0-9]+(\.[0-9]+)*\ [0-9]{3}(\ |$) ]]; then
echo "$APP_NAME: Hetzner API call '$__METHOD /$__URL' failed: Invalid HTTP response" >&2
exit 1
fi
local __RESPONSE_CODE="$(echo "$__HTTP_STATUS" | cut -s -d " " -f 2)"
local __RESPONSE_CODE_INFO="$(echo "$__HTTP_STATUS" | cut -s -d " " -f 3-)"
if [ -n "$__RESULT_STATUS" ]; then
eval "$__RESULT_STATUS=\"$__RESPONSE_CODE $__RESPONSE_CODE_INFO\""
fi
if [ "$__PRINT_STATUS" == "yes" ]; then
echo "$__RESPONSE_CODE $__RESPONSE_CODE_INFO"
fi
if [ -n "$__RESULT_DOCUMENT" ]; then
eval "$__RESULT_DOCUMENT=\"\$(cat \"$__DUMP_DOCUMENT\")\""
fi
if [ "$__PRINT_DOCUMENT" == "yes" ]; then
cat "$__DUMP_DOCUMENT"
fi
rm "$__DUMP_HEADER"
rm "$__DUMP_DOCUMENT"
if [ $__RESPONSE_CODE -ge 200 ] && [ $__RESPONSE_CODE -lt 300 ]; then
return 0
elif [ "$__SHOW_ERROR" == "yes" ]; then
echo "$APP_NAME: Hetzner API call '$__METHOD /$__URL' failed: $__RESPONSE_CODE $__RESPONSE_CODE_INFO" >&2
return 1
else
return 1
fi
}
@mikulabc
Copy link

mikulabc commented Jan 5, 2021

ok, i will have to continue my search unless you have this already set up too? :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment