#!/usr/bin/python from __future__ import (absolute_import, division, print_function) __metaclass__ = type DOCUMENTATION = r''' --- module: config short_description: Set Nextcloud configuration options. # If this is part of a collection, you need to use semantic versioning, # i.e. the version is of the form "2.5.0" and not "2.4". version_added: "0.0.1" description: Set Nextcloud configuration options via C(occ config). options: system: description: System configuration options to set. required: false default: {} type: dict apps: description: App configuration options to set. required: false default: {} type: dict force: description: >- Override change protection of options that should never be changed. Use with EXTREME CARE, as IRRECOVERABLE DATA LOSS may be the result of changing these options. Currently the following options are covered by this protection: instanceid, secret, passwordsalt. required: false default: false type: bool webroot: description: Path to the Nextcloud webroot. required: false default: /var/www/html type: str php: description: Path to the php-cli binary. required: false default: /usr/bin/php type: str # Specify this value according to your collection # in format of namespace.collection.doc_fragment_name #extends_documentation_fragment: # - s3lph.nextcloud.status author: - s3lph ''' EXAMPLES = r''' - name: Set up Redis cache config all redis configuration all at once s3lph.nextcloud.config: system: redis: host: localhost port: 6379 dbindex: 0 memcache.local: "\\OC\\Memcache\\Redis" memcache.remote: "\\OC\\Memcache\\Redis" memcache.locking: "\\OC\\Memcache\\Redis" ''' RETURN = r''' ''' from ansible.module_utils.basic import AnsibleModule import json import math import subprocess # Changing these keys may lead to irrecoverable data loss REQUIRE_FORCE = ['instanceid', 'secret', 'passwordsalt'] def iter_system(module, result, tree=None, value=None): if tree is None: tree = [] if value is None: value = module.params['system'] # Recursively iterate the options tree for k, v in value.items(): subtree = tree + [k] if isinstance(v, dict): iter_system(module, result, subtree, v) continue if isinstance(v, list): v = {str(i): v for i, v in enumerate(v)} iter_system(module, result, subtree, v) continue elif isinstance(v, int): typ = 'integer' elif isinstance(v, float): typ = 'double' elif isinstance(v, bool): typ = 'boolean' else: typ = 'string' # Get current value of the system option rc = subprocess.run([module.params['php'], 'occ', 'config:system:get', '--output=json'] + subtree, cwd=module.params['webroot'], capture_output=True, encoding='utf-8') if rc.returncode not in [0, 1]: result['stdout'] = rc.stdout result['stderr'] = rc.stderr module.fail_json(msg='occ config:system:get returned non-zero exit code. Run with -vvv to view the output', **result) if rc.returncode == 1: old_value = None else: old_value = json.loads(rc.stdout) if isinstance(v, float) and isinstance(old_value, float) and math.isclose(v, old_value): continue elif v == old_value: continue result['changed'] = True stjoined = ' => '.join(subtree) if old_value is not None: result['diff'][0]['before'] += 'system => {} => {}\n'.format(stjoined, old_value) if v is not None: result['diff'][0]['after'] += 'system => {} => {}\n'.format(stjoined, v) if not module.check_mode: # Remove key if the new value is none if v is None: wc = subprocess.run([module.params['php'], 'occ', 'config:system:delete'] + subtree, cwd=module.params['webroot'], capture_output=True, encoding='utf-8') if wc.returncode != 0: result['stdout'] = wc.stdout result['stderr'] = wc.stderr msg = 'occ config:system:delete returned non-zero exit code. Run with -vvv to view the output' module.fail_json(msg=msg, **result) # Set option to new value else: cmdline = [module.params['php'], 'occ', 'config:system:set', '--type', typ, '--value', str(v)] + subtree wc = subprocess.run(cmdline, cwd=module.params['webroot'], capture_output=True, encoding='utf-8') if wc.returncode != 0: result['stdout'] = wc.stdout result['stderr'] = wc.stderr msg = 'occ config:system:set returned non-zero exit code. Run with -vvv to view the output' module.fail_json(msg=msg, **result) def run_module(): # define available arguments/parameters a user can pass to the module module_args = dict( webroot=dict(required=False, default='/var/www/html', type='str'), php=dict(required=False, default='/usr/bin/php', type='str'), system=dict(required=False, default={}, type='dict'), apps=dict(required=False, default={}, type='dict'), force=dict(required=False, default=False, type='bool'), ) # seed the result dict in the object # we primarily care about changed and state # changed is if this module effectively modified the target # state will include any data that you want your module to pass back # for consumption, for example, in a subsequent task result = dict( changed=False, diff=[dict(before='', after='')], ) # the AnsibleModule object will be our abstraction working with Ansible # this includes instantiation, a couple of common attr would be the # args/params passed to the execution, as well as if the module # supports check mode module = AnsibleModule( argument_spec=module_args, supports_check_mode=True, ) # Gather Nextcloud installation status sc = subprocess.run([module.params['php'], 'occ', 'status', '--output=json'], cwd=module.params['webroot'], capture_output=True, encoding='utf-8') if sc.returncode != 0: result['stdout'] = sc.stdout result['stderr'] = sc.stderr module.fail_json(msg='occ status returned non-zero exit code. Run with -vvv to view the output', **result) status = json.loads(sc.stdout) if not status['installed']: module.fail_json(msg='Nextcloud installation has not been completed, so occ config is not available.', **result) # Check for protected options for k in REQUIRE_FORCE: if k in module.params['system'] and not module.params['force']: msg = 'Refusing to change option "' + k + '" as IRRECOVERABLE DATA LOSS may be the result of such a change.' module.fail_json(msg=msg, **result) # Apply Nextcloud system configuration recursively iter_system(module, result) # Apply Nextcloud app configuration for app, ac in module.params['apps'].items(): for k, v in ac.items(): # Get current value of the app option rc = subprocess.run([module.params['php'], 'occ', 'config:app:get', app, k], cwd=module.params['webroot'], capture_output=True, encoding='utf-8') if rc.returncode not in [0, 1]: result['stdout'] = rc.stdout result['stderr'] = rc.stderr module.fail_json(msg='occ config:app:get returned non-zero exit code. Run with -vvv to view the output', **result) if rc.returncode == 1: old_value = None else: old_value = rc.stdout.strip() if old_value is None and new_value is None: continue if old_value == v: continue result['changed'] = True if old_value is not None: result['diff'][0]['before'] += 'apps => {} => {} => {}\n'.format(app, k, old_value) if v is not None: result['diff'][0]['after'] += 'apps => {} => {} => {}\n'.format(app, k, v) if not module.check_mode: # Delete key if value is None if v is None: rc = subprocess.run([module.params['php'], 'occ', 'config:app:delete', app, k], cwd=module.params['webroot'], capture_output=True, encoding='utf-8') if rc.returncode != 0: result['stdout'] = rc.stdout result['stderr'] = rc.stderr msg = 'occ config:app:delete returned non-zero exit code. Run with -vvv to view the output' module.fail_json(msg=msg, **result) else: rc = subprocess.run([module.params['php'], 'occ', 'config:app:set', '--value', v, app, k], cwd=module.params['webroot'], capture_output=True, encoding='utf-8') if rc.returncode != 0: result['stdout'] = rc.stdout result['stderr'] = rc.stderr msg = 'occ config:app:set returned non-zero exit code. Run with -vvv to view the output' module.fail_json(msg=msg, **result) module.exit_json(**result) def main(): run_module() if __name__ == '__main__': main()