#!/usr/bin/python # Copyright (c) 2023, Salvatore Mesoraca # GNU General Public License v3.0+ (see COPYING 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: kdeconfig short_description: Manage KDE configuration files version_added: "6.5.0" description: - Add or change individual settings in KDE configuration files. - It uses B(kwriteconfig) under the hood. options: path: description: - Path to the config file. If the file does not exist it is created. type: path required: true kwriteconfig_path: description: - Path to the kwriteconfig executable. If not specified, Ansible tries to discover it. type: path values: description: - List of values to set. type: list elements: dict suboptions: group: description: - The option's group. One between this and O(values[].groups) is required. type: str groups: description: - List of the option's groups. One between this and O(values[].group) is required. type: list elements: str key: description: - The option's name. type: str required: true value: description: - The option's value. One between this and O(values[].bool_value) is required. type: str bool_value: description: - Boolean value. - One between this and O(values[].value) is required. type: bool required: true backup: description: - Create a backup file. type: bool default: false extends_documentation_fragment: - files - community.general.attributes attributes: check_mode: support: full diff_mode: support: full requirements: - kwriteconfig author: - Salvatore Mesoraca (@smeso) """ EXAMPLES = r""" - name: Ensure "Homepage=https://www.ansible.com/" in group "Branding" community.general.kdeconfig: path: /etc/xdg/kickoffrc values: - group: Branding key: Homepage value: https://www.ansible.com/ mode: '0644' - name: Ensure "KEY=true" in groups "Group" and "Subgroup", and "KEY=VALUE" in Group2 community.general.kdeconfig: path: /etc/xdg/someconfigrc values: - groups: [Group, Subgroup] key: KEY bool_value: true - group: Group2 key: KEY value: VALUE backup: true """ RETURN = r""" # """ import os import shutil import tempfile import traceback from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_bytes, to_text class TemporaryDirectory: """Basic backport of tempfile.TemporaryDirectory""" def __init__(self, suffix="", prefix="tmp", dir=None): self.name = None self.name = tempfile.mkdtemp(suffix, prefix, dir) def __enter__(self): return self.name def rm(self): if self.name: shutil.rmtree(self.name, ignore_errors=True) self.name = None def __exit__(self, exc, value, tb): self.rm() def __del__(self): self.rm() def run_kwriteconfig(module, cmd, path, groups, key, value): """Invoke kwriteconfig with arguments""" args = [cmd, "--file", path, "--key", key] for group in groups: args.extend(["--group", group]) if isinstance(value, bool): args.extend(["--type", "bool"]) if value: args.append("true") else: args.append("false") else: args.extend(["--", value]) module.run_command(args, check_rc=True) def run_module(module, tmpdir, kwriteconfig): result = dict(changed=False, msg="OK", path=module.params["path"]) b_path = to_bytes(module.params["path"]) tmpfile = os.path.join(tmpdir, "file") b_tmpfile = to_bytes(tmpfile) diff = dict( before="", after="", before_header=result["path"], after_header=result["path"], ) try: with open(b_tmpfile, "wb") as dst: try: with open(b_path, "rb") as src: b_data = src.read() except IOError: result["changed"] = True else: dst.write(b_data) try: diff["before"] = to_text(b_data) except UnicodeError: diff["before"] = repr(b_data) except IOError: module.fail_json(msg="Unable to create temporary file", traceback=traceback.format_exc()) for row in module.params["values"]: groups = row["groups"] if groups is None: groups = [row["group"]] key = row["key"] value = row["bool_value"] if value is None: value = row["value"] run_kwriteconfig(module, kwriteconfig, tmpfile, groups, key, value) with open(b_tmpfile, "rb") as tmpf: b_data = tmpf.read() try: diff["after"] = to_text(b_data) except UnicodeError: diff["after"] = repr(b_data) result["changed"] = result["changed"] or diff["after"] != diff["before"] file_args = module.load_file_common_arguments(module.params) if module.check_mode: if not result["changed"]: shutil.copystat(b_path, b_tmpfile) uid, gid = module.user_and_group(b_path) os.chown(b_tmpfile, uid, gid) if module._diff: diff = {} else: diff = None result["changed"] = module.set_fs_attributes_if_different(file_args, result["changed"], diff=diff) if module._diff: result["diff"] = diff module.exit_json(**result) if result["changed"]: if module.params["backup"] and os.path.exists(b_path): result["backup_file"] = module.backup_local(result["path"]) try: module.atomic_move(b_tmpfile, os.path.abspath(b_path)) except IOError: module.ansible.fail_json( msg=f"Unable to move temporary file {tmpfile} to {result['path']}, IOError", traceback=traceback.format_exc(), ) if result["changed"]: module.set_fs_attributes_if_different(file_args, result["changed"]) else: if module._diff: diff = {} else: diff = None result["changed"] = module.set_fs_attributes_if_different(file_args, result["changed"], diff=diff) if module._diff: result["diff"] = diff module.exit_json(**result) def main(): single_value_arg = dict( group=dict(type="str"), groups=dict(type="list", elements="str"), key=dict(type="str", required=True, no_log=False), value=dict(type="str"), bool_value=dict(type="bool"), ) required_alternatives = [("group", "groups"), ("value", "bool_value")] module_args = dict( values=dict( type="list", elements="dict", options=single_value_arg, mutually_exclusive=required_alternatives, required_one_of=required_alternatives, required=True, ), path=dict(type="path", required=True), kwriteconfig_path=dict(type="path"), backup=dict(type="bool", default=False), ) module = AnsibleModule( argument_spec=module_args, add_file_common_args=True, supports_check_mode=True, ) kwriteconfig = None if module.params["kwriteconfig_path"] is not None: kwriteconfig = module.get_bin_path(module.params["kwriteconfig_path"], required=True) else: for progname in ("kwriteconfig6", "kwriteconfig5", "kwriteconfig", "kwriteconfig4"): kwriteconfig = module.get_bin_path(progname) if kwriteconfig is not None: break if kwriteconfig is None: module.fail_json(msg="kwriteconfig is not installed") for v in module.params["values"]: if not v["key"]: module.fail_json(msg="'key' cannot be empty") with TemporaryDirectory(dir=module.tmpdir) as tmpdir: run_module(module, tmpdir, kwriteconfig) if __name__ == "__main__": main()