1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-06-11 10:35:34 +00:00

feat: Add kopia_server module

- Manage Kopia repository server users and ACL entries via the
  Kopia CLI.
- Extends community.general._kopia doc fragment for shared password
  and config options.
- Uses fixed args for read-only _get() list_users method.
This commit is contained in:
munchtoast 2026-06-04 10:41:12 -04:00
parent bac4e25a86
commit 8d06b76e30
3 changed files with 630 additions and 0 deletions

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,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