From 586867b6a2f23235cf6f7ead5d7247fedc3ec88a Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Wed, 13 Nov 2024 18:13:56 +0100 Subject: [PATCH] Implement restore-backup-content - Create and set-up permissions of "Restored content" folder, if missing. - Remove previously restored contents. - Restore the backup, capturing Restic's progress percentage. --- .../50restore_backup_content | 68 +++++++++++++++++++ .../validate-input.json | 51 ++++++++++++++ .../validate-output.json | 36 ++++++++++ 3 files changed, 155 insertions(+) create mode 100755 imageroot/actions/restore-backup-content/50restore_backup_content create mode 100644 imageroot/actions/restore-backup-content/validate-input.json create mode 100644 imageroot/actions/restore-backup-content/validate-output.json diff --git a/imageroot/actions/restore-backup-content/50restore_backup_content b/imageroot/actions/restore-backup-content/50restore_backup_content new file mode 100755 index 00000000..8dcd9a2e --- /dev/null +++ b/imageroot/actions/restore-backup-content/50restore_backup_content @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2024 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import json +import sys +import os +import subprocess +import agent +import hashlib + +request = json.load(sys.stdin) + +content_basename = os.path.basename(request['content']) +content_path = os.path.dirname(request['content']) +destroot = request.get("destroot", "Restored folder") + +pre_script = """ +set -e +share="${1:?share param missing}" +destroot="${2:?destroot param missing}" +content_basename="${3:?content_basename param is missing}" +# SDDL ACL. World read-only permissions. SYstem and Builtin Admins (BA) have full access: +destroot_acl='O:LAG:DUD:AI(A;OICIID;0x001f01ff;;;SY)(A;OICIID;0x001f01ff;;;BA)(A;OICIID;0x001301bf;;;WD)' +cd "${SAMBA_SHARES_DIR:?}/${share}" +if [ ! -e "${destroot}" ]; then + mkdir -vp "${destroot}" + chmod -c u+rwx,g+srwx,a+rx "${destroot}" + chown -c root:users "${destroot}" + samba-tool ntacl set "${destroot_acl}" "${destroot}" +fi +# Drop any existing content +rm -rf "${destroot}/${content_basename}" +""" +pre_cmd = ['podman', 'exec', '-i', 'samba-dc', 'bash', '-s', request['share'], destroot, content_basename] +subprocess.run(pre_cmd, input=pre_script, stdout=sys.stderr, text=True).check_returncode() + +podman_args = ["--workdir=/srv"] + agent.agent.get_state_volume_args() +restic_args = [ + "restore", + "--json", + f"{request['snapshot']}:volumes/shares/{request['share']}/{content_path}", + f"--include={content_basename}", + f"--include={content_basename}/**", + f"--target=volumes/shares/{request['share']}/{destroot}" +] +set_restore_progress_value = agent.get_progress_callback(2, 97) +restic_cmd, restic_env = agent.prepare_restic_command(agent.redis_connect(), request["destination"], request["repopath"], podman_args, restic_args) +with subprocess.Popen(restic_cmd, env=restic_env, stdout=subprocess.PIPE, text=True, errors='replace') as prestore: + while True: + line = prestore.stdout.readline() + if not line: + break + try: + omessage = json.loads(line) + if omessage['message_type'] == 'status': + fpercent = float(omessage['percent_done']) + set_restore_progress_value(int(fpercent * 100)) + except Exception as ex: + print(agent.SD_WARNING+"Error decoding restic json progress", ex, file=sys.stderr) + +json.dump({ + "request": request, + "last_restic_message": omessage, +}, fp=sys.stdout) diff --git a/imageroot/actions/restore-backup-content/validate-input.json b/imageroot/actions/restore-backup-content/validate-input.json new file mode 100644 index 00000000..714a50a6 --- /dev/null +++ b/imageroot/actions/restore-backup-content/validate-input.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "restore-backup-content input", + "$id": "http://schema.nethserver.org/mail/restore-backup-content-input.json", + "description": "Extract content from a backup snapshot", + "examples": [ + { + "snapshot": "b9ae143be9a5cf86fccff4bd7907296a3feb9f904457e3d521f215c5445cdac7", + "destination": "86d1a8ac-ef89-557a-8e19-8582ab86b7c4", + "repopath": "samba/8efb6625-e70f-4a5f-9cb5-2836096d5054", + "share": "pub", + "content": "Clienti/StudioV" + } + ], + "type": "object", + "required": [ + "destination", + "repopath", + "snapshot", + "share", + "content" + ], + "properties": { + "destination": { + "type": "string", + "description": "The UUID of the backup destination where the Restic repository resides." + }, + "repopath": { + "type": "string", + "description": "Restic repository path, under the backup destination" + }, + "snapshot": { + "type": "string", + "description": "Restic snapshot ID to restore" + }, + "share": { + "type": "string", + "pattern": "^[^/\\\\:><\"|?*]+$", + "description": "Seek the paths of this Samba share" + }, + "destroot": { + "type": "string", + "pattern": "^[^/\\\\:><\"|?*]+$", + "description": "Name of a share root-level directory where content is restored. Existing content is removed before restoring the backup." + }, + "content": { + "type": "string", + "description": "Content path to restore" + } + } +} diff --git a/imageroot/actions/restore-backup-content/validate-output.json b/imageroot/actions/restore-backup-content/validate-output.json new file mode 100644 index 00000000..fb65ee39 --- /dev/null +++ b/imageroot/actions/restore-backup-content/validate-output.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "restore-backup-content output", + "$id": "http://schema.nethserver.org/mail/restore-backup-content-output.json", + "description": "Extract content from a backup snapshot", + "examples": [ + { + "request": { + "content": "Clienti/StudioV", + "destination": "86d1a8ac-ef89-557a-8e19-8582ab86b7c4", + "repopath": "samba/8efb6625-e70f-4a5f-9cb5-2836096d5054", + "share": "pub", + "snapshot": "b9ae143be9a5cf86fccff4bd7907296a3feb9f904457e3d521f215c5445cdac7" + }, + "last_restic_message": { + "message_type": "summary", + "seconds_elapsed": 14, + "total_files": 2165, + "files_restored": 2165, + "total_bytes": 717166163, + "bytes_restored": 717166163 + } + } + ], + "type": "object", + "properties": { + "request": { + "type": "object", + "description": "Original request object" + }, + "last_restic_message": { + "type": "object", + "description": "Last JSON message from Restic" + } + } +}