diff --git a/plugins/modules/kopia_notification.py b/plugins/modules/kopia_notification.py new file mode 100644 index 0000000000..45c85c32c0 --- /dev/null +++ b/plugins/modules/kopia_notification.py @@ -0,0 +1,502 @@ +#!/usr/bin/python + +# Copyright (c) 2026, Dexter Le +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +DOCUMENTATION = r""" +module: kopia_notification +short_description: Manage Kopia notification profiles and templates +author: + - Dexter Le (@munchtoast) +version_added: "13.1.0" +description: + - Manage Kopia notification profiles and message templates using the Kopia CLI. + - Supports configuring email, Pushover, and webhook notification profiles, + as well as listing, showing, deleting, and testing profiles. + - Supports listing, showing, setting, and removing notification message templates. +extends_documentation_fragment: + - community.general._attributes + - community.general._kopia +attributes: + check_mode: + support: full + diff_mode: + support: none +options: + state: + description: + - Desired state of the notification resource. + - V(profile_email) creates or updates an email notification profile. + Requires O(profile_name), O(smtp_server), O(mail_to), and O(mail_from). + - V(profile_pushover) creates or updates a Pushover notification profile. + Requires O(profile_name), O(pushover_app_token), and O(pushover_user_key). + - V(profile_webhook) creates or updates a webhook notification profile. + Requires O(profile_name) and O(webhook_endpoint). + - V(profile_deleted) removes a notification profile. Requires O(profile_name). + - V(profile_tested) sends a test notification via the named profile. + Requires O(profile_name). + - V(profiles_listed) lists all configured notification profiles. + - V(profile_shown) displays a specific notification profile. Requires O(profile_name). + - V(template_set) sets a notification message template. + Requires O(template_name) and O(template_file). + - V(template_removed) removes a notification message template. Requires O(template_name). + - V(templates_listed) lists all notification message templates. + - V(template_shown) displays a specific notification message template. + Requires O(template_name). + type: str + choices: + - profile_email + - profile_pushover + - profile_webhook + - profile_deleted + - profile_tested + - profiles_listed + - profile_shown + - template_set + - template_removed + - templates_listed + - template_shown + default: profiles_listed + profile_name: + description: + - Name of the notification profile to create, update, delete, test, or show. + - Required if O(state=profile_email), O(state=profile_pushover), + O(state=profile_webhook), O(state=profile_deleted), O(state=profile_tested), + or O(state=profile_shown). + type: str + min_severity: + description: + - Minimum notification severity level that triggers this profile. + - Optional for O(state=profile_email), O(state=profile_pushover), + or O(state=profile_webhook). + type: str + choices: [verbose, info, warning, error] + format: + description: + - Message format for notifications. + - Optional for O(state=profile_email), O(state=profile_pushover), + or O(state=profile_webhook). + type: str + choices: [html, md, txt] + send_test: + description: + - When V(true), send a test notification immediately after configuring the profile. + - Optional for O(state=profile_email), O(state=profile_pushover), + or O(state=profile_webhook). + type: bool + default: false + smtp_server: + description: + - SMTP server hostname or IP address. + - Required if O(state=profile_email). + type: str + smtp_port: + description: + - SMTP server port. + - Optional if O(state=profile_email). + type: int + smtp_username: + description: + - SMTP authentication username. + - Optional if O(state=profile_email). + type: str + smtp_password: + description: + - SMTP authentication password. + - Optional if O(state=profile_email). + type: str + smtp_identity: + description: + - SMTP SASL identity string. + - Optional if O(state=profile_email). + type: str + mail_from: + description: + - Sender email address. + - Required if O(state=profile_email). + type: str + mail_to: + description: + - Recipient email address. + - Required if O(state=profile_email). + type: str + mail_cc: + description: + - CC email address. + - Optional if O(state=profile_email). + type: str + pushover_app_token: + description: + - Pushover application token. + - Required if O(state=profile_pushover). + type: str + pushover_user_key: + description: + - Pushover user key. + - Required if O(state=profile_pushover). + type: str + webhook_endpoint: + description: + - Webhook destination URL. + - Required if O(state=profile_webhook). + type: str + webhook_method: + description: + - HTTP method to use when calling the webhook. + - Optional if O(state=profile_webhook). + type: str + choices: [GET, POST, PUT, PATCH] + webhook_headers: + description: + - List of HTTP headers to include with webhook requests, each in C(key:value) format. + - Optional if O(state=profile_webhook). + type: list + elements: str + template_name: + description: + - Name of the notification template to set, remove, or show. + - Required if O(state=template_set), O(state=template_removed), + or O(state=template_shown). + type: str + template_file: + description: + - Path to a file containing the template body to set. + - Required if O(state=template_set). + type: path +""" + +EXAMPLES = r""" +- name: Configure an email notification profile + community.general.kopia_notification: + state: profile_email + profile_name: ops-email + smtp_server: smtp.example.com + smtp_port: 587 + smtp_username: notify@example.com + smtp_password: smtpsecret + mail_from: notify@example.com + mail_to: ops@example.com + min_severity: warning + config: /etc/kopia/root.config + +- name: Configure a Pushover notification profile + community.general.kopia_notification: + state: profile_pushover + profile_name: ops-pushover + pushover_app_token: "aToken123" + pushover_user_key: "uKey456" + min_severity: error + config: /etc/kopia/root.config + +- name: Configure a webhook notification profile + community.general.kopia_notification: + state: profile_webhook + profile_name: ops-webhook + webhook_endpoint: https://hooks.example.com/kopia + webhook_method: POST + webhook_headers: + - "Authorization:Bearer mytoken" + format: html + config: /etc/kopia/root.config + +- name: Send a test notification + community.general.kopia_notification: + state: profile_tested + profile_name: ops-email + config: /etc/kopia/root.config + +- name: Show a notification profile + community.general.kopia_notification: + state: profile_shown + profile_name: ops-email + config: /etc/kopia/root.config + +- name: List all notification profiles + community.general.kopia_notification: + state: profiles_listed + config: /etc/kopia/root.config + +- name: Delete a notification profile + community.general.kopia_notification: + state: profile_deleted + profile_name: ops-email + config: /etc/kopia/root.config + +- name: Set a notification template from a file + community.general.kopia_notification: + state: template_set + template_name: snapshot-complete + template_file: /etc/kopia/templates/snapshot-complete.html + config: /etc/kopia/root.config + +- name: List all notification templates + community.general.kopia_notification: + state: templates_listed + config: /etc/kopia/root.config + +- name: Show a notification template + community.general.kopia_notification: + state: template_shown + template_name: snapshot-complete + config: /etc/kopia/root.config + +- name: Remove a notification template + community.general.kopia_notification: + state: template_removed + template_name: snapshot-complete + config: /etc/kopia/root.config +""" + +RETURN = r""" +kopia_notification: + description: Output from the Kopia notification command. + type: str + sample: "" + returned: always +""" + +from ansible_collections.community.general.plugins.module_utils._cmd_runner import cmd_runner_fmt +from ansible_collections.community.general.plugins.module_utils._kopia import ( + KOPIA_COMMON_ARGUMENT_SPEC, + kopia_runner, +) +from ansible_collections.community.general.plugins.module_utils._module_helper import StateModuleHelper + +# Maps each module state to the kopia CLI words that follow `kopia notification`. +# profile configure email → ("profile", "configure", "email") +# profile delete → ("profile", "delete") +# template set → ("template", "set") +# etc. +_STATE_CLI_MAP = { + "profile_email": ("profile", "configure", "email"), + "profile_pushover": ("profile", "configure", "pushover"), + "profile_webhook": ("profile", "configure", "webhook"), + "profile_deleted": ("profile", "delete"), + "profile_tested": ("profile", "test"), + "profiles_listed": ("profile", "list"), + "profile_shown": ("profile", "show"), + "template_set": ("template", "set"), + "template_removed": ("template", "remove"), + "templates_listed": ("template", "list"), + "template_shown": ("template", "show"), +} + +# Read-only states that must not be skipped in check mode. +_READONLY_STATES = frozenset(["profiles_listed", "profile_shown", "templates_listed", "template_shown"]) + + +def _fmt_webhook_headers(value): + """Expand a list of key:value strings into repeated --http-header flags.""" + if not value: + return [] + result = [] + for header in value: + result.extend(["--http-header", header]) + return result + + +class KopiaNotification(StateModuleHelper): + module = dict( + supports_check_mode=True, + argument_spec=dict( + **KOPIA_COMMON_ARGUMENT_SPEC, + state=dict( + type="str", + default="profiles_listed", + choices=[ + "profile_email", + "profile_pushover", + "profile_webhook", + "profile_deleted", + "profile_tested", + "profiles_listed", + "profile_shown", + "template_set", + "template_removed", + "templates_listed", + "template_shown", + ], + ), + profile_name=dict(type="str"), + min_severity=dict(type="str", choices=["verbose", "info", "warning", "error"]), + format=dict(type="str", choices=["html", "md", "txt"]), + send_test=dict(type="bool", default=False), + # email + smtp_server=dict(type="str"), + smtp_port=dict(type="int"), + smtp_username=dict(type="str"), + smtp_password=dict(type="str", no_log=True), + smtp_identity=dict(type="str"), + mail_from=dict(type="str"), + mail_to=dict(type="str"), + mail_cc=dict(type="str"), + # pushover + pushover_app_token=dict(type="str", no_log=True), + pushover_user_key=dict(type="str", no_log=True), + # webhook + webhook_endpoint=dict(type="str"), + webhook_method=dict(type="str", choices=["GET", "POST", "PUT", "PATCH"]), + webhook_headers=dict(type="list", elements="str"), + # template + template_name=dict(type="str"), + template_file=dict(type="path"), + ), + required_if=[ + ("state", "profile_email", ["profile_name", "smtp_server", "mail_from", "mail_to"]), + ("state", "profile_pushover", ["profile_name", "pushover_app_token", "pushover_user_key"]), + ("state", "profile_webhook", ["profile_name", "webhook_endpoint"]), + ("state", "profile_deleted", ["profile_name"]), + ("state", "profile_tested", ["profile_name"]), + ("state", "profile_shown", ["profile_name"]), + ("state", "template_set", ["template_name", "template_file"]), + ("state", "template_removed", ["template_name"]), + ("state", "template_shown", ["template_name"]), + ], + ) + + def __init_module__(self): + self.runner = kopia_runner( + self.module, + extra_formats=dict( + list_profiles=cmd_runner_fmt.as_fixed("notification", "profile", "list"), + notif_group=cmd_runner_fmt.as_list(), + notif_subcommand=cmd_runner_fmt.as_list(), + notif_provider=cmd_runner_fmt.as_list(), + profile_name=cmd_runner_fmt.as_opt_val("--profile-name"), + min_severity=cmd_runner_fmt.as_opt_val("--min-severity"), + format=cmd_runner_fmt.as_opt_val("--format"), + send_test=cmd_runner_fmt.as_bool("--send-test-notification"), + smtp_server=cmd_runner_fmt.as_opt_val("--smtp-server"), + smtp_port=cmd_runner_fmt.as_opt_val("--smtp-port"), + smtp_username=cmd_runner_fmt.as_opt_val("--smtp-username"), + smtp_password=cmd_runner_fmt.as_opt_val("--smtp-password"), + smtp_identity=cmd_runner_fmt.as_opt_val("--smtp-identity"), + mail_from=cmd_runner_fmt.as_opt_val("--mail-from"), + mail_to=cmd_runner_fmt.as_opt_val("--mail-to"), + mail_cc=cmd_runner_fmt.as_opt_val("--mail-cc"), + pushover_app_token=cmd_runner_fmt.as_opt_val("--app-token"), + pushover_user_key=cmd_runner_fmt.as_opt_val("--user-key"), + webhook_endpoint=cmd_runner_fmt.as_opt_val("--endpoint"), + webhook_method=cmd_runner_fmt.as_opt_val("--method"), + webhook_headers=cmd_runner_fmt.as_func(_fmt_webhook_headers), + template_name=cmd_runner_fmt.as_list(), + template_file=cmd_runner_fmt.as_list(), + ), + ) + self.vars.set("previous_value", self._get()["out"]) + self.vars.set("value", self.vars.previous_value, change=True, diff=True) + + def __quit_module__(self): + self.vars.set("value", self._get()["out"]) + + def _get(self): + with self.runner("list_profiles config") as ctx: + result = ctx.run() + return dict( + rc=result[0], + out=(result[1].rstrip() if result[1] else None), + err=result[2], + ) + + def _process_command_output(self, fail_on_err, ignore_err_msg=""): + def process(rc, out, err): + if fail_on_err and rc != 0 and err and ignore_err_msg not in err: + self.do_raise(f"kopia failed with error (rc={rc}): {err}") + out = out.rstrip() if out else "" + return None if out == "" else out + + return process + + def _run_notif_cmd(self, args_order, ignore_err_msg="", **run_kwargs): + cli_words = _STATE_CLI_MAP[self.vars.state] + notif_group = cli_words[0] + notif_subcommand = cli_words[1] + notif_provider = cli_words[2] if len(cli_words) == 3 else None + check_mode_skip = self.vars.state not in _READONLY_STATES + with self.runner( + args_order, + output_process=self._process_command_output(True, ignore_err_msg), + check_mode_skip=check_mode_skip, + ) as ctx: + ctx.run( + cli_action="notification", + notif_group=notif_group, + notif_subcommand=notif_subcommand, + notif_provider=notif_provider, + **run_kwargs, + ) + + def state_profile_email(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand notif_provider" + " profile_name smtp_server smtp_port smtp_username smtp_password" + " smtp_identity mail_from mail_to mail_cc" + " min_severity format send_test config", + ) + + def state_profile_pushover(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand notif_provider" + " profile_name pushover_app_token pushover_user_key" + " min_severity format send_test config", + ) + + def state_profile_webhook(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand notif_provider" + " profile_name webhook_endpoint webhook_method webhook_headers" + " min_severity format send_test config", + ) + + def state_profile_deleted(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand profile_name config", + ignore_err_msg="no such profile", + ) + + def state_profile_tested(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand profile_name config", + ) + + def state_profiles_listed(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand config", + ) + + def state_profile_shown(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand profile_name config", + ) + + def state_template_set(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand template_name template_file config", + ) + + def state_template_removed(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand template_name config", + ignore_err_msg="no such template", + ) + + def state_templates_listed(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand config", + ) + + def state_template_shown(self): + self._run_notif_cmd( + "cli_action notif_group notif_subcommand template_name config", + ) + + +def main(): + KopiaNotification.execute() + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/test_kopia_notification.py b/tests/unit/plugins/modules/test_kopia_notification.py new file mode 100644 index 0000000000..8edb31ecb2 --- /dev/null +++ b/tests/unit/plugins/modules/test_kopia_notification.py @@ -0,0 +1,11 @@ +# Copyright (c) 2026, Dexter Le +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import annotations + +from ansible_collections.community.general.plugins.modules import kopia_notification + +from .uthelper import RunCommandMock, UTHelper + +UTHelper.from_module(kopia_notification, __name__, mocks=[RunCommandMock]) diff --git a/tests/unit/plugins/modules/test_kopia_notification.yaml b/tests/unit/plugins/modules/test_kopia_notification.yaml new file mode 100644 index 0000000000..6a30ad7096 --- /dev/null +++ b/tests/unit/plugins/modules/test_kopia_notification.yaml @@ -0,0 +1,455 @@ +# Copyright (c) 2026, Dexter Le +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +--- +anchors: + environ: &env-def {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + profiles_list_ok: &profiles-list-ok + command: [/testbin/kopia, notification, profile, list, --config-file=/etc/kopia/root.config] + environ: *env-def + rc: 0 + out: "ops-email" + err: '' + profiles_list_empty: &profiles-list-empty + command: [/testbin/kopia, notification, profile, list, --config-file=/etc/kopia/root.config] + environ: *env-def + rc: 0 + out: '' + err: '' + +test_cases: + - id: configure_email_profile + input: + state: profile_email + profile_name: ops-email + smtp_server: smtp.example.com + smtp_port: 587 + smtp_username: notify@example.com + smtp_password: smtpsecret + mail_from: notify@example.com + mail_to: ops@example.com + min_severity: warning + config: /etc/kopia/root.config + output: + changed: true + mocks: + run_command: + - *profiles-list-empty + - command: + - /testbin/kopia + - notification + - profile + - configure + - email + - --profile-name + - ops-email + - --smtp-server + - smtp.example.com + - --smtp-port + - "587" + - --smtp-username + - notify@example.com + - --smtp-password + - smtpsecret + - --mail-from + - notify@example.com + - --mail-to + - ops@example.com + - --min-severity + - warning + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: '' + err: '' + - *profiles-list-ok + + - id: configure_pushover_profile + input: + state: profile_pushover + profile_name: ops-pushover + pushover_app_token: "aToken123" + pushover_user_key: "uKey456" + min_severity: error + config: /etc/kopia/root.config + output: + changed: true + mocks: + run_command: + - *profiles-list-empty + - command: + - /testbin/kopia + - notification + - profile + - configure + - pushover + - --profile-name + - ops-pushover + - --app-token + - aToken123 + - --user-key + - uKey456 + - --min-severity + - error + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: '' + err: '' + - *profiles-list-ok + + - id: configure_webhook_profile + input: + state: profile_webhook + profile_name: ops-webhook + webhook_endpoint: https://hooks.example.com/kopia + webhook_method: POST + webhook_headers: + - "Authorization:Bearer mytoken" + format: html + config: /etc/kopia/root.config + output: + changed: true + mocks: + run_command: + - *profiles-list-empty + - command: + - /testbin/kopia + - notification + - profile + - configure + - webhook + - --profile-name + - ops-webhook + - --endpoint + - https://hooks.example.com/kopia + - --method + - POST + - --http-header + - "Authorization:Bearer mytoken" + - --format + - html + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: '' + err: '' + - *profiles-list-ok + + - id: delete_profile + input: + state: profile_deleted + profile_name: ops-email + config: /etc/kopia/root.config + output: + changed: true + mocks: + run_command: + - *profiles-list-ok + - command: + - /testbin/kopia + - notification + - profile + - delete + - --profile-name + - ops-email + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: '' + err: '' + - *profiles-list-empty + + - id: delete_profile_no_such_profile + input: + state: profile_deleted + profile_name: ops-email + config: /etc/kopia/root.config + output: + changed: false + mocks: + run_command: + - *profiles-list-empty + - command: + - /testbin/kopia + - notification + - profile + - delete + - --profile-name + - ops-email + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 1 + out: '' + err: 'no such profile' + - *profiles-list-empty + + - id: list_profiles + input: + state: profiles_listed + config: /etc/kopia/root.config + output: + changed: false + mocks: + run_command: + - *profiles-list-ok + - command: + - /testbin/kopia + - notification + - profile + - list + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: "ops-email" + err: '' + - *profiles-list-ok + + - id: show_profile + input: + state: profile_shown + profile_name: ops-email + config: /etc/kopia/root.config + output: + changed: false + mocks: + run_command: + - *profiles-list-ok + - command: + - /testbin/kopia + - notification + - profile + - show + - --profile-name + - ops-email + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: "Profile: ops-email" + err: '' + - *profiles-list-ok + + - id: set_template + input: + state: template_set + template_name: snapshot-complete + template_file: /etc/kopia/templates/snapshot-complete.html + config: /etc/kopia/root.config + output: + changed: false + mocks: + run_command: + - *profiles-list-ok + - command: + - /testbin/kopia + - notification + - template + - set + - snapshot-complete + - /etc/kopia/templates/snapshot-complete.html + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: '' + err: '' + - *profiles-list-ok + + - id: list_templates + input: + state: templates_listed + config: /etc/kopia/root.config + output: + changed: false + mocks: + run_command: + - *profiles-list-ok + - command: + - /testbin/kopia + - notification + - template + - list + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: "snapshot-complete" + err: '' + - *profiles-list-ok + + - id: remove_template + input: + state: template_removed + template_name: snapshot-complete + config: /etc/kopia/root.config + output: + changed: false + mocks: + run_command: + - *profiles-list-ok + - command: + - /testbin/kopia + - notification + - template + - remove + - snapshot-complete + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: '' + err: '' + - *profiles-list-ok + + - id: test_profile + input: + state: profile_tested + profile_name: ops-email + config: /etc/kopia/root.config + output: + changed: false + mocks: + run_command: + - *profiles-list-ok + - command: + - /testbin/kopia + - notification + - profile + - test + - --profile-name + - ops-email + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: '' + err: '' + - *profiles-list-ok + + - id: show_template + input: + state: template_shown + template_name: snapshot-complete + config: /etc/kopia/root.config + output: + changed: false + mocks: + run_command: + - *profiles-list-ok + - command: + - /testbin/kopia + - notification + - template + - show + - snapshot-complete + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: "Subject: Snapshot complete" + err: '' + - *profiles-list-ok + + - id: remove_template_no_such_template + input: + state: template_removed + template_name: snapshot-complete + config: /etc/kopia/root.config + output: + changed: false + mocks: + run_command: + - *profiles-list-ok + - command: + - /testbin/kopia + - notification + - template + - remove + - snapshot-complete + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 1 + out: '' + err: 'no such template' + - *profiles-list-ok + + - id: configure_email_with_cc_and_identity + input: + state: profile_email + profile_name: ops-email + smtp_server: smtp.example.com + smtp_port: 587 + smtp_username: notify@example.com + smtp_password: smtpsecret + smtp_identity: "notify" + mail_from: notify@example.com + mail_to: ops@example.com + mail_cc: manager@example.com + config: /etc/kopia/root.config + output: + changed: true + mocks: + run_command: + - *profiles-list-empty + - command: + - /testbin/kopia + - notification + - profile + - configure + - email + - --profile-name + - ops-email + - --smtp-server + - smtp.example.com + - --smtp-port + - "587" + - --smtp-username + - notify@example.com + - --smtp-password + - smtpsecret + - --smtp-identity + - notify + - --mail-from + - notify@example.com + - --mail-to + - ops@example.com + - --mail-cc + - manager@example.com + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: '' + err: '' + - *profiles-list-ok + + - id: configure_webhook_with_send_test + input: + state: profile_webhook + profile_name: ops-webhook + webhook_endpoint: https://hooks.example.com/kopia + webhook_method: POST + format: md + send_test: true + config: /etc/kopia/root.config + output: + changed: true + mocks: + run_command: + - *profiles-list-empty + - command: + - /testbin/kopia + - notification + - profile + - configure + - webhook + - --profile-name + - ops-webhook + - --endpoint + - https://hooks.example.com/kopia + - --method + - POST + - --format + - md + - --send-test-notification + - --config-file=/etc/kopia/root.config + environ: *env-def + rc: 0 + out: '' + err: '' + - *profiles-list-ok