-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #58 from NethServer/feat-7072
Selective restore of Samba File Server contents Refs NethServer/dev#7072
- Loading branch information
Showing
12 changed files
with
426 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
71 changes: 71 additions & 0 deletions
71
imageroot/actions/restore-backup-content/50restore_backup_content
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
#!/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}" | ||
] | ||
|
||
# Prepare progress callback function that captures non-progress messages too: | ||
last_restic_message = {} | ||
def build_restore_progress_callback(): | ||
restore_progress = agent.get_progress_callback(2, 97) | ||
def fprog(omessage): | ||
global last_restic_message | ||
last_restic_message = omessage | ||
if omessage['message_type'] == 'status': | ||
fpercent = float(omessage['percent_done']) | ||
restore_progress(int(fpercent * 100)) | ||
return fprog | ||
# Run the restic restore command capturing the progress status: | ||
prestore = agent.run_restic(agent.redis_connect(), request["destination"], request["repopath"], podman_args, restic_args, progress_callback=build_restore_progress_callback()) | ||
if prestore.returncode != 0: | ||
print(agent.SD_ERR + f"Restic restore command failed with exit code {prestore.returncode}.", file=sys.stderr) | ||
sys.exit(1) | ||
|
||
json.dump({ | ||
"request": request, | ||
"last_restic_message": last_restic_message, | ||
}, fp=sys.stdout) |
51 changes: 51 additions & 0 deletions
51
imageroot/actions/restore-backup-content/validate-input.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
imageroot/actions/restore-backup-content/validate-output.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 restore" | ||
} | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
imageroot/actions/seek-snapshot-contents/50seek_snapshot_contents
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
#!/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) | ||
|
||
limit_reached = False | ||
contents_limit = request.get("limit", 10) | ||
cache_seed = "%s-%s" % (request['destination'], request['snapshot']) | ||
plocate_cache = hashlib.md5(cache_seed.encode(), usedforsecurity=False).hexdigest() | ||
|
||
podman_exec = ["podman", "exec", "samba-dc"] | ||
pcheck = agent.run_helper(*(podman_exec + ["net", "conf", "showshare", request["share"]]), stdout=subprocess.DEVNULL) | ||
if pcheck.returncode != 0: | ||
agent.set_status('validation-failed') | ||
json.dump([{'field':'share', 'parameter':'share','value': request['share'], 'error':'share_not_found'}], fp=sys.stdout) | ||
sys.exit(2) | ||
|
||
def locate_share_content(): | ||
global limit_reached, contents, contents_limit | ||
# Search share paths matching the query: | ||
plocate_cmd = ['podman', 'exec', 'samba-dc', 'locate-share-content', '-d', f"/var/lib/samba/plocate/{plocate_cache}", '-s', request['share'], "-q", request.get('query', "")] | ||
with subprocess.Popen(plocate_cmd, stdout=subprocess.PIPE, stderr=sys.stderr, text=True, errors='replace') as vproc: | ||
contents = [] | ||
while True: | ||
line = vproc.stdout.readline() | ||
if not line: | ||
break | ||
content = line.rstrip() | ||
if not content in contents: | ||
contents.append(content) | ||
if len(contents) >= contents_limit: | ||
limit_reached = True | ||
break | ||
return vproc.wait(timeout=1) | ||
|
||
def purge_plocate_cache(): | ||
# Remove cache dirs older than 8 hours | ||
purge_script='echo Removing old plocate databases ; find /var/lib/samba/plocate/ -mindepth 1 -maxdepth 1 -type d -cmin +480 -print0 | xargs -r -0 -- rm -rvf' | ||
agent.run_helper('podman', 'exec', 'samba-dc', 'bash', '-c', purge_script) | ||
|
||
if locate_share_content() == 3: | ||
print(agent.SD_INFO + "DB is not cached, fetch it from the backup snapshot", file=sys.stderr) | ||
purge_plocate_cache() | ||
# Cache is missing. Extract the .plocate files from the snapshot and | ||
# store them under a temporary cache directory: | ||
podman_args = ["--workdir=/srv"] + agent.agent.get_state_volume_args() | ||
restic_args = [ | ||
"restore", | ||
f"{request['snapshot']}:volumes/data/backup", | ||
"--include=*.plocate", | ||
f"--target=/srv/volumes/data/plocate/{plocate_cache}" | ||
] | ||
agent.run_restic(agent.redis_connect(), request["destination"], request["repopath"], podman_args, restic_args, stdout=sys.stderr).check_returncode() | ||
# Repeat the search | ||
locate2_returncode = locate_share_content() | ||
if not limit_reached and locate2_returncode != 0: | ||
print(agent.SD_ERR + f"locate-share-content failed with exit code {locate2_returncode}.", file=sys.stderr) | ||
sys.exit(1) | ||
|
||
json.dump({ | ||
"request": request, | ||
"contents": contents, | ||
"limit_reached": limit_reached, | ||
}, fp=sys.stdout) |
56 changes: 56 additions & 0 deletions
56
imageroot/actions/seek-snapshot-contents/validate-input.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"title": "seek-snapshot-contents input", | ||
"$id": "http://schema.nethserver.org/mail/seek-snapshot-contents-input.json", | ||
"description": "Locate a file or directory in a backup snapshot", | ||
"examples": [ | ||
{ | ||
"snapshot": "b9ae143be9a5cf86fccff4bd7907296a3feb9f904457e3d521f215c5445cdac7", | ||
"destination": "86d1a8ac-ef89-557a-8e19-8582ab86b7c4", | ||
"repopath": "samba/8efb6625-e70f-4a5f-9cb5-2836096d5054", | ||
"share": "Complex Shar€ name" | ||
}, | ||
{ | ||
"snapshot": "b9ae143be9a5cf86fccff4bd7907296a3feb9f904457e3d521f215c5445cdac7", | ||
"destination": "86d1a8ac-ef89-557a-8e19-8582ab86b7c4", | ||
"repopath": "samba/8efb6625-e70f-4a5f-9cb5-2836096d5054", | ||
"share": "Complex Shar€ name", | ||
"limit": 50, | ||
"query": "MYFILE.TXT" | ||
} | ||
], | ||
"type": "object", | ||
"required": [ | ||
"destination", | ||
"repopath", | ||
"snapshot", | ||
"share" | ||
], | ||
"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" | ||
}, | ||
"query": { | ||
"type": "string", | ||
"description": "Seek matching paths" | ||
}, | ||
"limit": { | ||
"type": "integer", | ||
"description": "Limit the number of returned contents" | ||
} | ||
} | ||
} |
46 changes: 46 additions & 0 deletions
46
imageroot/actions/seek-snapshot-contents/validate-output.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"title": "seek-snapshot-content output", | ||
"$id": "http://schema.nethserver.org/mail/seek-snapshot-content-output.json", | ||
"description": "Locate a file or directory in a backup snapshot", | ||
"examples": [ | ||
{ | ||
"request": { | ||
"destination": "14030a59-a4e6-54cc-b8ea-cd5f97fe44c8", | ||
"repopath": "mail/4372a5d5-0886-45d3-82e7-68d913716a4c", | ||
"snapshot": "latest", | ||
"share": "myshare", | ||
"query": "*.php", | ||
"limit": 100 | ||
}, | ||
"contents": [ | ||
"dir1/file001.php", | ||
"dir1/file002.php", | ||
"Project/NethServer/Main.php" | ||
], | ||
"limit_reached": false | ||
} | ||
], | ||
"type": "object", | ||
"required": [ | ||
"contents", | ||
"limit_reached" | ||
], | ||
"properties": { | ||
"contents": { | ||
"type": "array", | ||
"description": "List of absolute share content paths", | ||
"items": { | ||
"type": "string" | ||
} | ||
}, | ||
"limit_reached": { | ||
"type": "boolean", | ||
"description": "If true, the query matches more contents than the returned items" | ||
}, | ||
"request": { | ||
"type": "object", | ||
"title": "Original request object" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.