diff --git a/AppImage/scripts/flask_notification_routes.py b/AppImage/scripts/flask_notification_routes.py index 8fa54695..d30fe2e7 100644 --- a/AppImage/scripts/flask_notification_routes.py +++ b/AppImage/scripts/flask_notification_routes.py @@ -231,12 +231,15 @@ def _pve_remove_our_blocks(text, headers_to_remove): def _build_webhook_fallback(): """Build fallback manual commands for webhook setup.""" - body_tpl = '{"title":"{{ escape title }}","message":"{{ escape message }}","severity":"{{ severity }}","timestamp":"{{ timestamp }}","type":"{{#if fields.type}}{{ fields.type }}{{else}}test{{/if}}","hostname":"{{#if fields.hostname}}{{ fields.hostname }}{{else}}unknown{{/if}}"}' + import base64 + body_tpl = '{"title":"{{ title }}","message":"{{ message }}","severity":"{{ severity }}","timestamp":"{{ timestamp }}"}' + body_b64 = base64.b64encode(body_tpl.encode()).decode() return [ "# 1. Append to END of /etc/pve/notifications.cfg", "# (do NOT delete existing content):", "", f"webhook: {_PVE_ENDPOINT_ID}", + f"\tbody {body_b64}", f"\tmethod post", f"\turl {_PVE_WEBHOOK_URL}", "", @@ -246,9 +249,6 @@ def _build_webhook_fallback(): "", "# 2. Append to /etc/pve/priv/notifications.cfg :", f"webhook: {_PVE_ENDPOINT_ID}", - "", - "# 3. Set body via pvesh (NOT in the config file -- PVE stores it base64):", - f"pvesh set /cluster/notifications/endpoints/webhook/{_PVE_ENDPOINT_ID} --body '{body_tpl}'", ] @@ -310,12 +310,16 @@ def setup_pve_webhook_core() -> dict: # PVE secret format is: secret name=key,value= # Neither is needed for localhost calls. - # Write ONLY basic properties (method, url) to the config file. - # body and header MUST be set via pvesh API (Step 10) because PVE - # stores them base64-encoded internally. Writing them as plain text - # to the config file corrupts PVE's config parser. + # PVE stores body as base64 in the config file. We encode it here + # so the config parser reads it correctly. The plain-text template: + # {"title":"{{ title }}","message":"{{ message }}","severity":"{{ severity }}","timestamp":"{{ timestamp }}"} + import base64 + body_template = '{"title":"{{ title }}","message":"{{ message }}","severity":"{{ severity }}","timestamp":"{{ timestamp }}"}' + body_b64 = base64.b64encode(body_template.encode()).decode() + endpoint_block = ( f"webhook: {_PVE_ENDPOINT_ID}\n" + f"\tbody {body_b64}\n" f"\tmethod post\n" f"\turl {_PVE_WEBHOOK_URL}\n" ) @@ -380,46 +384,6 @@ def setup_pve_webhook_core() -> dict: except Exception: pass - # ── Step 10: Configure body and header via pvesh API ── - # body and header are stored base64-encoded in the config file. - # Writing them as plain text corrupts PVE's parser. pvesh handles - # the encoding correctly. - import subprocess - - pvesh_path = f'/cluster/notifications/endpoints/webhook/{_PVE_ENDPOINT_ID}' - - # Body template using PVE Handlebars syntax. - # - {{ escape title }} and {{ escape message }} are PVE built-in helpers - # - {{ severity }} gives: info, notice, warning, error, unknown - # - {{ fields.X }} is populated for real events (backup, replication, etc.) - # but NOT for test notifications, so we use {{#if}} fallbacks. - # - {{ timestamp }} gives epoch seconds - body_template = ( - '{' - '"title":"{{ escape title }}",' - '"message":"{{ escape message }}",' - '"severity":"{{ severity }}",' - '"timestamp":"{{ timestamp }}",' - '"type":"{{#if fields.type}}{{ fields.type }}{{else}}test{{/if}}",' - '"hostname":"{{#if fields.hostname}}{{ fields.hostname }}{{else}}unknown{{/if}}"' - '}' - ) - - try: - # Set body template via pvesh (NOT in the config file -- PVE - # stores body base64-encoded internally, pvesh handles that). - # No header needed: our webhook handler parses JSON from raw - # body regardless of Content-Type. - r1 = subprocess.run( - ['pvesh', 'set', pvesh_path, '--body', body_template], - capture_output=True, text=True, timeout=10 - ) - if r1.returncode != 0: - result['body_config_warning'] = f'pvesh set --body failed: {r1.stderr.strip()}' - except Exception as e: - # Non-fatal: if pvesh fails, webhook still receives POSTs (just empty body) - result['body_config_warning'] = str(e) - result['configured'] = True result['secret'] = secret return result