feat: add minecraft backup method

Minecraft server can now be backed up using the restic-compose-backup.minecraft flag
This commit is contained in:
Silthus 2020-11-25 17:06:24 +01:00 committed by Silthus
parent 02ae4ca6d8
commit 9eb050173f
10 changed files with 75 additions and 195 deletions

1
.gitignore vendored
View File

@ -20,6 +20,7 @@ venv
restic_data/
restic_cache/
alerts.env
minecraft/
# build
build/

View File

@ -67,6 +67,17 @@ services:
volumes:
- pgdata:/var/lib/postgresql/data
minecraft:
image: itzg/minecraft-server
labels:
restic-compose-backup.minecraft: true
restic-compose-backup.volumes.include: "minecraft"
environment:
- RCON_PASSWORD=minecraft
- EULA=TRUE
volumes:
- ./minecraft:/data
volumes:
mysqldata:
mariadbdata:

View File

@ -37,8 +37,5 @@ ENV XDG_CACHE_HOME=/cache
# end install
ADD backup.sh /backup.sh
RUN chmod +x /backup.sh
ENTRYPOINT []
CMD ["./entrypoint.sh"]

View File

@ -1,158 +0,0 @@
#!/bin/bash
set -euo pipefail
if [ "${DEBUG:-false}" == "true" ]; then
set -x
fi
: "${RCON_HOST:=localhost}"
: "${RCON_PORT:=25575}"
: "${RCON_PASSWORD:=minecraft}"
export RCON_HOST
export RCON_PORT
export RCON_PASSWORD
###############
## common ##
## functions ##
###############
is_elem_in_array() {
# $1 = element
# All remaining arguments are array to search for the element in
if [ "$#" -lt 2 ]; then
log INTERNALERROR "Wrong number of arguments passed to is_elem_in_array function"
return 2
fi
local element="${1}"
shift
local e
for e; do
if [ "${element}" == "${e}" ]; then
return 0
fi
done
return 1
}
log() {
if [ "$#" -lt 1 ]; then
log INTERNALERROR "Wrong number of arguments passed to log function"
return 2
fi
local level="${1}"
shift
local valid_levels=(
"INFO"
"WARN"
"ERROR"
"INTERNALERROR"
)
if ! is_elem_in_array "${level}" "${valid_levels[@]}"; then
log INTERNALERROR "Log level ${level} is not a valid level."
return 2
fi
(
# If any arguments are passed besides log level
if [ "$#" -ge 1 ]; then
# then use them as log message(s)
<<<"${*}" cat -
else
# otherwise read log messages from standard input
cat -
fi
if [ "${level}" == "INTERNALERROR" ]; then
echo "Please report this: https://github.com/itzg/docker-mc-backup/issues"
fi
) | awk -v level="${level}" '{ printf("%s %s %s\n", strftime("%FT%T%z"), level, $0); fflush(); }'
} >&2
retry() {
if [ "$#" -lt 3 ]; then
log INTERNALERROR "Wrong number of arguments passed to retry function"
return 1
fi
# How many times should we retry?
# Value smaller than zero means infinitely
local retries="${1}"
# Time to sleep between retries
local interval="${2}"
readonly retries interval
shift 2
if (( retries < 0 )); then
local retries_msg="infinite"
else
local retries_msg="${retries}"
fi
local i=-1 # -1 since we will increment it before printing
while (( retries >= ++i )) || [ "${retries_msg}" != "${retries}" ]; do
# Send SIGINT after 5 minutes. If it doesn't shut down in 30 seconds, kill it.
if output="$(timeout --signal=SIGINT --kill-after=30s 5m "${@}" 2>&1 | tr '\n' '\t')"; then
log INFO "Command executed successfully ${*}"
return 0
else
log ERROR "Unable to execute ${*} - try ${i}/${retries_msg}. Retrying in ${interval}"
if [ -n "${output}" ]; then
log ERROR "Failure reason: ${output}"
fi
fi
# shellcheck disable=SC2086
sleep ${interval}
done
return 2
}
is_function() {
if [ "${#}" -ne 1 ]; then
log INTERNALERROR "is_function expects 1 argument, received ${#}"
fi
name="${1}"
[ "$(type -t "${name}")" == "function" ]
}
call_if_function_exists() {
if [ "${#}" -lt 1 ]; then
log INTERNALERROR "call_if_function_exists expects at least 1 argument, received ${#}"
return 2
fi
function_name="${1}"
if is_function "${function_name}"; then
eval "${@}"
else
log INTERNALERROR "${function_name} is not a valid function!"
return 2
fi
}
##########
## main ##
##########
log INFO "waiting for rcon readiness..."
# 20 times, 10 second delay
retry 20 10s rcon-cli save-on
if retry 5 10s rcon-cli save-off; then
# No matter what we were doing, from now on if the script crashes
# or gets shut down, we want to make sure saving is on
trap 'retry 5 5s rcon-cli save-on' EXIT
retry 5 10s rcon-cli save-all
retry 5 10s sync
rcb backup
retry 20 10s rcon-cli save-on
# Remove our exit trap now
trap EXIT
else
log ERROR "Unable to turn saving off. Is the server running?"
exit 1
fi

View File

@ -1 +1,2 @@
10 2 * * * . /env.sh && /backup.sh > /proc/1/fd/1 2>&1
10 2 * * * source /env.sh && rcb backup > /proc/1/fd/1

View File

