summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/general/plugins/modules/dconf.py
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/community/general/plugins/modules/dconf.py')
-rw-r--r--ansible_collections/community/general/plugins/modules/dconf.py490
1 files changed, 490 insertions, 0 deletions
diff --git a/ansible_collections/community/general/plugins/modules/dconf.py b/ansible_collections/community/general/plugins/modules/dconf.py
new file mode 100644
index 000000000..8c325486c
--- /dev/null
+++ b/ansible_collections/community/general/plugins/modules/dconf.py
@@ -0,0 +1,490 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017, Branko Majic <branko@majic.rs>
+# 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 absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+module: dconf
+author:
+ - "Branko Majic (@azaghal)"
+short_description: Modify and read dconf database
+description:
+ - This module allows modifications and reading of C(dconf) database. The module
+ is implemented as a wrapper around C(dconf) tool. Please see the dconf(1) man
+ page for more details.
+ - Since C(dconf) requires a running D-Bus session to change values, the module
+ will try to detect an existing session and reuse it, or run the tool via
+ C(dbus-run-session).
+requirements:
+ - Optionally the C(gi.repository) Python library (usually included in the OS
+ on hosts which have C(dconf)); this will become a non-optional requirement
+ in a future major release of community.general.
+notes:
+ - This module depends on C(psutil) Python library (version 4.0.0 and upwards),
+ C(dconf), C(dbus-send), and C(dbus-run-session) binaries. Depending on
+ distribution you are using, you may need to install additional packages to
+ have these available.
+ - This module uses the C(gi.repository) Python library when available for
+ accurate comparison of values in C(dconf) to values specified in Ansible
+ code. C(gi.repository) is likely to be present on most systems which have
+ C(dconf) but may not be present everywhere. When it is missing, a simple
+ string comparison between values is used, and there may be false positives,
+ that is, Ansible may think that a value is being changed when it is not.
+ This fallback will be removed in a future version of this module, at which
+ point the module will stop working on hosts without C(gi.repository).
+ - Detection of existing, running D-Bus session, required to change settings
+ via C(dconf), is not 100% reliable due to implementation details of D-Bus
+ daemon itself. This might lead to running applications not picking-up
+ changes on the fly if options are changed via Ansible and
+ C(dbus-run-session).
+ - Keep in mind that the C(dconf) CLI tool, which this module wraps around,
+ utilises an unusual syntax for the values (GVariant). For example, if you
+ wanted to provide a string value, the correct syntax would be
+ I(value="'myvalue'") - with single quotes as part of the Ansible parameter
+ value.
+ - When using loops in combination with a value like
+ "[('xkb', 'us'), ('xkb', 'se')]", you need to be aware of possible
+ type conversions. Applying a filter C({{ item.value | string }})
+ to the parameter variable can avoid potential conversion problems.
+ - The easiest way to figure out exact syntax/value you need to provide for a
+ key is by making the configuration change in application affected by the
+ key, and then having a look at value set via commands C(dconf dump
+ /path/to/dir/) or C(dconf read /path/to/key).
+extends_documentation_fragment:
+ - community.general.attributes
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+options:
+ key:
+ type: str
+ required: true
+ description:
+ - A dconf key to modify or read from the dconf database.
+ value:
+ type: raw
+ required: false
+ description:
+ - Value to set for the specified dconf key. Value should be specified in
+ GVariant format. Due to complexity of this format, it is best to have a
+ look at existing values in the dconf database.
+ - Required for I(state=present).
+ - Although the type is specified as "raw", it should typically be
+ specified as a string. However, boolean values in particular are
+ handled properly even when specified as booleans rather than strings
+ (in fact, handling booleans properly is why the type of this parameter
+ is "raw").
+ state:
+ type: str
+ required: false
+ default: present
+ choices: [ 'read', 'present', 'absent' ]
+ description:
+ - The action to take upon the key/value.
+'''
+
+RETURN = r"""
+value:
+ description: value associated with the requested key
+ returned: success, state was "read"
+ type: str
+ sample: "'Default'"
+"""
+
+EXAMPLES = r"""
+- name: Configure available keyboard layouts in Gnome
+ community.general.dconf:
+ key: "/org/gnome/desktop/input-sources/sources"
+ value: "[('xkb', 'us'), ('xkb', 'se')]"
+ state: present
+
+- name: Read currently available keyboard layouts in Gnome
+ community.general.dconf:
+ key: "/org/gnome/desktop/input-sources/sources"
+ state: read
+ register: keyboard_layouts
+
+- name: Reset the available keyboard layouts in Gnome
+ community.general.dconf:
+ key: "/org/gnome/desktop/input-sources/sources"
+ state: absent
+
+- name: Configure available keyboard layouts in Cinnamon
+ community.general.dconf:
+ key: "/org/gnome/libgnomekbd/keyboard/layouts"
+ value: "['us', 'se']"
+ state: present
+
+- name: Read currently available keyboard layouts in Cinnamon
+ community.general.dconf:
+ key: "/org/gnome/libgnomekbd/keyboard/layouts"
+ state: read
+ register: keyboard_layouts
+
+- name: Reset the available keyboard layouts in Cinnamon
+ community.general.dconf:
+ key: "/org/gnome/libgnomekbd/keyboard/layouts"
+ state: absent
+
+- name: Disable desktop effects in Cinnamon
+ community.general.dconf:
+ key: "/org/cinnamon/desktop-effects"
+ value: "false"
+ state: present
+"""
+
+
+import os
+import sys
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.respawn import (
+ has_respawned,
+ probe_interpreters_for_module,
+ respawn_module,
+)
+from ansible.module_utils.common.text.converters import to_native
+from ansible_collections.community.general.plugins.module_utils import deps
+
+glib_module_name = 'gi.repository.GLib'
+
+try:
+ from gi.repository.GLib import Variant, GError
+except ImportError:
+ Variant = None
+ GError = AttributeError
+
+with deps.declare("psutil"):
+ import psutil
+
+
+class DBusWrapper(object):
+ """
+ Helper class that can be used for running a command with a working D-Bus
+ session.
+
+ If possible, command will be run against an existing D-Bus session,
+ otherwise the session will be spawned via dbus-run-session.
+
+ Example usage:
+
+ dbus_wrapper = DBusWrapper(ansible_module)
+ dbus_wrapper.run_command(["printenv", "DBUS_SESSION_BUS_ADDRESS"])
+ """
+
+ def __init__(self, module):
+ """
+ Initialises an instance of the class.
+
+ :param module: Ansible module instance used to signal failures and run commands.
+ :type module: AnsibleModule
+ """
+
+ # Store passed-in arguments and set-up some defaults.
+ self.module = module
+
+ # Try to extract existing D-Bus session address.
+ self.dbus_session_bus_address = self._get_existing_dbus_session()
+
+ # If no existing D-Bus session was detected, check if dbus-run-session
+ # is available.
+ if self.dbus_session_bus_address is None:
+ self.dbus_run_session_cmd = self.module.get_bin_path('dbus-run-session', required=True)
+
+ def _get_existing_dbus_session(self):
+ """
+ Detects and returns an existing D-Bus session bus address.
+
+ :returns: string -- D-Bus session bus address. If a running D-Bus session was not detected, returns None.
+ """
+
+ # We'll be checking the processes of current user only.
+ uid = os.getuid()
+
+ # Go through all the pids for this user, try to extract the D-Bus
+ # session bus address from environment, and ensure it is possible to
+ # connect to it.
+ self.module.debug("Trying to detect existing D-Bus user session for user: %d" % uid)
+
+ for pid in psutil.pids():
+ try:
+ process = psutil.Process(pid)
+ process_real_uid, dummy, dummy = process.uids()
+ if process_real_uid == uid and 'DBUS_SESSION_BUS_ADDRESS' in process.environ():
+ dbus_session_bus_address_candidate = process.environ()['DBUS_SESSION_BUS_ADDRESS']
+ self.module.debug("Found D-Bus user session candidate at address: %s" % dbus_session_bus_address_candidate)
+ dbus_send_cmd = self.module.get_bin_path('dbus-send', required=True)
+ command = [dbus_send_cmd, '--address=%s' % dbus_session_bus_address_candidate, '--type=signal', '/', 'com.example.test']
+ rc, dummy, dummy = self.module.run_command(command)
+
+ if rc == 0:
+ self.module.debug("Verified D-Bus user session candidate as usable at address: %s" % dbus_session_bus_address_candidate)
+
+ return dbus_session_bus_address_candidate
+
+ # This can happen with things like SSH sessions etc.
+ except psutil.AccessDenied:
+ pass
+ # Process has disappeared while inspecting it
+ except psutil.NoSuchProcess:
+ pass
+
+ self.module.debug("Failed to find running D-Bus user session, will use dbus-run-session")
+
+ return None
+
+ def run_command(self, command):
+ """
+ Runs the specified command within a functional D-Bus session. Command is
+ effectively passed-on to AnsibleModule.run_command() method, with
+ modification for using dbus-run-session if necessary.
+
+ :param command: Command to run, including parameters. Each element of the list should be a string.
+ :type module: list
+
+ :returns: tuple(result_code, standard_output, standard_error) -- Result code, standard output, and standard error from running the command.
+ """
+
+ if self.dbus_session_bus_address is None:
+ self.module.debug("Using dbus-run-session wrapper for running commands.")
+ command = [self.dbus_run_session_cmd] + command
+ rc, out, err = self.module.run_command(command)
+
+ if self.dbus_session_bus_address is None and rc == 127:
+ self.module.fail_json(msg="Failed to run passed-in command, dbus-run-session faced an internal error: %s" % err)
+ else:
+ extra_environment = {'DBUS_SESSION_BUS_ADDRESS': self.dbus_session_bus_address}
+ rc, out, err = self.module.run_command(command, environ_update=extra_environment)
+
+ return rc, out, err
+
+
+class DconfPreference(object):
+
+ def __init__(self, module, check_mode=False):
+ """
+ Initialises instance of the class.
+
+ :param module: Ansible module instance used to signal failures and run commands.
+ :type module: AnsibleModule
+
+ :param check_mode: Specify whether to only check if a change should be made or if to actually make a change.
+ :type check_mode: bool
+ """
+
+ self.module = module
+ self.check_mode = check_mode
+ # Check if dconf binary exists
+ self.dconf_bin = self.module.get_bin_path('dconf', required=True)
+
+ @staticmethod
+ def variants_are_equal(canonical_value, user_value):
+ """Compare two string GVariant representations for equality.
+
+ Assumes `canonical_value` is "canonical" in the sense that the type of
+ the variant is specified explicitly if it cannot be inferred; this is
+ true for textual representations of variants generated by the `dconf`
+ command. The type of `canonical_value` is used to parse `user_value`,
+ so the latter does not need to be explicitly typed.
+
+ Returns True if the two values are equal.
+ """
+ if canonical_value is None:
+ # It's unset in dconf database, so anything the user is trying to
+ # set is a change.
+ return False
+ try:
+ variant1 = Variant.parse(None, canonical_value)
+ variant2 = Variant.parse(variant1.get_type(), user_value)
+ return variant1 == variant2
+ except GError:
+ return canonical_value == user_value
+
+ def read(self, key):
+ """
+ Retrieves current value associated with the dconf key.
+
+ If an error occurs, a call will be made to AnsibleModule.fail_json.
+
+ :returns: string -- Value assigned to the provided key. If the value is not set for specified key, returns None.
+ """
+ command = [self.dconf_bin, "read", key]
+
+ rc, out, err = self.module.run_command(command)
+
+ if rc != 0:
+ self.module.fail_json(msg='dconf failed while reading the value with error: %s' % err,
+ out=out,
+ err=err)
+
+ if out == '':
+ value = None
+ else:
+ value = out.rstrip('\n')
+
+ return value
+
+ def write(self, key, value):
+ """
+ Writes the value for specified key.
+
+ If an error occurs, a call will be made to AnsibleModule.fail_json.
+
+ :param key: dconf key for which the value should be set. Should be a full path.
+ :type key: str
+
+ :param value: Value to set for the specified dconf key. Should be specified in GVariant format.
+ :type value: str
+
+ :returns: bool -- True if a change was made, False if no change was required.
+ """
+ # If no change is needed (or won't be done due to check_mode), notify
+ # caller straight away.
+ if self.variants_are_equal(self.read(key), value):
+ return False
+ elif self.check_mode:
+ return True
+
+ # Set-up command to run. Since DBus is needed for write operation, wrap
+ # dconf command dbus-launch.
+ command = [self.dconf_bin, "write", key, value]
+
+ # Run the command and fetch standard return code, stdout, and stderr.
+ dbus_wrapper = DBusWrapper(self.module)
+ rc, out, err = dbus_wrapper.run_command(command)
+
+ if rc != 0:
+ self.module.fail_json(msg='dconf failed while writing key %s, value %s with error: %s' % (key, value, err),
+ out=out,
+ err=err)
+
+ # Value was changed.
+ return True
+
+ def reset(self, key):
+ """
+ Returns value for the specified key (removes it from user configuration).
+
+ If an error occurs, a call will be made to AnsibleModule.fail_json.
+
+ :param key: dconf key to reset. Should be a full path.
+ :type key: str
+
+ :returns: bool -- True if a change was made, False if no change was required.
+ """
+
+ # Read the current value first.
+ current_value = self.read(key)
+
+ # No change was needed, key is not set at all, or just notify user if we
+ # are in check mode.
+ if current_value is None:
+ return False
+ elif self.check_mode:
+ return True
+
+ # Set-up command to run. Since DBus is needed for reset operation, wrap
+ # dconf command dbus-launch.
+ command = [self.dconf_bin, "reset", key]
+
+ # Run the command and fetch standard return code, stdout, and stderr.
+ dbus_wrapper = DBusWrapper(self.module)
+ rc, out, err = dbus_wrapper.run_command(command)
+
+ if rc != 0:
+ self.module.fail_json(msg='dconf failed while reseting the value with error: %s' % err,
+ out=out,
+ err=err)
+
+ # Value was changed.
+ return True
+
+
+def main():
+ # Setup the Ansible module
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(default='present', choices=['present', 'absent', 'read']),
+ key=dict(required=True, type='str', no_log=False),
+ # Converted to str below after special handling of bool.
+ value=dict(required=False, default=None, type='raw'),
+ ),
+ supports_check_mode=True,
+ required_if=[
+ ('state', 'present', ['value']),
+ ],
+ )
+
+ if Variant is None:
+ # This interpreter can't see the GLib module. To try to fix that, we'll
+ # look in common locations for system-owned interpreters that can see
+ # it; if we find one, we'll respawn under it. Otherwise we'll proceed
+ # with degraded performance, without the ability to parse GVariants.
+ # Later (in a different PR) we'll actually deprecate this degraded
+ # performance level and fail with an error if the library can't be
+ # found.
+
+ if has_respawned():
+ # This shouldn't be possible; short-circuit early if it happens.
+ module.fail_json(
+ msg="%s must be installed and visible from %s." %
+ (glib_module_name, sys.executable))
+
+ interpreters = ['/usr/bin/python3', '/usr/bin/python2',
+ '/usr/bin/python']
+
+ interpreter = probe_interpreters_for_module(
+ interpreters, glib_module_name)
+
+ if interpreter:
+ # Found the Python bindings; respawn this module under the
+ # interpreter where we found them.
+ respawn_module(interpreter)
+ # This is the end of the line for this process, it will exit here
+ # once the respawned module has completed.
+
+ # Try to be forgiving about the user specifying a boolean as the value, or
+ # more accurately about the fact that YAML and Ansible are quite insistent
+ # about converting strings that look like booleans into booleans. Convert
+ # the boolean into a string of the type dconf will understand. Any type for
+ # the value other than boolean is just converted into a string directly.
+ if module.params['value'] is not None:
+ if isinstance(module.params['value'], bool):
+ module.params['value'] = 'true' if module.params['value'] else 'false'
+ else:
+ module.params['value'] = to_native(
+ module.params['value'], errors='surrogate_or_strict')
+
+ if Variant is None:
+ module.warn(
+ 'WARNING: The gi.repository Python library is not available; '
+ 'using string comparison to check value equality. This fallback '
+ 'will be deprecated in a future version of community.general.')
+
+ deps.validate(module)
+
+ # Create wrapper instance.
+ dconf = DconfPreference(module, module.check_mode)
+
+ # Process based on different states.
+ if module.params['state'] == 'read':
+ value = dconf.read(module.params['key'])
+ module.exit_json(changed=False, value=value)
+ elif module.params['state'] == 'present':
+ changed = dconf.write(module.params['key'], module.params['value'])
+ module.exit_json(changed=changed)
+ elif module.params['state'] == 'absent':
+ changed = dconf.reset(module.params['key'])
+ module.exit_json(changed=changed)
+
+
+if __name__ == '__main__':
+ main()