add app versions into restic backup tags
This commit is contained in:
parent
333b7ec16d
commit
c9b04db7a0
70
backupbot.py
70
backupbot.py
@ -98,11 +98,11 @@ def export_secrets():
|
|||||||
@click.option('retries', '--retries', '-r', envvar='RETRIES', default=1)
|
@click.option('retries', '--retries', '-r', envvar='RETRIES', default=1)
|
||||||
def create(retries):
|
def create(retries):
|
||||||
app_settings = parse_backup_labels()
|
app_settings = parse_backup_labels()
|
||||||
pre_commands, post_commands, backup_paths, apps = get_backup_details(app_settings)
|
pre_commands, post_commands, backup_paths, apps_versions = get_backup_details(app_settings)
|
||||||
copy_secrets(apps)
|
copy_secrets(apps_versions)
|
||||||
backup_paths.append(Path(SECRET_PATH))
|
backup_paths.append(Path(SECRET_PATH))
|
||||||
run_commands(pre_commands)
|
run_commands(pre_commands)
|
||||||
backup_volumes(backup_paths, apps, int(retries))
|
backup_volumes(backup_paths, apps_versions, int(retries))
|
||||||
run_commands(post_commands)
|
run_commands(post_commands)
|
||||||
|
|
||||||
|
|
||||||
@ -117,15 +117,19 @@ def restore(snapshot, target, noninteractive, volumes, container, no_commands):
|
|||||||
app_settings = parse_backup_labels('restore', container)
|
app_settings = parse_backup_labels('restore', container)
|
||||||
if SERVICE != 'ALL':
|
if SERVICE != 'ALL':
|
||||||
app_settings = {SERVICE: app_settings[SERVICE]}
|
app_settings = {SERVICE: app_settings[SERVICE]}
|
||||||
pre_commands, post_commands, backup_paths, apps = get_backup_details(app_settings, volumes)
|
pre_commands, post_commands, backup_paths, apps_versions = get_backup_details(app_settings, volumes)
|
||||||
snapshots = get_snapshots(snapshot_id=snapshot)
|
snapshots = get_snapshots(snapshot_id=snapshot)
|
||||||
if not snapshot:
|
if not snapshot:
|
||||||
logger.error("No Snapshots with ID {snapshots} for {apps} found.")
|
logger.error(f"No Snapshots with ID {snapshots} for {apps_versions.keys()} found.")
|
||||||
exit(1)
|
exit(1)
|
||||||
if not noninteractive:
|
if not noninteractive:
|
||||||
snapshot_date = datetime.fromisoformat(snapshots[0]['time'])
|
print(f"Snapshot to restore: \t{snapshot}")
|
||||||
delta = datetime.now(tz=timezone.utc) - snapshot_date
|
restore_app_versions = app_versions_from_tags(snapshots[0].get('tags'))
|
||||||
print(f"You are going to restore Snapshot {snapshot} of {apps} at {target}")
|
print("Apps:")
|
||||||
|
for app, version in restore_app_versions.items():
|
||||||
|
print(f"\t{app} \t {version}")
|
||||||
|
if apps_versions.get(app) != version:
|
||||||
|
print(f"WARNING!!! The running app is deployed with version {apps_versions.get(app)}")
|
||||||
print("The following volume paths will be restored:")
|
print("The following volume paths will be restored:")
|
||||||
for p in backup_paths:
|
for p in backup_paths:
|
||||||
print(f'\t{p}')
|
print(f'\t{p}')
|
||||||
@ -133,6 +137,8 @@ def restore(snapshot, target, noninteractive, volumes, container, no_commands):
|
|||||||
print("The following commands will be executed:")
|
print("The following commands will be executed:")
|
||||||
for container, cmd in list(pre_commands.items()) + list(post_commands.items()):
|
for container, cmd in list(pre_commands.items()) + list(post_commands.items()):
|
||||||
print(f"\t{container.labels['com.docker.swarm.service.name']}:\t{cmd}")
|
print(f"\t{container.labels['com.docker.swarm.service.name']}:\t{cmd}")
|
||||||
|
snapshot_date = datetime.fromisoformat(snapshots[0]['time'])
|
||||||
|
delta = datetime.now(tz=timezone.utc) - snapshot_date
|
||||||
print(f"This snapshot is {delta} old")
|
print(f"This snapshot is {delta} old")
|
||||||
print("\nTHIS COMMAND WILL IRREVERSIBLY OVERWRITES FILES")
|
print("\nTHIS COMMAND WILL IRREVERSIBLY OVERWRITES FILES")
|
||||||
prompt = input("Type YES (uppercase) to continue: ")
|
prompt = input("Type YES (uppercase) to continue: ")
|
||||||
@ -162,18 +168,25 @@ def restic_restore(snapshot_id='latest', include=[], target_dir=None):
|
|||||||
def get_snapshots(snapshot_id=None):
|
def get_snapshots(snapshot_id=None):
|
||||||
if snapshot_id and snapshot_id != 'latest':
|
if snapshot_id and snapshot_id != 'latest':
|
||||||
snapshots = restic.snapshots(snapshot_id=snapshot_id)
|
snapshots = restic.snapshots(snapshot_id=snapshot_id)
|
||||||
if SERVICE not in snapshots[0]['tags']:
|
if not SERVICE in app_versions_from_tags(snapshots[0].get('tags')):
|
||||||
logger.error(f'Snapshot with ID {snapshot_id} does not contain {SERVICE}')
|
logger.error(f'Snapshot with ID {snapshot_id} does not contain {SERVICE}')
|
||||||
exit(1)
|
exit(1)
|
||||||
else:
|
else:
|
||||||
snapshots = restic.snapshots()
|
snapshots = restic.snapshots()
|
||||||
snapshots = list(filter(lambda x: x.get('tags') and SERVICE in x.get('tags'), snapshots))
|
snapshots = list(filter(lambda x: SERVICE in app_versions_from_tags(x.get('tags')), snapshots))
|
||||||
if snapshot_id == 'latest':
|
if snapshot_id == 'latest':
|
||||||
return snapshots[-1:]
|
return snapshots[-1:]
|
||||||
else:
|
else:
|
||||||
return snapshots
|
return snapshots
|
||||||
|
|
||||||
|
|
||||||
|
def app_versions_from_tags(tags):
|
||||||
|
if tags:
|
||||||
|
app_versions = map(lambda x: x.split(':'), tags)
|
||||||
|
return {i[0]: i[1] if len(i) > 1 else None for i in app_versions}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
def parse_backup_labels(hook_type='backup', selected_container=[]):
|
def parse_backup_labels(hook_type='backup', selected_container=[]):
|
||||||
client = docker.from_env()
|
client = docker.from_env()
|
||||||
container_by_service = {
|
container_by_service = {
|
||||||
@ -185,9 +198,12 @@ def parse_backup_labels(hook_type='backup', selected_container=[]):
|
|||||||
labels = specs['Labels']
|
labels = specs['Labels']
|
||||||
stack_name = labels['com.docker.stack.namespace']
|
stack_name = labels['com.docker.stack.namespace']
|
||||||
container_name = s.name.removeprefix(f"{stack_name}_")
|
container_name = s.name.removeprefix(f"{stack_name}_")
|
||||||
|
version = labels.get(f'coop-cloud.{stack_name}.version')
|
||||||
settings = app_settings[stack_name] = app_settings.get(stack_name) or {}
|
settings = app_settings[stack_name] = app_settings.get(stack_name) or {}
|
||||||
if (backup := labels.get('backupbot.backup')) and bool(backup):
|
if (backup := labels.get('backupbot.backup')) and bool(backup):
|
||||||
settings['enabled'] = True
|
settings['enabled'] = True
|
||||||
|
if version:
|
||||||
|
settings['version'] = version
|
||||||
if selected_container and container_name not in selected_container:
|
if selected_container and container_name not in selected_container:
|
||||||
logger.debug(f"Skipping {s.name} because it's not a selected container")
|
logger.debug(f"Skipping {s.name} because it's not a selected container")
|
||||||
continue
|
continue
|
||||||
@ -216,20 +232,20 @@ def parse_backup_labels(hook_type='backup', selected_container=[]):
|
|||||||
|
|
||||||
def get_backup_details(app_settings, volumes=[]):
|
def get_backup_details(app_settings, volumes=[]):
|
||||||
backup_paths = set()
|
backup_paths = set()
|
||||||
backup_apps = []
|
backup_apps_versions = {}
|
||||||
pre_hooks= {}
|
pre_hooks= {}
|
||||||
post_hooks = {}
|
post_hooks = {}
|
||||||
for app, settings in app_settings.items():
|
for app, settings in app_settings.items():
|
||||||
if settings.get('enabled'):
|
if settings.get('enabled'):
|
||||||
if SERVICE != 'ALL' and SERVICE != app:
|
if SERVICE != 'ALL' and SERVICE != app:
|
||||||
continue
|
continue
|
||||||
backup_apps.append(app)
|
backup_apps_versions[app] = settings.get('version')
|
||||||
add_backup_paths(backup_paths, settings, app, volumes)
|
add_backup_paths(backup_paths, settings, app, volumes)
|
||||||
if hooks:= settings.get('pre_hooks'):
|
if hooks:= settings.get('pre_hooks'):
|
||||||
pre_hooks.update(hooks)
|
pre_hooks.update(hooks)
|
||||||
if hooks:= settings.get('post_hooks'):
|
if hooks:= settings.get('post_hooks'):
|
||||||
post_hooks.update(hooks)
|
post_hooks.update(hooks)
|
||||||
return pre_hooks, post_hooks, list(backup_paths), backup_apps
|
return pre_hooks, post_hooks, list(backup_paths), backup_apps_versions
|
||||||
|
|
||||||
|
|
||||||
def add_backup_paths(backup_paths, settings, app, selected_volumes):
|
def add_backup_paths(backup_paths, settings, app, selected_volumes):
|
||||||
@ -339,7 +355,7 @@ def run_commands(commands):
|
|||||||
logger.info(result.output.decode())
|
logger.info(result.output.decode())
|
||||||
|
|
||||||
|
|
||||||
def backup_volumes(backup_paths, apps, retries, dry_run=False):
|
def backup_volumes(backup_paths, apps_versions, retries, dry_run=False):
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
logger.info("Backup these paths:")
|
logger.info("Backup these paths:")
|
||||||
@ -350,7 +366,9 @@ def backup_volumes(backup_paths, apps, retries, dry_run=False):
|
|||||||
if parent:
|
if parent:
|
||||||
# https://restic.readthedocs.io/en/stable/040_backup.html#file-change-detection
|
# https://restic.readthedocs.io/en/stable/040_backup.html#file-change-detection
|
||||||
cmd.extend(['--parent', parent[0]['short_id']])
|
cmd.extend(['--parent', parent[0]['short_id']])
|
||||||
tags = set(apps + [SERVICE])
|
tags = [f"{app}:{version}" for app,version in apps_versions.items()]
|
||||||
|
if SERVICE == 'ALL':
|
||||||
|
tags.append(SERVICE)
|
||||||
logger.info("Start volume backup")
|
logger.info("Start volume backup")
|
||||||
result = restic.internal.backup.run(cmd, backup_paths, dry_run=dry_run, tags=tags)
|
result = restic.internal.backup.run(cmd, backup_paths, dry_run=dry_run, tags=tags)
|
||||||
logger.summary("backup finished", extra=result)
|
logger.summary("backup finished", extra=result)
|
||||||
@ -374,7 +392,12 @@ def path_exists(path):
|
|||||||
def snapshots():
|
def snapshots():
|
||||||
snapshots = get_snapshots()
|
snapshots = get_snapshots()
|
||||||
for snap in snapshots:
|
for snap in snapshots:
|
||||||
print(snap['time'], snap['id'])
|
output = [snap['time'], snap['id']]
|
||||||
|
if tags:= snap.get('tags'):
|
||||||
|
app_versions = app_versions_from_tags(tags)
|
||||||
|
if version:= app_versions.get(SERVICE):
|
||||||
|
output.append(version)
|
||||||
|
print(*output)
|
||||||
if not snapshots:
|
if not snapshots:
|
||||||
err_msg = "No Snapshots found"
|
err_msg = "No Snapshots found"
|
||||||
if SERVICE != 'ALL':
|
if SERVICE != 'ALL':
|
||||||
@ -395,7 +418,12 @@ def ls(snapshot, path):
|
|||||||
|
|
||||||
def list_files(snapshot, path):
|
def list_files(snapshot, path):
|
||||||
cmd = restic.cat.base_command() + ['ls']
|
cmd = restic.cat.base_command() + ['ls']
|
||||||
cmd = cmd + ['--tag', SERVICE]
|
if snapshot == 'latest':
|
||||||
|
latest_snapshot = get_snapshots('latest')
|
||||||
|
if not latest_snapshot:
|
||||||
|
logger.error(f"There is no latest snapshot for {SERVICE}")
|
||||||
|
exit(1)
|
||||||
|
snapshot = latest_snapshot[0]['short_id']
|
||||||
cmd.append(snapshot)
|
cmd.append(snapshot)
|
||||||
if path:
|
if path:
|
||||||
cmd.append(path)
|
cmd.append(path)
|
||||||
@ -486,7 +514,13 @@ def get_formatted_size(file_path):
|
|||||||
|
|
||||||
def dump(snapshot, path):
|
def dump(snapshot, path):
|
||||||
cmd = restic.cat.base_command() + ['dump']
|
cmd = restic.cat.base_command() + ['dump']
|
||||||
cmd = cmd + ['--tag', SERVICE]
|
if snapshot == 'latest':
|
||||||
|
latest_snapshot = get_snapshots('latest')
|
||||||
|
if not latest_snapshot:
|
||||||
|
logger.error(f"There is no latest snapshot for {SERVICE}")
|
||||||
|
exit(1)
|
||||||
|
snapshot = latest_snapshot[0]['short_id']
|
||||||
|
cmd.append(snapshot)
|
||||||
cmd = cmd + [snapshot, path]
|
cmd = cmd + [snapshot, path]
|
||||||
print(f"Dumping {path} from snapshot '{snapshot}'")
|
print(f"Dumping {path} from snapshot '{snapshot}'")
|
||||||
output = subprocess.run(cmd, capture_output=True)
|
output = subprocess.run(cmd, capture_output=True)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user