From 3f9e7302c6888fdd295e6500bff1312965bdb1e0 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Fri, 13 Dec 2024 14:33:27 +0100 Subject: [PATCH] feat: implement determine-restore-eligibility This new cluster action inspects the contents of the backup snapshot of a certain application and returns its IMAGE_URL and the list of nodes eligible for restore. --- .../50determine_restore_eligibility | 39 +++++++++ .../validate-input.json | 37 ++++++++ .../validate-output.json | 84 +++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100755 core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/50determine_restore_eligibility create mode 100644 core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/validate-input.json create mode 100644 core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/validate-output.json diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/50determine_restore_eligibility b/core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/50determine_restore_eligibility new file mode 100755 index 000000000..b634f18ae --- /dev/null +++ b/core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/50determine_restore_eligibility @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2024 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import agent +import sys +import os +import json +import tempfile +import cluster.modules + +request = json.load(sys.stdin) +repository = request['repository'] +path = request['path'] +snapshot = request.get("snapshot", "latest") +# Write privileges are required for Redis repository metadata caching: +rdb = agent.redis_connect(privileged=True) +# Write the output to a named temporary file, to parse it with the +# existing read_envfile() function +with tempfile.NamedTemporaryFile() as fenv: + agent.run_restic(rdb, repository, path, ["--workdir=/srv"], ["dump", snapshot, "state/environment"], text=True, encoding='utf-8', stdout=fenv, check=True) + fenv.seek(0) + original_environment = agent.read_envfile(fenv.name) +module_source, _ = original_environment['IMAGE_URL'].rsplit(':', 1) +# Reduce the list to one element, matching the original source: +available = list(filter(lambda omod: omod["source"] == module_source, cluster.modules.list_available(rdb, skip_core_modules = False))) +if not available: + agent.set_status('validation-failed') + json.dump([{'field':'none', 'parameter':'none','value': '', 'error':'module_not_available'}], fp=sys.stdout) + sys.exit(2) +cluster.modules.decorate_with_installed(rdb, available) +cluster.modules.decorate_with_install_destinations(rdb, available) +json.dump({ + "image_url": original_environment['IMAGE_URL'], + "install_destinations": available[0]["install_destinations"], +}, fp=sys.stdout) diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/validate-input.json b/core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/validate-input.json new file mode 100644 index 000000000..703668fdc --- /dev/null +++ b/core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/validate-input.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "determine-restore-eligibility input", + "$id": "http://schema.nethserver.org/cluster/determine-restore-eligibility-input.json", + "description": "Input schema of the determine-restore-eligibility action", + "examples": [ + { + "repository": "48ce000a-79b7-5fe6-8558-177fd70c27b4", + "path": "dokuwiki/dokuwiki1@f5d24fcd-819c-4b1d-98ad-a1b2ebcee8cf", + "snapshot": "a6b8317eef" + } + ], + "type": "object", + "required": [ + "repository", + "path", + "snapshot" + ], + "properties": { + "repository": { + "title": "Destination ID", + "description": "Backup destination identifier", + "type": "string", + "minLength": 1 + }, + "path": { + "title": "Backup repository path", + "description": "Path of the Restic backup repository in the destination", + "type": "string", + "minLength": 1 + }, + "snapshot": { + "title": "Restic snapshot ID", + "type": "string" + } + } +} diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/validate-output.json b/core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/validate-output.json new file mode 100644 index 000000000..68b4cfb43 --- /dev/null +++ b/core/imageroot/var/lib/nethserver/cluster/actions/determine-restore-eligibility/validate-output.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "determine-restore-eligibility output", + "$id": "http://schema.nethserver.org/cluster/determine-restore-eligibility-output.json", + "description": "Output schema of the determine-restore-eligibility action", + "examples": [ + { + "image_url": "ghcr.io/nethserver/ejabberd:1.0.1", + "install_destinations": [ + { + "node_id": 1, + "instances": 0, + "eligible": true, + "reject_reason": null + }, + { + "node_id": 2, + "instances": 1, + "eligible": false, + "reject_reason": { + "message": "max_per_node_limit", + "parameter": "1" + } + } + ] + } + ], + "type": "object", + "required": [ + "image_url", + "install_destinations" + ], + "properties": { + "image_url": { + "type":"string" + }, + "install_destinations": { + "description": "Describe for each node of the cluster if the node is eligible or not to install a new module instance. If not, a reject reason is returned.", + "type": "array", + "items": { + "type": "object", + "required": [ + "node_id", + "instances", + "eligible", + "reject_reason" + ], + "properties": { + "node_id": { + "type": "integer", + "description": "Node identifier" + }, + "instances": { + "type": "integer", + "description": "Number of module instances currently installed on the node" + }, + "eligible": { + "type": "boolean", + "description": "True if another instance of the module can be installed on the node" + }, + "reject_reason": { + "type": [ + "object", + "null" + ], + "descripton": "If it is an object, it tells why the node is not eligible to host a module instance", + "properties": { + "message": { + "type": "string" + }, + "parameter": { + "type": "string" + } + }, + "required": [ + "message", + "parameter" + ] + } + } + } + } + } +} \ No newline at end of file