From b4f4d98e85be75a8f407f1efdb41c860f3065578 Mon Sep 17 00:00:00 2001 From: MacRimi Date: Tue, 6 Oct 2026 17:26:55 +0200 Subject: [PATCH] fix(backup): send host backup notifications over HTTPS when the Monitor uses it --- .../scripts/tests/test_backup_notify_https.py | 58 +++++++++++++++++++ .../backup_restore/apply_cluster_postboot.sh | 8 ++- .../backup_restore/lib_host_backup_common.sh | 7 ++- 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 .github/scripts/tests/test_backup_notify_https.py diff --git a/.github/scripts/tests/test_backup_notify_https.py b/.github/scripts/tests/test_backup_notify_https.py new file mode 100644 index 00000000..03022403 --- /dev/null +++ b/.github/scripts/tests/test_backup_notify_https.py @@ -0,0 +1,58 @@ +"""A host backup and a finished restore tell the Monitor on its own port, +over HTTPS once the Monitor has a certificate.""" +from pathlib import Path +import subprocess +import tempfile +from unittest import TestCase + +ROOT = Path(__file__).resolve().parents[3] +LIBRARY = ROOT / 'scripts/backup_restore/lib_host_backup_common.sh' +POSTBOOT = ROOT / 'scripts/backup_restore/apply_cluster_postboot.sh' +SETTING = '/etc/proxmenux/ssl_config.json' + + +def function(text, name): + start = text.index(name + '() {') + return text[start:text.index('\n}\n', start) + 3] + + +class BackupNotifyHttps(TestCase): + def address(self, setting): + with tempfile.TemporaryDirectory() as folder: + path = Path(folder) / 'ssl_config.json' + if setting is not None: + path.write_text(setting) + body = function(LIBRARY.read_text(), 'hb_notify_lifecycle').replace(SETTING, str(path)) + # The function silences curl, so the stub writes the address it was given. + called = Path(folder) / 'called' + script = (f'curl() {{ printf "%s" "${{@: -1}}" > {called}; }}\n' + body + + 'HB_NOTIFY_JOB_ID=job1 hb_notify_lifecycle complete\n') + result = subprocess.run(['/bin/bash', '--noprofile', '--norc', '-c', script], + capture_output=True, text=True, timeout=10) + self.assertEqual(result.returncode, 0, result.stderr) + return called.read_text() + + def test_plain_http_while_the_monitor_has_no_certificate(self): + expected = 'http://127.0.0.1:8008/api/notifications/webhook' + self.assertEqual(self.address(None), expected) + self.assertEqual(self.address('{"enabled": false}'), expected) + + def test_https_once_the_monitor_has_a_certificate(self): + self.assertEqual(self.address('{"enabled": true, "source": "proxmox"}'), + 'https://127.0.0.1:8008/api/notifications/webhook') + + def test_the_finished_restore_follows_the_same_setting(self): + source = POSTBOOT.read_text() + self.assertIn('"${NOTIFY_SCHEME}://127.0.0.1:8008/api/internal/restore-event"', source) + self.assertNotIn('"http://127.0.0.1:8008/api/internal/restore-event"', source) + check = source[source.index('NOTIFY_SCHEME="http"'):source.index('NOTIFY_HTTP=$(curl')] + with tempfile.TemporaryDirectory() as folder: + path = Path(folder) / 'ssl_config.json' + for setting, expected in (('{"enabled": true}', 'https'), ('{"enabled":false}', 'http'), (None, 'http')): + path.unlink(missing_ok=True) + if setting is not None: + path.write_text(setting) + result = subprocess.run(['/bin/bash', '--noprofile', '--norc', '-c', + check.replace(SETTING, str(path)) + '\necho "$NOTIFY_SCHEME"'], + capture_output=True, text=True, timeout=10) + self.assertEqual(result.stdout.strip(), expected, setting) diff --git a/scripts/backup_restore/apply_cluster_postboot.sh b/scripts/backup_restore/apply_cluster_postboot.sh index 261c810b..0c24708e 100755 --- a/scripts/backup_restore/apply_cluster_postboot.sh +++ b/scripts/backup_restore/apply_cluster_postboot.sh @@ -805,8 +805,12 @@ if command -v curl >/dev/null 2>&1; then "$POSTBOOT_DURATION_FMT" \ "$SANITY_WARNINGS") fi - NOTIFY_HTTP=$(curl -s -o /dev/null -w '%{http_code}' \ - -X POST "http://127.0.0.1:8008/api/internal/restore-event" \ + # The Monitor answers on the same port over HTTPS once it has a certificate. + NOTIFY_SCHEME="http" + grep -Eq '"enabled"[[:space:]]*:[[:space:]]*true' /etc/proxmenux/ssl_config.json 2>/dev/null && \ + NOTIFY_SCHEME="https" + NOTIFY_HTTP=$(curl -sk -o /dev/null -w '%{http_code}' \ + -X POST "${NOTIFY_SCHEME}://127.0.0.1:8008/api/internal/restore-event" \ -H "Content-Type: application/json" \ -d "$PAYLOAD" \ --max-time 5 2>/dev/null || echo "000") diff --git a/scripts/backup_restore/lib_host_backup_common.sh b/scripts/backup_restore/lib_host_backup_common.sh index a1177676..8eaadd1e 100755 --- a/scripts/backup_restore/lib_host_backup_common.sh +++ b/scripts/backup_restore/lib_host_backup_common.sh @@ -3420,11 +3420,16 @@ Log: ${HB_NOTIFY_LOG_FILE:-}" log_file:$log, reason:$reason}}' \ 2>/dev/null) [[ -z "$payload" ]] && return 0 + # The Monitor answers on the same port over HTTPS once it has a certificate. + local scheme="http" + [[ -f /etc/proxmenux/ssl_config.json ]] && \ + jq -e '.enabled' /etc/proxmenux/ssl_config.json >/dev/null 2>&1 && \ + scheme="https" curl -sk --connect-timeout 3 --max-time 5 \ -X POST \ -H "Content-Type: application/json" \ -d "$payload" \ - http://127.0.0.1:8008/api/notifications/webhook \ + "${scheme}://127.0.0.1:8008/api/notifications/webhook" \ >/dev/null 2>&1 || true }