@ -9,7 +9,7 @@ from restic_compose_backup import (
restic,
)
from restic_compose_backup.config import Config
from restic_compose_backup.containers import RunningContainers
from restic_compose_backup.containers import RunningContainers, Container
from restic_compose_backup import cron, utils
logger = logging.getLogger(__name__)
@ -94,6 +94,21 @@ def status(config, containers):
for container in backup_containers:
logger.info('service: %s', container.service_name)
if container.minecraft_backup_enabled:
instance = container.instance
ping = instance.ping()
logger.info(
' - %s (is_ready=%s):',
instance.container_type,
ping == 0
)
for mount in container.filter_mounts():
logger.info(
' - volume: %s -> %s',
mount.source,
container.get_volume_backup_destination(mount, '/minecraft'),
)
if container.volume_backup_enabled:
for mount in container.filter_mounts():
logger.info(

View File

@ -2,8 +2,8 @@ import os
class Config:
default_backup_command = ". /env.sh && /backup.sh > /proc/1/fd/1 2>&1"
default_crontab_schedule = "0 */4 * * *"
default_backup_command = "source /env.sh && rcb backup > /proc/1/fd/1"
default_crontab_schedule = "10 2 * * *"
"""Bag for config values"""
def __init__(self, check=True):

View File

@ -144,6 +144,7 @@ class Container:
return any([
self.volume_backup_enabled,
self.database_backup_enabled,
self.minecraft_backup_enabled
])
@property
@ -206,7 +207,7 @@ class Container:
exclude_bind_mounts = utils.is_true(config.exclude_bind_mounts)
mounts = list(filter(lambda m: not exclude_bind_mounts or m.type == "volume", self._mounts))
if not self.volume_backup_enabled:
if not self.volume_backup_enabled and not self.minecraft_backup_enabled:
return filtered
if self._include:

View File

@ -1,3 +1,6 @@
import os
import logging
from pathlib import Path
from restic_compose_backup.containers import Container
@ -9,6 +12,8 @@ from restic_compose_backup import (
)
from restic_compose_backup import utils
logger = logging.getLogger(__name__)
class MinecraftContainer(Container):
container_type = 'minecraft'
@ -21,16 +26,30 @@ class MinecraftContainer(Container):
'port': self.get_config_env('RCON_PORT'),
}
def prepare_mc_backup(self) -> bool:
creds = self.get_credentials()
with utils.environment('RCON_PASSWORD', creds['password']):
rcon.save_off(creds['host'], creds['port'])
rcon.save_all(creds['host'], creds['port'])
rcon.sync(creds['host'], creds['port'])
return True
def ping(self) -> bool:
"""Check the availability of the service"""
creds = self.get_credentials()
logger.debug("[rcon-cli] checking if minecraft server %s is online...", self.service_name)
with utils.environment('RCON_PASSWORD', creds['password']):
return rcon.is_online(
creds['host'],
creds['port']
)
try:
logger.debug("[rcon-cli] checking if minecraft server %s is online...", self.service_name)
with utils.environment('RCON_PASSWORD', creds['password']):
return rcon.is_online(
creds['host'],
creds['port']
)
except Exception as ex:
logger.error('[rcon-cli] unable to contact minecraft server %s', self.service_name)
logger.exception(ex)
return 1
def backup(self) -> bool:
config = Config()
@ -40,15 +59,16 @@ class MinecraftContainer(Container):
with utils.environment('RCON_PASSWORD', creds['password']):
try:
# turn off auto-save and sync all data to the disk before backing up worlds
prepare_mc_backup()
for mount in container.filter_mounts():
backup_data = container.get_volume_backup_destination(mount, '/volumes')
logger.info('Backing up %s', mount.source)
vol_result = restic.backup_files(config.repository, source=backup_data)
logger.debug('Minecraft backup exit code: %s', vol_result)
if vol_result != 0:
logger.error('Minecraft backup exited with non-zero code: %s', vol_result)
errors = True
self.prepare_mc_backup()
for mount in self.filter_mounts():
backup_data = self.get_volume_backup_destination(mount, '/minecraft')
logger.info('Backing up %s', mount.source)
vol_result = restic.backup_files(config.repository, source=backup_data)
logger.debug('Minecraft backup exit code: %s', vol_result)
if vol_result != 0:
logger.error('Minecraft backup exited with non-zero code: %s', vol_result)
errors = True
except Exception as ex:
logger.error('Exception raised during minecraft backup')
logger.exception(ex)
@ -57,13 +77,4 @@ class MinecraftContainer(Container):
# always always turn saving back on
rcon.save_on(creds['host'], creds['port'])
return errors
def prepare_mc_backup():
creds = self.get_credentials()
with utils.environment('RCON_PASSWORD', creds['password']):
rcon.save_off(creds['host'], creds['port'])
rcon.save_all(creds['host'], creds['port'])
rcon.sync(creds['host'], creds['port'])
return errors

View File

@ -8,25 +8,26 @@ from restic_compose_backup import (
commands,
containers
)
from restic_compose_backup import utils
logger = logging.getLogger(__name__)
def rcon_cli(host, port, cmd: str) -> int:
exit_code = commands.run([
"rcon-cli",
f"--host {host}",
f"--port {port}",
f"--host={host}",
f"--port={port}",
cmd
])
if exit_code != 0:
raise RconException("rcon-cli %s exited with a non-zero exit code: %s", cmd, exit_code)
raise RconException(f"rcon-cli {cmd} exited with a non-zero exit code: {exit_code}")
return exit_code
def is_online(host, port) -> int:
"""Check if rcon can be reached"""
return rcon_cli(host, port, "version")
return rcon_cli(host, port, "help")
def save_off(host, port) -> int:
"""Turn saving off"""