feat: populate a subpath from stdin using load command

This commit is contained in:
sheidan 2025-07-15 18:23:08 -04:00
commit 29b11f0fa5

View file

@ -65,11 +65,18 @@ options:
- Although the type is specified as "raw", it should typically be specified as a string. However, boolean values in - 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 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"). is why the type of this parameter is "raw").
remote_config:
type: str
required: false
description:
- Remote path to the configuration to apply.
- Required for O(state=load).
state: state:
type: str type: str
required: false required: false
default: present default: present
choices: ['read', 'present', 'absent'] choices: ['read', 'load', 'present', 'absent']
description: description:
- The action to take upon the key/value. - The action to take upon the key/value.
""" """
@ -122,12 +129,20 @@ EXAMPLES = r"""
key: "/org/cinnamon/desktop-effects" key: "/org/cinnamon/desktop-effects"
value: "false" value: "false"
state: present state: present
- name: Load terminal profile in Gnome
community.general.dconf:
key: "/org/gnome/terminal/legacy/profiles/:"
remote_config: "/tmp/solarized_dark.dump"
state: load
""" """
import os import os
import sys import sys
from configparser import ConfigParser
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.respawn import ( from ansible.module_utils.common.respawn import (
has_respawned, has_respawned,
@ -224,7 +239,7 @@ class DBusWrapper(object):
return None return None
def run_command(self, command): def run_command(self, command, data=None):
""" """
Runs the specified command within a functional D-Bus session. Command is Runs the specified command within a functional D-Bus session. Command is
effectively passed-on to AnsibleModule.run_command() method, with effectively passed-on to AnsibleModule.run_command() method, with
@ -233,19 +248,21 @@ class DBusWrapper(object):
:param command: Command to run, including parameters. Each element of the list should be a string. :param command: Command to run, including parameters. Each element of the list should be a string.
:type module: list :type module: list
:kw data: If given, information to write to the stdin of the command
:returns: tuple(result_code, standard_output, standard_error) -- Result code, standard output, and standard error from running the command. :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: if self.dbus_session_bus_address is None:
self.module.debug("Using dbus-run-session wrapper for running commands.") self.module.debug("Using dbus-run-session wrapper for running commands.")
command = [self.dbus_run_session_cmd] + command command = [self.dbus_run_session_cmd] + command
rc, out, err = self.module.run_command(command) rc, out, err = self.module.run_command(command, data=data)
if self.dbus_session_bus_address is None and rc == 127: 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) self.module.fail_json(msg="Failed to run passed-in command, dbus-run-session faced an internal error: %s" % err)
else: else:
extra_environment = {'DBUS_SESSION_BUS_ADDRESS': self.dbus_session_bus_address} extra_environment = {'DBUS_SESSION_BUS_ADDRESS': self.dbus_session_bus_address}
rc, out, err = self.module.run_command(command, environ_update=extra_environment) rc, out, err = self.module.run_command(command, data=data, environ_update=extra_environment)
return rc, out, err return rc, out, err
@ -390,19 +407,86 @@ class DconfPreference(object):
# Value was changed. # Value was changed.
return True return True
def load(self, key, remote_config):
"""
Load the config file in specified path.
if an error occurs, a call will be made to AnsibleModule.fail_json.
:param key: dconf directory for which the config should be set.
:type key: str
:param remote_config: Remote configuration path to set for the specified dconf path.
:type value: str
:returns: bool -- True if a change was made, False if no change was required.
"""
# Ensure key refers to a directory, as required by dconf
root_dir = key
if not root_dir.endswith('/'):
root_dir += '/'
# Read config to check if change is needed and passing to command line
try:
with open(remote_config, 'r') as fd:
raw_config = fd.read()
except FileNotFoundError as ex:
self.module.fail_json(msg='dconf failed while reading configuration file with error: %s' % ex)
# Parse configuratoin file
config = ConfigParser()
try:
config.read_string(raw_config)
except Exception as e:
self.module.fail_json(msg='dconf failed while reading config with error: %s' % e)
# For each sub-directory, check if at least on change is needed
for sub_dir in config.sections():
for sub_key, new_value in config[sub_dir].items():
absolute_key = '%s%s/%s' % (root_dir, sub_dir, sub_key)
if not self.variants_are_equal(self.read(absolute_key), new_value):
# if at least one change is needed, load the whole config
break
else:
# No change in the sub-directory, check the next one
continue
break
else:
# No change is needed
return False
if 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, 'load', root_dir]
# Run the command and fetch standard return code, stdout, and stderr.
dbus_wrapper = DBusWrapper(self.module)
rc, out, err = dbus_wrapper.run_command(command, data=raw_config)
if rc != 0:
self.module.fail_json(msg='dconf failed while load config %s, root dir %s with error: %s' % (remote_config, root_dir, err),
out=out,
err=err)
# Value was changed.
return True
def main(): def main():
# Setup the Ansible module # Setup the Ansible module
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
state=dict(default='present', choices=['present', 'absent', 'read']), state=dict(default='present', choices=['present', 'absent', 'read', 'load']),
key=dict(required=True, type='str', no_log=False), key=dict(required=True, type='str', no_log=False),
# Converted to str below after special handling of bool. # Converted to str below after special handling of bool.
value=dict(required=False, default=None, type='raw'), value=dict(required=False, default=None, type='raw'),
remote_config=dict(required=False, default=None, type='str'),
), ),
supports_check_mode=True, supports_check_mode=True,
required_if=[ required_if=[
('state', 'present', ['value']), ('state', 'present', ['value']),
('state', 'load', ['remote_config']),
], ],
) )
@ -467,6 +551,9 @@ def main():
elif module.params['state'] == 'absent': elif module.params['state'] == 'absent':
changed = dconf.reset(module.params['key']) changed = dconf.reset(module.params['key'])
module.exit_json(changed=changed) module.exit_json(changed=changed)
elif module.params['state'] == 'load':
changed = dconf.load(module.params['key'], module.params['remote_config'])
module.exit_json(changed=changed)
if __name__ == '__main__': if __name__ == '__main__':