-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement module list-backup-repositories
Return the list of backup destinations with a Restic repository for the current module.
- Loading branch information
1 parent
9c1b43b
commit 21db535
Showing
3 changed files
with
211 additions
and
1 deletion.
There are no files selected for viewing
103 changes: 103 additions & 0 deletions
103
core/imageroot/usr/local/agent/actions/list-backup-repositories/50list_backup_repositories
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,103 @@ | ||
#!/usr/bin/env python3 | ||
|
||
# | ||
# Copyright (C) 2024 Nethesis S.r.l. | ||
# SPDX-License-Identifier: GPL-3.0-or-later | ||
# | ||
|
||
import sys | ||
import json | ||
import agent | ||
import asyncio | ||
import os | ||
import time | ||
from datetime import datetime, timezone | ||
|
||
rdb = agent.redis_connect(privileged=False) | ||
module_id = os.environ['MODULE_ID'] | ||
module_uuid = os.environ['MODULE_UUID'] | ||
module_ui_name = rdb.get(f'module/{module_id}/ui_name') or "" | ||
image_name = agent.get_image_name_from_url(os.environ["IMAGE_URL"]) | ||
cluster_uuid = rdb.get("cluster/uuid") or "" | ||
odests = {} | ||
for krepo in rdb.scan_iter('cluster/backup_repository/*'): | ||
dest_uuid = krepo.removeprefix('cluster/backup_repository/') | ||
odests[dest_uuid] = rdb.hgetall(krepo) | ||
rdb.close() | ||
|
||
# | ||
# Fetch data from all backup destinations | ||
# | ||
|
||
async def read_destination_repo(dest_uuid, dest_path): | ||
proc = await asyncio.create_subprocess_exec('rclone-wrapper', dest_uuid, 'lsjson', f'REMOTE_PATH/{dest_path}/config', stdout=asyncio.subprocess.PIPE) | ||
# Return the first and only element of the expected JSON array | ||
out, _ = await proc.communicate() | ||
if out == b'[\n]\n' or not out: | ||
data = {} | ||
else: | ||
data = json.loads(out)[0] | ||
return data | ||
|
||
async def read_destination_meta(dest_uuid, dest_path): | ||
proc = await asyncio.create_subprocess_exec('rclone-wrapper', dest_uuid, 'cat', f'REMOTE_PATH/{dest_path}.json', stdout=asyncio.subprocess.PIPE) | ||
out, _ = await proc.communicate() | ||
if out: | ||
data = json.loads(out) | ||
else: | ||
data = {} | ||
return data | ||
|
||
|
||
async def get_destination_info(dest_uuid, odest): | ||
global cluster_uuid, module_id, module_uuid, module_ui_name, image_name | ||
|
||
dest_path = f"{image_name}/{module_uuid}" | ||
|
||
async with asyncio.TaskGroup() as tg: | ||
task_repo = tg.create_task(read_destination_repo(dest_uuid, dest_path)) | ||
task_meta = tg.create_task(read_destination_meta(dest_uuid, dest_path)) | ||
|
||
info = { | ||
"module_id": module_id, | ||
"module_ui_name": module_ui_name, | ||
"node_fqdn": "", | ||
"path": dest_path, | ||
"name": image_name, | ||
"uuid": module_uuid, | ||
"timestamp": 0, | ||
"repository_id" : dest_uuid, | ||
"repository_name": odest["name"], | ||
"repository_provider": odest["provider"], | ||
"repository_url": odest["url"], | ||
"installed_instance": module_id, | ||
"installed_instance_ui_name": module_ui_name, | ||
"is_generated_locally": False, | ||
} | ||
|
||
result_repo = task_repo.result() | ||
if not result_repo: | ||
return None | ||
|
||
try: | ||
# Obtain from lsjson the repository creation timestamp | ||
info['timestamp'] = int(time.mktime(datetime.fromisoformat(result_repo["ModTime"]).timetuple())) | ||
except: | ||
info['timestamp'] = int(time.time()) | ||
|
||
result_meta = task_meta.result() | ||
if "cluster_uuid" in result_meta and result_meta["cluster_uuid"] == cluster_uuid: | ||
info['is_generated_locally'] = True | ||
info.update(result_meta) # merge two dictionaries | ||
|
||
return info | ||
|
||
async def print_destinations(odests): | ||
tasks = [] | ||
async with asyncio.TaskGroup() as tg: | ||
for dest_uuid, odest in odests.items(): | ||
tasks.append(tg.create_task(get_destination_info(dest_uuid, odest))) | ||
destinations = list(filter(lambda r: r, [task.result() for task in tasks])) | ||
json.dump(destinations, fp=sys.stdout) | ||
|
||
asyncio.run(print_destinations(odests)) |
107 changes: 107 additions & 0 deletions
107
core/imageroot/usr/local/agent/actions/list-backup-repositories/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,107 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"title": "list-backup-repositories output", | ||
"$id": "http://schema.nethserver.org/module/list-backup-repositories-output.json", | ||
"description": "Return a list of the module's Restic backup repositories. The format is the same of cluster/read-backup-repositories.", | ||
"examples": [ | ||
[ | ||
{ | ||
"module_id": "loki1", | ||
"module_ui_name": "My Loki", | ||
"node_fqdn": "rl1.dp.nethserver.net", | ||
"path": "loki/35f45b73-f81e-467b-b622-96ec3b7fec19", | ||
"name": "loki", | ||
"uuid": "35f45b73-f81e-467b-b622-96ec3b7fec19", | ||
"timestamp": 1721405723, | ||
"repository_id": "14030a59-a4e6-54cc-b8ea-cd5f97fe44c8", | ||
"repository_name": "BackBlaze repo1", | ||
"repository_provider": "backblaze", | ||
"repository_url": "b2:ns8-davidep", | ||
"installed_instance": "loki1", | ||
"installed_instance_ui_name": "My Loki", | ||
"is_generated_locally": true | ||
} | ||
] | ||
], | ||
"type": "array", | ||
"items": { | ||
"type": "object", | ||
"properties": { | ||
"module_id": { | ||
"type": "string", | ||
"description": "Original module ID value." | ||
}, | ||
"module_ui_name": { | ||
"type": "string", | ||
"description": "Original module label, assigned by the user." | ||
}, | ||
"node_fqdn": { | ||
"type": "string", | ||
"description": "The FQDN of the node where the module of the backup is hosted." | ||
}, | ||
"path": { | ||
"type": "string", | ||
"description": "Path of the repository, relative to the backup destination." | ||
}, | ||
"name": { | ||
"type": "string", | ||
"description": "Name of the module. It is equal to the module image name." | ||
}, | ||
"uuid": { | ||
"type": "string", | ||
"description": "Universal, unique identifier of the module instance." | ||
}, | ||
"timestamp": { | ||
"type": "integer", | ||
"description": "Unix timestamp of the last backup run." | ||
}, | ||
"repository_id": { | ||
"type": "string", | ||
"description": "UUID of the backup destination." | ||
}, | ||
"repository_name": { | ||
"type": "string", | ||
"description": "Human readable name of the backup destination." | ||
}, | ||
"repository_provider": { | ||
"type": "string", | ||
"description": "Type of backup destination provider, e.g. SMB, S3..." | ||
}, | ||
"repository_url": { | ||
"type": "string", | ||
"description": "Restic URL of the backup destination." | ||
}, | ||
"installed_instance": { | ||
"type": "string", | ||
"description": "If the backup belongs to an installed module instance this is its module ID." | ||
}, | ||
"installed_instance_ui_name": { | ||
"type": "string", | ||
"description": "If the backup belongs to an installed module instance this is its module friendly name." | ||
}, | ||
"is_generated_locally": { | ||
"type": [ | ||
"boolean", | ||
"null" | ||
], | ||
"description": "Tells if the backup originates from the local cluster or from another cluster. The null value is returned if this information is missing completely, as it happens in old backups." | ||
} | ||
}, | ||
"required": [ | ||
"module_id", | ||
"module_ui_name", | ||
"node_fqdn", | ||
"path", | ||
"name", | ||
"uuid", | ||
"timestamp", | ||
"repository_id", | ||
"repository_name", | ||
"repository_provider", | ||
"repository_url", | ||
"installed_instance", | ||
"installed_instance_ui_name", | ||
"is_generated_locally" | ||
] | ||
} | ||
} |
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