feat: add minecraft backup method
Minecraft server can now be backed up using the restic-compose-backup.minecraft flag
This commit is contained in:
parent
02ae4ca6d8
commit
9eb050173f
|
@ -20,6 +20,7 @@ venv
|
|||
restic_data/
|
||||
restic_cache/
|
||||
alerts.env
|
||||
minecraft/
|
||||
|
||||
# build
|
||||
build/
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"]
|
||||
|
|
158
src/backup.sh
158
src/backup.sh
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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"""
|
||||
|
|
Loading…
Reference in New Issue