feat: add option to backup minecraft containers via rcon-cli

This commit is contained in:
Michael Reichenbach 2020-11-25 15:07:35 +01:00 committed by Silthus
parent 47d1687bc1
commit 02ae4ca6d8
5 changed files with 178 additions and 16 deletions

View File

@ -142,6 +142,8 @@ def backup(config, containers):
# Map volumes from other containers we are backing up
mounts = containers.generate_backup_mounts('/volumes')
volumes.update(mounts)
mounts = containers.generate_minecraft_mounts('/minecraft')
volumes.update(mounts)
logger.debug('Starting backup container with image %s', containers.this_container.image)
try:
@ -208,25 +210,41 @@ def start_backup_process(config, containers):
logger.warning("Found no volumes to back up")
has_volumes = False
try:
has_minecraft_volumes = os.stat('/minecraft') is not None
except FileNotFoundError:
logger.warning("Found no minecraft servers to back up")
has_minecraft_volumes = False
# Warn if there is nothing to do
if len(containers.containers_for_backup()) == 0 and not has_volumes:
backup_containers = containers.containers_for_backup()
if len(backup_containers) == 0 and not has_volumes:
logger.error("No containers for backup found")
exit(1)
if has_volumes:
logger.info('Backing up volumes')
for volume in [f for f in os.scandir('/volumes') if f.is_dir()]:
logger.info('Backing up volumes of %s', volume.name)
for path in [f.path for f in os.scandir(volume.path) if f.is_dir()]:
try:
logger.info('Backing up volumes')
vol_result = restic.backup_files(config.repository, source='/volumes')
logger.debug('Volume backup exit code: %s', vol_result)
if vol_result != 0:
logger.error('Volume backup exited with non-zero code: %s', vol_result)
errors = True
except Exception as ex:
logger.error('Exception raised during volume backup')
logger.exception(ex)
errors = True
if has_minecraft_volumes:
logger.info('Backing up minecraft servers')
for container in containers.containers_for_backup():
if container.minecraft_backup_enabled:
try:
logger.info('Backing up %s', path)
vol_result = restic.backup_files(config.repository, source=path)
logger.debug('Volume backup exit code: %s', vol_result)
if vol_result != 0:
logger.error('Volume backup exited with non-zero code: %s', vol_result)
result = backup_container_instance(container)
if result != 0:
logger.error('Backup command exited with non-zero code: %s', result)
errors = True
except Exception as ex:
logger.error('Exception raised during volume backup')
logger.exception(ex)
errors = True
@ -235,10 +253,7 @@ def start_backup_process(config, containers):
for container in containers.containers_for_backup():
if container.database_backup_enabled:
try:
instance = container.instance
logger.info('Backing up %s in service %s', instance.container_type, instance.service_name)
result = instance.backup()
logger.debug('Exit code: %s', result)
result = backup_container_instance(container)
if result != 0:
logger.error('Backup command exited with non-zero code: %s', result)
errors = True
@ -267,6 +282,12 @@ def start_backup_process(config, containers):
logger.info('Backup completed')
def backup_container_instance(container: Container) -> int:
instance = container.instance
logger.info('Backing up %s in service %s', instance.container_type, instance.service_name)
result = instance.backup()
logger.debug('Exit code: %s', result)
return result
def cleanup(config, containers):
"""Run forget / prune to minimize storage space"""

View File

@ -3,7 +3,7 @@ import logging
from pathlib import Path
from typing import List
from restic_compose_backup import enums, utils
from restic_compose_backup import enums, utils, rcon
from restic_compose_backup.config import config
logger = logging.getLogger(__name__)
@ -46,6 +46,9 @@ class Container:
return containers_db.MysqlContainer(self._data)
if self.postgresql_backup_enabled:
return containers_db.PostgresContainer(self._data)
elif self.minecraft_backup_enabled:
from restic_compose_backup import containers_minecraft
return containers_minecraft.MinecraftContainer(self._data)
else:
return self
@ -148,6 +151,11 @@ class Container:
"""bool: If the ``restic-compose-backup.volumes`` label is set"""
return utils.is_true(self.get_label(enums.LABEL_VOLUMES_ENABLED))
@property
def minecraft_backup_enabled(self) -> bool:
"""bool: If the ``restic-compose-backup.minecraft``` label is set"""
return utils.is_true(self.get_label(enums.LABEL_MINECRAFT_ENABLED))
@property
def database_backup_enabled(self) -> bool:
"""bool: Is database backup enabled in any shape or form?"""
@ -428,6 +436,15 @@ class RunningContainers:
return mounts
def generate_minecraft_mounts(self, dest_prefix='/minecraft') -> dict:
"""Generate minecraft mounts for backup for the entire compose setup"""
mounts = {}
for container in self.containers_for_backup():
if container.minecraft_backup_enabled:
mounts.update(container.volumes_for_backup(source_prefix=dest_prefix, mode='ro'))
return mounts
def get_service(self, name) -> Container:
"""Container: Get a service by name"""
for container in self.containers:

View File

@ -0,0 +1,69 @@
from pathlib import Path
from restic_compose_backup.containers import Container
from restic_compose_backup.config import config, Config
from restic_compose_backup import (
commands,
restic,
rcon
)
from restic_compose_backup import utils
class MinecraftContainer(Container):
container_type = 'minecraft'
def get_credentials(self) -> dict:
"""dict: get credentials for the service"""
return {
'host': self.hostname,
'password': self.get_config_env('RCON_PASSWORD'),
'port': self.get_config_env('RCON_PORT'),
}
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']
)
def backup(self) -> bool:
config = Config()
creds = self.get_credentials()
errors = False
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
except Exception as ex:
logger.error('Exception raised during minecraft backup')
logger.exception(ex)
errors = True
# 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'])

View File

@ -9,3 +9,5 @@ LABEL_POSTGRES_ENABLED = 'restic-compose-backup.postgres'
LABEL_MARIADB_ENABLED = 'restic-compose-backup.mariadb'
LABEL_BACKUP_PROCESS = 'restic-compose-backup.process'
LABEL_MINECRAFT_ENABLED = 'restic-compose-backup.minecraft'

View File

@ -0,0 +1,53 @@
"""
rcon-cli commands
"""
import logging
from typing import List, Tuple
from subprocess import Popen, PIPE
from restic_compose_backup import (
commands,
containers
)
logger = logging.getLogger(__name__)
def rcon_cli(host, port, cmd: str) -> int:
exit_code = commands.run([
"rcon-cli",
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)
return exit_code
def is_online(host, port) -> int:
"""Check if rcon can be reached"""
return rcon_cli(host, port, "version")
def save_off(host, port) -> int:
"""Turn saving off"""
return rcon_cli(host, port, "save-off")
def save_on(host, port) -> int:
"""Turn saving on"""
return rcon_cli(host, port, "save-on")
def save_all(host, port) -> int:
"""Save all worlds"""
return rcon_cli(host, port, "save-all")
def sync(host, port) -> int:
"""sync data"""
return rcon_cli(host, port, "sync")
class RconException(Exception):
"""Raised when an error occured while using the rcon-cli"""
def __init__(self, message):
self.message = message