1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-06-12 02:55:29 +00:00
This commit is contained in:
munchtoast 2026-06-08 16:25:38 -04:00 committed by GitHub
commit 45a99322a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 3374 additions and 55 deletions

8
.github/BOTMETA.yml vendored
View file

@ -905,10 +905,18 @@ files:
maintainers: ahussey-redhat
$modules/kibana_plugin.py:
maintainers: barryib
$modules/kopia_notification.py:
maintainers: munchtoast
$modules/kopia_policy.py:
maintainers: munchtoast
$modules/kopia_repository_info.py:
maintainers: munchtoast
$modules/kopia_repository.py:
maintainers: munchtoast
$modules/kopia_server.py:
maintainers: munchtoast
$modules/kopia_snapshot.py:
maintainers: munchtoast
$modules/krb_ticket.py:
maintainers: abakanovskii
$modules/launchd.py:

View file

@ -14,73 +14,102 @@ from ansible_collections.community.general.plugins.module_utils._cmd_runner impo
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
# Maps kopia_repository module state values to kopia CLI subcommands.
# Maps state values across all kopia modules to their kopia CLI subcommands.
# Used with cmd_runner_fmt.as_map() for the 'state' arg format.
REPOSITORY_STATE_MAP = {
STATE_MAP = {
# kopia_repository
"created": "create",
"connected": "connect",
"disconnected": "disconnect",
"synced": "sync-to",
"throttled": "throttle",
# kopia_snapshot
"deleted": "delete",
"expired": "expire",
"listed": "list",
"verified": "verify",
# kopia_policy
"set": "set",
"shown": "show",
}
# Maps backend provider names to their CLI flag names.
# Each provider emits its name as a positional sub-subcommand, followed by
# --flag value pairs for each non-None param.
# Maps backend provider names to their CLI parameter definitions.
# Each provider maps param_name -> (flag, type) where type is:
# "str" - emit --flag=value (skip if None)
# "bool" - emit --flag only when value is True (skip if False or None)
# "list" - emit --flag item once per item in the list (skip if empty or None)
# The "server" provider is intentionally absent: `kopia repository connect server`
# uses top-level flags (--url, --server-cert-fingerprint) rather than backend flags,
# so fmt_backend() returns [] for it and those flags are passed separately.
_PROVIDER_BACKEND_MAP = {
_PROVIDER_BACKEND_MAP: dict[str, dict[str, tuple[str, str]]] = {
"azure": {
"container": "--container",
"storage_account": "--storage-account",
"storage_key": "--storage-key",
"sas_token": "--sas-token",
"storage_domain": "--storage-domain",
"prefix": "--prefix",
"container": ("--container", "str"),
"storage_account": ("--storage-account", "str"),
"storage_key": ("--storage-key", "str"),
"sas_token": ("--sas-token", "str"),
"storage_domain": ("--storage-domain", "str"),
"prefix": ("--prefix", "str"),
"client_id": ("--client-id", "str"),
"client_secret": ("--client-secret", "str"),
"tenant_id": ("--tenant-id", "str"),
"azure_federated_token_file": ("--azure-federated-token-file", "str"),
},
"b2": {
"bucket": "--bucket",
"access_key": "--key-id",
"secret_access_key": "--key",
"prefix": "--prefix",
"bucket": ("--bucket", "str"),
"access_key": ("--key-id", "str"),
"secret_access_key": ("--key", "str"),
"prefix": ("--prefix", "str"),
},
"filesystem": {
"path": "--path",
"path": ("--path", "str"),
},
"gcs": {
"bucket": "--bucket",
"credentials_file": "--credentials-file",
"prefix": "--prefix",
"bucket": ("--bucket", "str"),
"credentials_file": ("--credentials-file", "str"),
"prefix": ("--prefix", "str"),
"embed_credentials": ("--embed-credentials", "bool"),
"read_only": ("--read-only", "bool"),
},
"gdrive": {
"folder_id": "--folder-id",
"credentials_file": "--credentials-file",
"folder_id": ("--folder-id", "str"),
"credentials_file": ("--credentials-file", "str"),
"read_only": ("--read-only", "bool"),
},
"rclone": {
"path": "--remote-path",
"path": ("--remote-path", "str"),
"rclone_exe": ("--rclone-exe", "str"),
"rclone_args": ("--rclone-args", "list"),
"rclone_env": ("--rclone-env", "list"),
"embed_rclone_config": ("--embed-rclone-config", "bool"),
},
"s3": {
"bucket": "--bucket",
"access_key": "--access-key",
"secret_access_key": "--secret-access-key",
"endpoint": "--endpoint",
"region": "--region",
"prefix": "--prefix",
"session_token": "--session-token",
"bucket": ("--bucket", "str"),
"access_key": ("--access-key", "str"),
"secret_access_key": ("--secret-access-key", "str"),
"endpoint": ("--endpoint", "str"),
"region": ("--region", "str"),
"prefix": ("--prefix", "str"),
"session_token": ("--session-token", "str"),
},
"sftp": {
"path": "--path",
"host": "--host",
"username": "--username",
"port": "--port",
"keyfile": "--keyfile",
"known_hosts": "--known-hosts",
"path": ("--path", "str"),
"host": ("--host", "str"),
"username": ("--username", "str"),
"port": ("--port", "str"),
"keyfile": ("--keyfile", "str"),
"known_hosts": ("--known-hosts", "str"),
"sftp_password": ("--sftp-password", "str"),
"key_data": ("--key-data", "str"),
"known_hosts_data": ("--known-hosts-data", "str"),
"embed_credentials": ("--embed-credentials", "bool"),
"external": ("--external", "bool"),
"ssh_command": ("--ssh-command", "str"),
"ssh_args": ("--ssh-args", "list"),
},
"webdav": {
"url": "--url",
"webdav_username": "--webdav-username",
"webdav_password": "--webdav-password",
"url": ("--url", "str"),
"webdav_username": ("--webdav-username", "str"),
"webdav_password": ("--webdav-password", "str"),
},
}
@ -96,7 +125,12 @@ def fmt_backend(value):
"""Format the backend dict into positional + flag arguments for kopia CLI.
For most providers the output is:
[provider_name, --flag1, value1, --flag2, value2, ...]
[provider_name, --flag1=value1, --flag2, ...]
Param types:
str - emits --flag=value; skipped when value is None.
bool - emits --flag when True; skipped when False or None.
list - emits --flag item once per item; skipped when empty or None.
For the "server" provider, returns [] because server connect uses top-level
flags (--url, --server-cert-fingerprint) passed separately.
@ -105,10 +139,18 @@ def fmt_backend(value):
if provider == "server":
return []
result = [provider]
for param_name, flag in _PROVIDER_BACKEND_MAP[provider].items():
for param_name, (flag, kind) in _PROVIDER_BACKEND_MAP[provider].items():
param_value = value.get(param_name)
if param_value is not None:
result.append(f"{flag}={param_value}")
if kind == "str":
if param_value is not None:
result.append(f"{flag}={param_value}")
elif kind == "bool":
if param_value:
result.append(flag)
elif kind == "list":
if param_value:
for item in param_value:
result.extend([flag, item])
return result
@ -143,7 +185,7 @@ def kopia_runner(module: AnsibleModule, extra_formats: dict | None = None, **kwa
cli_action=cmd_runner_fmt.as_list(),
status=cmd_runner_fmt.as_fixed("repository", "status"),
get_throttle=cmd_runner_fmt.as_fixed("repository", "throttle", "get"),
state=cmd_runner_fmt.as_map(REPOSITORY_STATE_MAP),
state=cmd_runner_fmt.as_map(STATE_MAP),
backend=cmd_runner_fmt.as_func(fmt_backend),
password=cmd_runner_fmt.as_opt_eq_val("--password"),
fingerprint_tls=cmd_runner_fmt.as_opt_eq_val("--server-cert-fingerprint"),

View file

@ -0,0 +1,502 @@
#!/usr/bin/python
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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()

View file

@ -0,0 +1,402 @@
#!/usr/bin/python
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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_policy
short_description: Manage Kopia snapshot policies
author:
- Dexter Le (@munchtoast)
version_added: "13.1.0"
description:
- Manage Kopia snapshot policies using the Kopia CLI.
- Supports setting, deleting, showing, and listing policies.
- Policies control retention, scheduling, file exclusions, and compression for snapshots.
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 Kopia policy.
- V(set) creates or updates a policy. At least one policy option must be provided.
- V(deleted) removes the policy for O(target) or the global policy when O(global_policy=true).
- V(listed) lists all defined policies.
- V(shown) displays the effective policy for O(target), including inherited values.
type: str
choices: [set, deleted, listed, shown]
default: set
target:
description:
- Policy target in one of the following forms.
- A per-path policy C(user@host:/path).
- A per-host policy C(@host).
- A per-user policy C(user@host).
- Required if O(state=set), O(state=deleted), or O(state=shown), unless O(global_policy=true).
type: str
global_policy:
description:
- When V(true), operate on the global policy instead of a per-target policy.
- When V(true), O(target) must not be set.
type: bool
default: false
retention:
description:
- Snapshot retention settings. All sub-options accept an integer or the string C(inherit)
to remove an override and fall back to the parent policy.
type: dict
suboptions:
keep_latest:
description:
- Number of most-recent snapshots to keep.
- Pass C(inherit) to remove this override.
type: str
keep_hourly:
description:
- Number of most-recent hourly snapshots to keep.
- Pass C(inherit) to remove this override.
type: str
keep_daily:
description:
- Number of most-recent daily snapshots to keep.
- Pass C(inherit) to remove this override.
type: str
keep_weekly:
description:
- Number of most-recent weekly snapshots to keep.
- Pass C(inherit) to remove this override.
type: str
keep_monthly:
description:
- Number of most-recent monthly snapshots to keep.
- Pass C(inherit) to remove this override.
type: str
keep_annual:
description:
- Number of most-recent annual snapshots to keep.
- Pass C(inherit) to remove this override.
type: str
ignore_identical:
description:
- Do not save a new snapshot if its contents are identical to the previous one.
- Accepts V(true), V(false), or C(inherit).
type: str
scheduling:
description:
- Snapshot scheduling settings.
type: dict
suboptions:
interval:
description:
- Time between automatic snapshots, for example C(1h), C(30m), or C(24h).
type: str
times:
description:
- List of times of day at which to take snapshots, in C(HH:mm) format.
- Pass C(inherit) as the only list entry to remove this override.
type: list
elements: str
manual:
description:
- When V(true), only create snapshots manually and disable automatic scheduling.
type: bool
files:
description:
- File and directory exclusion settings.
type: dict
suboptions:
add_ignore:
description:
- List of path patterns to add to the ignore list.
type: list
elements: str
remove_ignore:
description:
- List of path patterns to remove from the ignore list.
type: list
elements: str
max_file_size:
description:
- Exclude files larger than this size. Accepts a byte count or human-readable string
such as C(100MB).
type: str
one_file_system:
description:
- When V(true), do not cross filesystem boundaries during backup.
- Accepts V(true), V(false), or C(inherit).
type: str
ignore_cache_dirs:
description:
- When V(true), ignore directories containing a C(CACHEDIR.TAG) file.
- Accepts V(true), V(false), or C(inherit).
type: str
compression:
description:
- Compression algorithm to apply to snapshot data.
- Refer to C(kopia policy set --compression help) for the list of supported algorithms.
type: str
"""
EXAMPLES = r"""
- name: Set a retention policy for a path
community.general.kopia_policy:
state: set
target: "user@hostname:/home/user"
retention:
keep_latest: "10"
keep_daily: "7"
keep_weekly: "4"
keep_monthly: "6"
config: /etc/kopia/root.config
- name: Set a scheduled snapshot policy
community.general.kopia_policy:
state: set
target: "user@hostname:/var/www"
scheduling:
times:
- "02:00"
- "14:00"
config: /etc/kopia/root.config
- name: Set the global policy with compression and ignore rules
community.general.kopia_policy:
state: set
global_policy: true
compression: zstd
files:
add_ignore:
- "*.tmp"
- ".cache"
ignore_cache_dirs: "true"
config: /etc/kopia/root.config
- name: Inherit keep_daily from the parent policy
community.general.kopia_policy:
state: set
target: "user@hostname:/home/user"
retention:
keep_daily: inherit
config: /etc/kopia/root.config
- name: Show the effective policy for a target
community.general.kopia_policy:
state: shown
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
- name: List all defined policies
community.general.kopia_policy:
state: listed
config: /etc/kopia/root.config
- name: Delete a policy for a specific target
community.general.kopia_policy:
state: deleted
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
"""
RETURN = r"""
kopia_policy:
description: Output from the Kopia policy 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
def _fmt_retention(value):
"""Expand the retention dict into --keep-* and --ignore-identical-snapshots flags."""
if not value:
return []
flag_map = {
"keep_latest": "--keep-latest",
"keep_hourly": "--keep-hourly",
"keep_daily": "--keep-daily",
"keep_weekly": "--keep-weekly",
"keep_monthly": "--keep-monthly",
"keep_annual": "--keep-annual",
"ignore_identical": "--ignore-identical-snapshots",
}
result = []
for param, flag in flag_map.items():
v = value.get(param)
if v is not None:
result.extend([flag, str(v)])
return result
def _fmt_scheduling(value):
"""Expand the scheduling dict into --snapshot-interval, --snapshot-time, --manual flags."""
if not value:
return []
result = []
if value.get("interval") is not None:
result.extend(["--snapshot-interval", value["interval"]])
if value.get("times") is not None:
result.extend(["--snapshot-time", ",".join(value["times"])])
if value.get("manual"):
result.append("--manual")
return result
def _fmt_files(value):
"""Expand the files dict into file-handling flags."""
if not value:
return []
result = []
for path in value.get("add_ignore") or []:
result.extend(["--add-ignore", path])
for path in value.get("remove_ignore") or []:
result.extend(["--remove-ignore", path])
if value.get("max_file_size") is not None:
result.extend(["--max-file-size", value["max_file_size"]])
if value.get("one_file_system") is not None:
result.extend(["--one-file-system", value["one_file_system"]])
if value.get("ignore_cache_dirs") is not None:
result.extend(["--ignore-cache-dirs", value["ignore_cache_dirs"]])
return result
class KopiaPolicy(StateModuleHelper):
module = dict(
supports_check_mode=True,
argument_spec=dict(
**KOPIA_COMMON_ARGUMENT_SPEC,
state=dict(
type="str",
default="set",
choices=["set", "deleted", "listed", "shown"],
),
target=dict(type="str"),
global_policy=dict(type="bool", default=False),
retention=dict(
type="dict",
options=dict(
keep_latest=dict(type="str"),
keep_hourly=dict(type="str"),
keep_daily=dict(type="str"),
keep_weekly=dict(type="str"),
keep_monthly=dict(type="str"),
keep_annual=dict(type="str"),
ignore_identical=dict(type="str"),
),
),
scheduling=dict(
type="dict",
options=dict(
interval=dict(type="str"),
times=dict(type="list", elements="str"),
manual=dict(type="bool"),
),
),
files=dict(
type="dict",
options=dict(
add_ignore=dict(type="list", elements="str"),
remove_ignore=dict(type="list", elements="str"),
max_file_size=dict(type="str"),
one_file_system=dict(type="str"),
ignore_cache_dirs=dict(type="str"),
),
),
compression=dict(type="str"),
),
required_if=[
("state", "set", ["target", "global_policy"], True),
("state", "deleted", ["target", "global_policy"], True),
("state", "shown", ["target", "global_policy"], True),
],
)
def __init_module__(self):
self.runner = kopia_runner(
self.module,
extra_formats=dict(
list_policies=cmd_runner_fmt.as_fixed("policy", "list"),
target=cmd_runner_fmt.as_list(),
global_policy=cmd_runner_fmt.as_bool("--global"),
retention=cmd_runner_fmt.as_func(_fmt_retention),
scheduling=cmd_runner_fmt.as_func(_fmt_scheduling),
files=cmd_runner_fmt.as_func(_fmt_files),
compression=cmd_runner_fmt.as_opt_val("--compression"),
),
)
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_policies 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 state_set(self):
with self.runner(
"cli_action state target global_policy retention scheduling files compression config",
output_process=self._process_command_output(True),
check_mode_skip=True,
) as ctx:
ctx.run(cli_action="policy")
def state_deleted(self):
with self.runner(
"cli_action state target global_policy config",
output_process=self._process_command_output(True, "no such policy"),
check_mode_skip=True,
) as ctx:
ctx.run(cli_action="policy")
def state_listed(self):
with self.runner(
"cli_action state config",
output_process=self._process_command_output(True),
) as ctx:
ctx.run(cli_action="policy")
def state_shown(self):
with self.runner(
"cli_action state target global_policy config",
output_process=self._process_command_output(True),
) as ctx:
ctx.run(cli_action="policy")
def main():
KopiaPolicy.execute()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,304 @@
#!/usr/bin/python
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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_server
short_description: Manage Kopia server users and ACL entries
author:
- Dexter Le (@munchtoast)
version_added: "13.1.0"
description:
- Manage users and access control list (ACL) entries on a Kopia repository server.
- Supports creating, updating, and deleting server users, and adding, listing,
and deleting ACL rules.
- This module targets the repository-side user and ACL configuration stored inside
the repository itself, not the running server process.
- To manage the server process lifecycle use your system's service manager (for
example C(ansible.builtin.systemd)).
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 resource.
- V(user_present) creates or updates a server user. Requires O(username).
- V(user_absent) removes a server user. Requires O(username).
- V(users_listed) lists all server users. No additional options required.
- V(acl_present) adds an ACL entry. Requires O(acl_user) and O(acl_access).
- V(acl_absent) removes an ACL entry. Requires O(acl_user).
- V(acl_listed) lists all ACL entries. No additional options required.
- V(acl_enabled) enables ACL enforcement and installs default entries.
No additional options required.
type: str
choices: [user_present, user_absent, users_listed, acl_present, acl_absent, acl_listed, acl_enabled]
default: user_present
username:
description:
- Repository server username in C(user@hostname) format.
- Required if O(state=user_present) or O(state=user_absent).
type: str
user_password:
description:
- Password for the server user.
- Required when O(state=user_present) and O(user_password_hash) is not set.
- Mutually exclusive with O(user_password_hash).
type: str
user_password_hash:
description:
- Pre-hashed password for the server user.
- Required when O(state=user_present) and O(user_password) is not set.
- Mutually exclusive with O(user_password).
type: str
acl_user:
description:
- User the ACL rule applies to, in C(user@hostname) format.
- Required if O(state=acl_present) or O(state=acl_absent).
type: str
acl_access:
description:
- Access level granted to O(acl_user).
- Required if O(state=acl_present).
- Refer to Kopia documentation for supported access level values.
type: str
acl_target:
description:
- Manifests targeted by the ACL rule, in C(type:T,key1:value1,...) format.
- Optional if O(state=acl_present).
type: str
acl_no_overwrite:
description:
- When V(true), do not overwrite an existing rule with the same user and target.
- Optional if O(state=acl_present).
type: bool
default: false
"""
EXAMPLES = r"""
- name: Add a repository server user
community.general.kopia_server:
state: user_present
username: alice@backuphost
user_password: secretpassword
config: /etc/kopia/root.config
- name: Update a user with a pre-hashed password
community.general.kopia_server:
state: user_present
username: alice@backuphost
user_password_hash: "$2a$12$..."
config: /etc/kopia/root.config
- name: Remove a repository server user
community.general.kopia_server:
state: user_absent
username: alice@backuphost
config: /etc/kopia/root.config
- name: List all server users
community.general.kopia_server:
state: users_listed
config: /etc/kopia/root.config
- name: Enable ACL enforcement with default entries
community.general.kopia_server:
state: acl_enabled
config: /etc/kopia/root.config
- name: Add an ACL entry granting full access
community.general.kopia_server:
state: acl_present
acl_user: alice@backuphost
acl_access: FULL
config: /etc/kopia/root.config
- name: Add a targeted ACL entry
community.general.kopia_server:
state: acl_present
acl_user: bob@backuphost
acl_access: READ
acl_target: "type:snapshot,username:bob"
config: /etc/kopia/root.config
- name: Delete an ACL entry
community.general.kopia_server:
state: acl_absent
acl_user: alice@backuphost
config: /etc/kopia/root.config
- name: List all ACL entries
community.general.kopia_server:
state: acl_listed
config: /etc/kopia/root.config
"""
RETURN = r"""
kopia_server:
description: Output from the Kopia server 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 module states to (cli_group, cli_subcommand) pairs used in _run_server_cmd().
_STATE_CLI_MAP = {
"user_present": ("users", "set"),
"user_absent": ("users", "delete"),
"users_listed": ("users", "list"),
"acl_present": ("acl", "add"),
"acl_absent": ("acl", "delete"),
"acl_listed": ("acl", "list"),
"acl_enabled": ("acl", "enable"),
}
class KopiaServer(StateModuleHelper):
module = dict(
supports_check_mode=True,
argument_spec=dict(
**KOPIA_COMMON_ARGUMENT_SPEC,
state=dict(
type="str",
default="user_present",
choices=[
"user_present",
"user_absent",
"users_listed",
"acl_present",
"acl_absent",
"acl_listed",
"acl_enabled",
],
),
username=dict(type="str"),
user_password=dict(type="str", no_log=True),
user_password_hash=dict(type="str", no_log=True),
acl_user=dict(type="str"),
acl_access=dict(type="str"),
acl_target=dict(type="str"),
acl_no_overwrite=dict(type="bool", default=False),
),
required_if=[
("state", "user_present", ["username"]),
("state", "user_absent", ["username"]),
("state", "acl_present", ["acl_user", "acl_access"]),
("state", "acl_absent", ["acl_user"]),
],
mutually_exclusive=[
("user_password", "user_password_hash"),
],
)
def __init_module__(self):
self.runner = kopia_runner(
self.module,
extra_formats=dict(
list_users=cmd_runner_fmt.as_fixed("server", "users", "list"),
server_group=cmd_runner_fmt.as_list(),
server_subcommand=cmd_runner_fmt.as_list(),
username=cmd_runner_fmt.as_list(),
user_password=cmd_runner_fmt.as_opt_val("--user-password"),
user_password_hash=cmd_runner_fmt.as_opt_val("--user-password-hash"),
acl_user=cmd_runner_fmt.as_opt_val("--user"),
acl_access=cmd_runner_fmt.as_opt_val("--access"),
acl_target=cmd_runner_fmt.as_opt_val("--target"),
acl_no_overwrite=cmd_runner_fmt.as_bool("--no-overwrite"),
),
)
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_users 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_server_cmd(self, args_order, ignore_err_msg="", check_mode_skip=True, **run_kwargs):
group, subcommand = _STATE_CLI_MAP[self.vars.state]
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="server", server_group=group, server_subcommand=subcommand, **run_kwargs)
def state_user_present(self):
self._run_server_cmd(
"cli_action server_group server_subcommand username user_password user_password_hash config",
ignore_err_msg="already exists",
)
def state_user_absent(self):
self._run_server_cmd(
"cli_action server_group server_subcommand username config",
ignore_err_msg="no such user",
)
def state_users_listed(self):
self._run_server_cmd(
"cli_action server_group server_subcommand config",
check_mode_skip=False,
)
def state_acl_present(self):
self._run_server_cmd(
"cli_action server_group server_subcommand acl_user acl_access acl_target acl_no_overwrite config",
)
def state_acl_absent(self):
self._run_server_cmd(
"cli_action server_group server_subcommand acl_user config",
ignore_err_msg="no such rule",
)
def state_acl_listed(self):
self._run_server_cmd(
"cli_action server_group server_subcommand config",
check_mode_skip=False,
)
def state_acl_enabled(self):
self._run_server_cmd(
"cli_action server_group server_subcommand config",
)
def main():
KopiaServer.execute()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,299 @@
#!/usr/bin/python
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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_snapshot
short_description: Manage Kopia snapshots
author:
- Dexter Le (@munchtoast)
version_added: "13.1.0"
description:
- Manage Kopia snapshots using the Kopia CLI.
- Supports creating, deleting, and verifying snapshots, as well as listing and expiring them.
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 Kopia snapshot.
- V(created) takes a new snapshot of O(source).
- V(deleted) deletes the snapshot identified by O(snapshot_id).
- V(expired) applies retention policy and deletes snapshots that no longer meet it.
This is a dry-run unless O(delete=true).
- V(listed) lists snapshots for O(source) or all sources when O(source) is not set.
- V(verified) verifies the integrity of snapshots identified by O(snapshot_id),
or all snapshots when O(snapshot_id) is not set.
type: str
choices: [created, deleted, expired, listed, verified]
default: created
source:
description:
- Path of the source directory or file to snapshot.
- Required if O(state=created).
- When O(state=listed) or O(state=expired), limits the operation to this source path.
type: str
snapshot_id:
description:
- One or more snapshot manifest IDs to operate on.
- Required if O(state=deleted).
- Optional if O(state=verified); verifies all snapshots when omitted.
type: list
elements: str
description:
description:
- Free-form description to attach to the snapshot.
- Optional if O(state=created).
type: str
tags:
description:
- List of tags to attach to or filter snapshots by, in C(key:value) format.
- Optional if O(state=created) or O(state=listed).
type: list
elements: str
all_sources:
description:
- When V(true), operate on all snapshot sources rather than only the current user and host.
- Optional if O(state=listed) or O(state=expired).
type: bool
default: false
delete:
description:
- When V(true), actually delete snapshots that have expired according to the retention policy.
- When V(false) the expiration is a dry-run and no snapshots are removed.
- Required to be V(true) to perform deletion when O(state=expired).
- Required to be V(true) to confirm deletion when O(state=deleted).
type: bool
default: false
parallel:
description:
- Number of parallel upload or verification workers.
- Optional if O(state=created) or O(state=verified).
type: int
fail_fast:
description:
- When V(true), abort the snapshot on the first error encountered.
- Optional if O(state=created).
type: bool
default: false
ignore_identical:
description:
- When V(true), skip saving a new snapshot if the contents are identical to the previous one.
- Optional if O(state=created).
type: bool
default: false
verify_files_percent:
description:
- Randomly verify this percentage of files during verification.
- Value must be between C(0.0) and C(100.0).
- Optional if O(state=verified).
type: float
"""
EXAMPLES = r"""
- name: Create a snapshot of /home/user
community.general.kopia_snapshot:
state: created
source: /home/user
password: secret
config: /etc/kopia/root.config
- name: Create a tagged snapshot with a description
community.general.kopia_snapshot:
state: created
source: /var/www
password: secret
description: "pre-deploy backup"
tags:
- env:production
- app:web
- name: List all snapshots for a source
community.general.kopia_snapshot:
state: listed
source: /home/user
config: /etc/kopia/root.config
- name: List snapshots across all users and hosts
community.general.kopia_snapshot:
state: listed
all_sources: true
config: /etc/kopia/root.config
- name: Delete a specific snapshot
community.general.kopia_snapshot:
state: deleted
snapshot_id:
- abc1234def5678
delete: true
config: /etc/kopia/root.config
- name: Expire snapshots (dry run)
community.general.kopia_snapshot:
state: expired
source: /home/user
config: /etc/kopia/root.config
- name: Expire snapshots (apply deletion)
community.general.kopia_snapshot:
state: expired
source: /home/user
delete: true
config: /etc/kopia/root.config
- name: Verify all snapshots
community.general.kopia_snapshot:
state: verified
password: secret
config: /etc/kopia/root.config
- name: Verify specific snapshots and check 10 percent of files
community.general.kopia_snapshot:
state: verified
snapshot_id:
- abc1234def5678
- def5678abc1234
verify_files_percent: 10.0
config: /etc/kopia/root.config
"""
RETURN = r"""
kopia_snapshot:
description: Output from the Kopia snapshot 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
class KopiaSnapshot(StateModuleHelper):
module = dict(
supports_check_mode=True,
argument_spec=dict(
**KOPIA_COMMON_ARGUMENT_SPEC,
state=dict(
type="str",
default="created",
choices=["created", "deleted", "expired", "listed", "verified"],
),
source=dict(type="str"),
snapshot_id=dict(type="list", elements="str"),
description=dict(type="str"),
tags=dict(type="list", elements="str"),
all_sources=dict(type="bool", default=False),
delete=dict(type="bool", default=False),
parallel=dict(type="int"),
fail_fast=dict(type="bool", default=False),
ignore_identical=dict(type="bool", default=False),
verify_files_percent=dict(type="float"),
),
required_if=[
("state", "created", ["source"]),
("state", "deleted", ["snapshot_id"]),
],
)
def __init_module__(self):
self.runner = kopia_runner(
self.module,
extra_formats=dict(
list_snapshots=cmd_runner_fmt.as_fixed("snapshot", "list"),
source=cmd_runner_fmt.as_list(),
snapshot_id=cmd_runner_fmt.as_list(),
description=cmd_runner_fmt.as_opt_eq_val("--description"),
tags=cmd_runner_fmt.as_func(lambda v: [x for tag in v for x in ("--tags", tag)]),
all_sources=cmd_runner_fmt.as_bool("--all"),
delete=cmd_runner_fmt.as_bool("--delete"),
parallel=cmd_runner_fmt.as_opt_eq_val("--parallel"),
fail_fast=cmd_runner_fmt.as_bool("--fail-fast"),
ignore_identical=cmd_runner_fmt.as_bool("--ignore-identical-snapshots"),
verify_files_percent=cmd_runner_fmt.as_opt_eq_val("--verify-files-percent"),
),
)
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_snapshots 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 state_created(self):
with self.runner(
"cli_action state source description tags parallel fail_fast ignore_identical password config",
output_process=self._process_command_output(True),
check_mode_skip=True,
) as ctx:
ctx.run(cli_action="snapshot")
def state_deleted(self):
with self.runner(
"cli_action state snapshot_id delete config",
output_process=self._process_command_output(True),
check_mode_skip=True,
) as ctx:
ctx.run(cli_action="snapshot")
def state_expired(self):
with self.runner(
"cli_action state source all_sources delete config",
output_process=self._process_command_output(True),
check_mode_skip=True,
) as ctx:
ctx.run(cli_action="snapshot")
def state_listed(self):
with self.runner(
"cli_action state source all_sources tags config",
output_process=self._process_command_output(True),
) as ctx:
ctx.run(cli_action="snapshot")
def state_verified(self):
with self.runner(
"cli_action state snapshot_id parallel verify_files_percent password config",
output_process=self._process_command_output(True),
check_mode_skip=True,
) as ctx:
ctx.run(cli_action="snapshot")
def main():
KopiaSnapshot.execute()
if __name__ == "__main__":
main()

View file

@ -8,7 +8,7 @@ import pytest
from ansible_collections.community.general.plugins.module_utils._kopia import (
KOPIA_COMMON_ARGUMENT_SPEC,
REPOSITORY_STATE_MAP,
STATE_MAP,
fmt_backend,
)
@ -34,22 +34,35 @@ def test_common_argument_spec_only_two_keys():
# ---------------------------------------------------------------------------
# REPOSITORY_STATE_MAP
# STATE_MAP
# ---------------------------------------------------------------------------
def test_repository_state_map_entries():
assert REPOSITORY_STATE_MAP["created"] == "create"
assert REPOSITORY_STATE_MAP["connected"] == "connect"
assert REPOSITORY_STATE_MAP["disconnected"] == "disconnect"
assert REPOSITORY_STATE_MAP["synced"] == "sync-to"
assert REPOSITORY_STATE_MAP["throttled"] == "throttle"
def test_state_map_repository_entries():
assert STATE_MAP["created"] == "create"
assert STATE_MAP["connected"] == "connect"
assert STATE_MAP["disconnected"] == "disconnect"
assert STATE_MAP["synced"] == "sync-to"
assert STATE_MAP["throttled"] == "throttle"
def test_state_map_snapshot_entries():
assert STATE_MAP["deleted"] == "delete"
assert STATE_MAP["expired"] == "expire"
assert STATE_MAP["listed"] == "list"
assert STATE_MAP["verified"] == "verify"
def test_state_map_policy_entries():
assert STATE_MAP["set"] == "set"
assert STATE_MAP["shown"] == "show"
# ---------------------------------------------------------------------------
# fmt_backend
# ---------------------------------------------------------------------------
TC_FMT_BACKEND = dict(
server=(
{"provider": "server"},
@ -113,29 +126,132 @@ TC_FMT_BACKEND = dict(
{"provider": "azure", "container": "my-container", "storage_account": "myaccount"},
["azure", "--container=my-container", "--storage-account=myaccount"],
),
azure_service_principal=(
{
"provider": "azure",
"container": "my-container",
"storage_account": "myaccount",
"client_id": "cid",
"client_secret": "csecret",
"tenant_id": "tid",
},
[
"azure",
"--container=my-container",
"--storage-account=myaccount",
"--client-id=cid",
"--client-secret=csecret",
"--tenant-id=tid",
],
),
azure_federated_token=(
{
"provider": "azure",
"container": "my-container",
"storage_account": "myaccount",
"azure_federated_token_file": "/var/run/secrets/azure/token",
},
[
"azure",
"--container=my-container",
"--storage-account=myaccount",
"--azure-federated-token-file=/var/run/secrets/azure/token",
],
),
gcs_full=(
{"provider": "gcs", "bucket": "my-bucket", "credentials_file": "/etc/gcs.json", "prefix": "kopia/"},
{
"provider": "gcs",
"bucket": "my-bucket",
"credentials_file": "/etc/gcs.json",
"prefix": "kopia/",
},
["gcs", "--bucket=my-bucket", "--credentials-file=/etc/gcs.json", "--prefix=kopia/"],
),
gcs_embed_credentials=(
{
"provider": "gcs",
"bucket": "my-bucket",
"embed_credentials": True,
},
["gcs", "--bucket=my-bucket", "--embed-credentials"],
),
gcs_embed_credentials_false=(
{
"provider": "gcs",
"bucket": "my-bucket",
"embed_credentials": False,
},
["gcs", "--bucket=my-bucket"],
),
gcs_read_only=(
{
"provider": "gcs",
"bucket": "my-bucket",
"read_only": True,
},
["gcs", "--bucket=my-bucket", "--read-only"],
),
gdrive=(
{"provider": "gdrive", "folder_id": "abc123", "credentials_file": "/etc/gdrive.json"},
["gdrive", "--folder-id=abc123", "--credentials-file=/etc/gdrive.json"],
),
gdrive_read_only=(
{"provider": "gdrive", "folder_id": "abc123", "read_only": True},
["gdrive", "--folder-id=abc123", "--read-only"],
),
b2_full=(
{"provider": "b2", "bucket": "my-b2-bucket", "access_key": "kid", "secret_access_key": "sec"},
["b2", "--bucket=my-b2-bucket", "--key-id=kid", "--key=sec"],
),
rclone=(
rclone_minimal=(
{"provider": "rclone", "path": "remote:backup"},
["rclone", "--remote-path=remote:backup"],
),
rclone_with_exe=(
{"provider": "rclone", "path": "remote:backup", "rclone_exe": "/usr/local/bin/rclone"},
["rclone", "--remote-path=remote:backup", "--rclone-exe=/usr/local/bin/rclone"],
),
rclone_with_args=(
{
"provider": "rclone",
"path": "remote:backup",
"rclone_args": ["--transfers=4", "--checkers=8"],
},
[
"rclone",
"--remote-path=remote:backup",
"--rclone-args",
"--transfers=4",
"--rclone-args",
"--checkers=8",
],
),
rclone_with_env=(
{
"provider": "rclone",
"path": "remote:backup",
"rclone_env": ["RCLONE_CONFIG=/etc/rclone.conf", "HOME=/root"],
},
[
"rclone",
"--remote-path=remote:backup",
"--rclone-env",
"RCLONE_CONFIG=/etc/rclone.conf",
"--rclone-env",
"HOME=/root",
],
),
rclone_embed_config=(
{"provider": "rclone", "path": "remote:backup", "embed_rclone_config": True},
["rclone", "--remote-path=remote:backup", "--embed-rclone-config"],
),
sftp_full=(
{
"provider": "sftp",
"path": "/backup",
"host": "sftp.example.com",
"username": "admin",
"port": "22",
"port": 22,
"keyfile": "/root/.ssh/id_rsa",
"known_hosts": "/root/.ssh/known_hosts",
},
@ -149,6 +265,79 @@ TC_FMT_BACKEND = dict(
"--known-hosts=/root/.ssh/known_hosts",
],
),
sftp_password=(
{
"provider": "sftp",
"path": "/backup",
"host": "sftp.example.com",
"username": "admin",
"sftp_password": "s3cr3t",
},
[
"sftp",
"--path=/backup",
"--host=sftp.example.com",
"--username=admin",
"--sftp-password=s3cr3t",
],
),
sftp_key_data=(
{
"provider": "sftp",
"path": "/backup",
"host": "sftp.example.com",
"username": "admin",
"key_data": "-----BEGIN RSA PRIVATE KEY-----\n...",
"known_hosts_data": "sftp.example.com ssh-rsa AAAA...",
},
[
"sftp",
"--path=/backup",
"--host=sftp.example.com",
"--username=admin",
"--key-data=-----BEGIN RSA PRIVATE KEY-----\n...",
"--known-hosts-data=sftp.example.com ssh-rsa AAAA...",
],
),
sftp_embed_credentials=(
{
"provider": "sftp",
"path": "/backup",
"host": "sftp.example.com",
"username": "admin",
"embed_credentials": True,
},
[
"sftp",
"--path=/backup",
"--host=sftp.example.com",
"--username=admin",
"--embed-credentials",
],
),
sftp_external=(
{
"provider": "sftp",
"path": "/backup",
"host": "sftp.example.com",
"username": "admin",
"external": True,
"ssh_command": "/usr/bin/ssh",
"ssh_args": ["-o", "StrictHostKeyChecking=no"],
},
[
"sftp",
"--path=/backup",
"--host=sftp.example.com",
"--username=admin",
"--external",
"--ssh-command=/usr/bin/ssh",
"--ssh-args",
"-o",
"--ssh-args",
"StrictHostKeyChecking=no",
],
),
webdav_full=(
{
"provider": "webdav",
@ -162,6 +351,14 @@ TC_FMT_BACKEND = dict(
{"provider": "s3", "bucket": "b", "access_key": None, "secret_access_key": None},
["s3", "--bucket=b"],
),
bool_false_skipped=(
{"provider": "gcs", "bucket": "b", "embed_credentials": False, "read_only": False},
["gcs", "--bucket=b"],
),
empty_list_skipped=(
{"provider": "rclone", "path": "remote:b", "rclone_args": []},
["rclone", "--remote-path=remote:b"],
),
)
TC_FMT_BACKEND_IDS = sorted(TC_FMT_BACKEND.keys())

View file

@ -0,0 +1,11 @@
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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])

View file

@ -0,0 +1,455 @@
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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

View file

@ -0,0 +1,11 @@
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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_policy
from .uthelper import RunCommandMock, UTHelper
UTHelper.from_module(kopia_policy, __name__, mocks=[RunCommandMock])

View file

@ -0,0 +1,359 @@
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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}
list_ok: &list-ok
command: [/testbin/kopia, policy, list, --config-file=/etc/kopia/root.config]
environ: *env-def
rc: 0
out: "user@host:/home/user"
err: ''
list_empty: &list-empty
command: [/testbin/kopia, policy, list, --config-file=/etc/kopia/root.config]
environ: *env-def
rc: 0
out: ''
err: ''
test_cases:
- id: set_retention_policy
input:
state: set
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
retention:
keep_latest: "10"
keep_daily: "7"
keep_weekly: "4"
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- policy
- set
- user@hostname:/home/user
- --keep-latest
- "10"
- --keep-daily
- "7"
- --keep-weekly
- "4"
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-ok
- id: set_global_policy_with_compression
input:
state: set
global_policy: true
config: /etc/kopia/root.config
compression: zstd
files:
add_ignore:
- "*.tmp"
- ".cache"
ignore_cache_dirs: "true"
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- policy
- set
- --global
- --add-ignore
- "*.tmp"
- --add-ignore
- .cache
- --ignore-cache-dirs
- "true"
- --compression
- zstd
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-ok
- id: set_scheduling_policy
input:
state: set
target: "user@hostname:/var/www"
config: /etc/kopia/root.config
scheduling:
times:
- "02:00"
- "14:00"
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- policy
- set
- user@hostname:/var/www
- --snapshot-time
- "02:00,14:00"
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-ok
- id: show_policy
input:
state: shown
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- policy
- show
- user@hostname:/home/user
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Policy for user@hostname:/home/user"
err: ''
- *list-ok
- id: list_policies
input:
state: listed
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- policy
- list
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "user@host:/home/user"
err: ''
- *list-ok
- id: delete_policy
input:
state: deleted
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
output:
changed: true
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- policy
- delete
- user@hostname:/home/user
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-empty
- id: delete_policy_no_such_policy
input:
state: deleted
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- policy
- delete
- user@hostname:/home/user
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 1
out: ''
err: 'no such policy'
- *list-empty
- id: set_scheduling_interval
input:
state: set
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
scheduling:
interval: "1h"
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- policy
- set
- user@hostname:/home/user
- --snapshot-interval
- "1h"
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-ok
- id: set_scheduling_manual
input:
state: set
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
scheduling:
manual: true
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- policy
- set
- user@hostname:/home/user
- --manual
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-ok
- id: set_policy_files_remove_ignore
input:
state: set
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
files:
remove_ignore:
- "*.tmp"
max_file_size: "100MB"
one_file_system: "true"
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- policy
- set
- user@hostname:/home/user
- --remove-ignore
- "*.tmp"
- --max-file-size
- 100MB
- --one-file-system
- "true"
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-ok
- id: set_retention_policy_inherit
input:
state: set
target: "user@hostname:/home/user"
config: /etc/kopia/root.config
retention:
keep_daily: inherit
keep_weekly: inherit
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- policy
- set
- user@hostname:/home/user
- --keep-daily
- inherit
- --keep-weekly
- inherit
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-ok
- id: delete_global_policy
input:
state: deleted
global_policy: true
config: /etc/kopia/root.config
output:
changed: true
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- policy
- delete
- --global
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-empty
- id: show_global_policy
input:
state: shown
global_policy: true
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- policy
- show
- --global
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Global policy settings"
err: ''
- *list-ok

View file

@ -0,0 +1,11 @@
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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_server
from .uthelper import RunCommandMock, UTHelper
UTHelper.from_module(kopia_server, __name__, mocks=[RunCommandMock])

View file

@ -0,0 +1,315 @@
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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}
users_list_ok: &users-list-ok
command: [/testbin/kopia, server, users, list, --config-file=/etc/kopia/root.config]
environ: *env-def
rc: 0
out: "alice@backuphost"
err: ''
users_list_empty: &users-list-empty
command: [/testbin/kopia, server, users, list, --config-file=/etc/kopia/root.config]
environ: *env-def
rc: 0
out: ''
err: ''
test_cases:
- id: add_user
input:
state: user_present
username: alice@backuphost
user_password: secretpassword
config: /etc/kopia/root.config
output:
changed: true
mocks:
run_command:
- *users-list-empty
- command:
- /testbin/kopia
- server
- users
- set
- alice@backuphost
- --user-password
- secretpassword
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *users-list-ok
- id: add_user_with_hash
input:
state: user_present
username: alice@backuphost
user_password_hash: "$2a$12$hashedvalue"
config: /etc/kopia/root.config
output:
changed: true
mocks:
run_command:
- *users-list-empty
- command:
- /testbin/kopia
- server
- users
- set
- alice@backuphost
- --user-password-hash
- "$2a$12$hashedvalue"
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *users-list-ok
- id: remove_user
input:
state: user_absent
username: alice@backuphost
config: /etc/kopia/root.config
output:
changed: true
mocks:
run_command:
- *users-list-ok
- command:
- /testbin/kopia
- server
- users
- delete
- alice@backuphost
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *users-list-empty
- id: remove_user_no_such_user
input:
state: user_absent
username: alice@backuphost
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *users-list-empty
- command:
- /testbin/kopia
- server
- users
- delete
- alice@backuphost
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 1
out: ''
err: 'no such user'
- *users-list-empty
- id: list_users
input:
state: users_listed
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *users-list-ok
- command:
- /testbin/kopia
- server
- users
- list
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "alice@backuphost"
err: ''
- *users-list-ok
- id: enable_acl
input:
state: acl_enabled
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *users-list-ok
- command:
- /testbin/kopia
- server
- acl
- enable
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *users-list-ok
- id: add_acl_entry
input:
state: acl_present
acl_user: alice@backuphost
acl_access: FULL
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *users-list-ok
- command:
- /testbin/kopia
- server
- acl
- add
- --user
- alice@backuphost
- --access
- FULL
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *users-list-ok
- id: add_targeted_acl_entry
input:
state: acl_present
acl_user: bob@backuphost
acl_access: READ
acl_target: "type:snapshot,username:bob"
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *users-list-ok
- command:
- /testbin/kopia
- server
- acl
- add
- --user
- bob@backuphost
- --access
- READ
- --target
- "type:snapshot,username:bob"
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *users-list-ok
- id: delete_acl_entry
input:
state: acl_absent
acl_user: alice@backuphost
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *users-list-ok
- command:
- /testbin/kopia
- server
- acl
- delete
- --user
- alice@backuphost
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *users-list-ok
- id: list_acl_entries
input:
state: acl_listed
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *users-list-ok
- command:
- /testbin/kopia
- server
- acl
- list
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "alice@backuphost FULL"
err: ''
- *users-list-ok
- id: add_acl_entry_no_overwrite
input:
state: acl_present
acl_user: alice@backuphost
acl_access: FULL
acl_no_overwrite: true
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *users-list-ok
- command:
- /testbin/kopia
- server
- acl
- add
- --user
- alice@backuphost
- --access
- FULL
- --no-overwrite
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *users-list-ok
- id: delete_acl_entry_no_such_rule
input:
state: acl_absent
acl_user: alice@backuphost
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *users-list-ok
- command:
- /testbin/kopia
- server
- acl
- delete
- --user
- alice@backuphost
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 1
out: ''
err: 'no such rule'
- *users-list-ok

View file

@ -0,0 +1,11 @@
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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_snapshot
from .uthelper import RunCommandMock, UTHelper
UTHelper.from_module(kopia_snapshot, __name__, mocks=[RunCommandMock])

View file

@ -0,0 +1,392 @@
# Copyright (c) 2026, Dexter Le <dextersydney2001@gmail.com>
# 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}
list_ok: &list-ok
command: [/testbin/kopia, snapshot, list, --config-file=/etc/kopia/root.config]
environ: *env-def
rc: 0
out: "abc123 /home/user 2026-01-01 00:00:00 UTC"
err: ''
list_empty: &list-empty
command: [/testbin/kopia, snapshot, list, --config-file=/etc/kopia/root.config]
environ: *env-def
rc: 0
out: ''
err: ''
test_cases:
- id: create_snapshot
input:
state: created
source: /home/user
password: secret
config: /etc/kopia/root.config
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- snapshot
- create
- /home/user
- --password=secret
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Created snapshot abc123"
err: ''
- *list-ok
- id: create_snapshot_with_tags
input:
state: created
source: /var/www
password: secret
config: /etc/kopia/root.config
description: "pre-deploy backup"
tags:
- env:production
- app:web
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- snapshot
- create
- /var/www
- --description=pre-deploy backup
- --tags
- env:production
- --tags
- app:web
- --password=secret
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Created snapshot def456"
err: ''
- *list-ok
- id: list_snapshots
input:
state: listed
source: /home/user
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- list
- /home/user
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "abc123 /home/user 2026-01-01 00:00:00 UTC"
err: ''
- *list-ok
- id: list_all_sources
input:
state: listed
all_sources: true
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- list
- --all
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "abc123 /home/user 2026-01-01 00:00:00 UTC"
err: ''
- *list-ok
- id: delete_snapshot
input:
state: deleted
snapshot_id:
- abc1234def5678
delete: true
config: /etc/kopia/root.config
output:
changed: true
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- delete
- abc1234def5678
- --delete
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-empty
- id: expire_snapshots_dry_run
input:
state: expired
source: /home/user
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- expire
- /home/user
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Would delete 2 snapshots"
err: ''
- *list-ok
- id: expire_snapshots_apply
input:
state: expired
source: /home/user
delete: true
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- expire
- /home/user
- --delete
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Deleted 2 snapshots"
err: ''
- *list-ok
- id: verify_all_snapshots
input:
state: verified
password: secret
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- verify
- --password=secret
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Verified 5 files."
err: ''
- *list-ok
- id: verify_specific_snapshot
input:
state: verified
snapshot_id:
- abc1234def5678
verify_files_percent: 10.0
config: /etc/kopia/root.config
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- verify
- abc1234def5678
- --verify-files-percent=10.0
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Verified 1 snapshot."
err: ''
- *list-ok
- id: create_snapshot_with_parallel
input:
state: created
source: /home/user
password: secret
config: /etc/kopia/root.config
parallel: 4
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- snapshot
- create
- /home/user
- --parallel=4
- --password=secret
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Created snapshot abc123"
err: ''
- *list-ok
- id: create_snapshot_fail_fast
input:
state: created
source: /home/user
password: secret
config: /etc/kopia/root.config
fail_fast: true
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- snapshot
- create
- /home/user
- --fail-fast
- --password=secret
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Created snapshot abc123"
err: ''
- *list-ok
- id: create_snapshot_ignore_identical
input:
state: created
source: /home/user
password: secret
config: /etc/kopia/root.config
ignore_identical: true
output:
changed: true
mocks:
run_command:
- *list-empty
- command:
- /testbin/kopia
- snapshot
- create
- /home/user
- --ignore-identical-snapshots
- --password=secret
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Created snapshot abc123"
err: ''
- *list-ok
- id: delete_multiple_snapshots
input:
state: deleted
snapshot_id:
- abc1234def5678
- def5678abc1234
delete: true
config: /etc/kopia/root.config
output:
changed: true
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- delete
- abc1234def5678
- def5678abc1234
- --delete
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: ''
err: ''
- *list-empty
- id: verify_with_parallel
input:
state: verified
password: secret
config: /etc/kopia/root.config
parallel: 8
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- verify
- --parallel=8
- --password=secret
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "Verified 5 files."
err: ''
- *list-ok
- id: list_snapshots_with_tags
input:
state: listed
config: /etc/kopia/root.config
tags:
- env:production
output:
changed: false
mocks:
run_command:
- *list-ok
- command:
- /testbin/kopia
- snapshot
- list
- --tags
- env:production
- --config-file=/etc/kopia/root.config
environ: *env-def
rc: 0
out: "abc123 /home/user 2026-01-01 00:00:00 UTC"
err: ''
- *list-ok