Skip to content

Commit

Permalink
Implement module list-backup-repositories
Browse files Browse the repository at this point in the history
Return the list of backup destinations with a Restic repository for the
current module.
  • Loading branch information
DavidePrincipi committed Nov 7, 2024
1 parent 9c1b43b commit 21db535
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 1 deletion.
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))
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"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "list-backup-repositories output",
"$id": "http://schema.nethserver.org/cluster/list-backup-repositories-output.json",
"description": "Get the list of available backup repositories and the status of cluster backup password",
"description": "Get the list of available backup destinations and the status of cluster backup password",
"examples": [
{
"repositories": [
Expand Down

0 comments on commit 21db535

Please sign in to comment.