feat: add option to backup minecraft containers via rcon-cli
This commit is contained in:
parent
47d1687bc1
commit
02ae4ca6d8
|
@ -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"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'])
|
|
@ -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'
|
|
@ -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
|
Loading…
Reference in New Issue