2023-02-07 09:04:38 +01:00
|
|
|
#!/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"
|
|
|
|
|
2024-04-14 19:12:40 +02:00
|
|
|
description: Set Nextcloud configuration options via C(occ config).
|
2023-02-07 09:04:38 +01:00
|
|
|
|
|
|
|
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
|
2024-04-14 19:12:40 +02:00
|
|
|
memcache.local: "\\OC\\Memcache\\Redis"
|
|
|
|
memcache.remote: "\\OC\\Memcache\\Redis"
|
|
|
|
memcache.locking: "\\OC\\Memcache\\Redis"
|
2023-02-07 09:04:38 +01:00
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
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']
|
|
|
|
|
|
|
|
|
2023-02-07 22:52:23 +01:00
|
|
|
def iter_system(module, result, tree=None, value=None):
|
2023-02-07 09:04:38 +01:00
|
|
|
if tree is None:
|
|
|
|
tree = []
|
|
|
|
if value is None:
|
|
|
|
value = module.params['system']
|
|
|
|
# Recursively iterate the options tree
|
|
|
|
for k, v in value.items():
|
2023-02-07 21:55:36 +01:00
|
|
|
subtree = tree + [k]
|
2023-02-07 09:04:38 +01:00
|
|
|
if isinstance(v, dict):
|
2023-02-08 01:37:33 +01:00
|
|
|
iter_system(module, result, subtree, v)
|
2023-02-07 09:04:38 +01:00
|
|
|
continue
|
|
|
|
if isinstance(v, list):
|
|
|
|
v = {str(i): v for i, v in enumerate(v)}
|
2023-02-08 01:37:33 +01:00
|
|
|
iter_system(module, result, subtree, v)
|
2023-02-07 09:04:38 +01:00
|
|
|
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
|
2023-02-08 01:37:33 +01:00
|
|
|
result['changed'] = True
|
|
|
|
stjoined = ' => '.join(subtree)
|
|
|
|
if old_value is not None:
|
2023-02-08 01:43:03 +01:00
|
|
|
result['diff'][0]['before'] += 'system => {} => {}\n'.format(stjoined, old_value)
|
2023-02-08 01:37:33 +01:00
|
|
|
if v is not None:
|
2023-02-08 01:43:03 +01:00
|
|
|
result['diff'][0]['after'] += 'system => {} => {}\n'.format(stjoined, v)
|
2023-02-07 09:04:38 +01:00
|
|
|
|
|
|
|
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'),
|
2023-02-07 21:40:03 +01:00
|
|
|
system=dict(required=False, default={}, type='dict'),
|
|
|
|
apps=dict(required=False, default={}, type='dict'),
|
2023-02-07 09:04:38 +01:00
|
|
|
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,
|
2023-02-08 01:37:33 +01:00
|
|
|
diff=[dict(before='', after='')],
|
2023-02-07 09:04:38 +01:00
|
|
|
)
|
2023-02-08 01:37:33 +01:00
|
|
|
|
2023-02-07 09:04:38 +01:00
|
|
|
# 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
|
2023-02-08 01:37:33 +01:00
|
|
|
iter_system(module, result)
|
2023-02-07 09:04:38 +01:00
|
|
|
|
|
|
|
# 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:
|
2023-02-08 01:54:51 +01:00
|
|
|
old_value = rc.stdout.strip()
|
2023-02-08 01:50:02 +01:00
|
|
|
if old_value is None and new_value is None:
|
|
|
|
continue
|
2023-02-07 09:04:38 +01:00
|
|
|
if old_value == v:
|
|
|
|
continue
|
2023-02-08 01:37:33 +01:00
|
|
|
result['changed'] = True
|
|
|
|
if old_value is not None:
|
2023-02-08 01:40:16 +01:00
|
|
|
result['diff'][0]['before'] += 'apps => {} => {} => {}\n'.format(app, k, old_value)
|
2023-02-08 01:37:33 +01:00
|
|
|
if v is not None:
|
2023-02-08 01:40:16 +01:00
|
|
|
result['diff'][0]['after'] += 'apps => {} => {} => {}\n'.format(app, k, v)
|
2023-02-07 09:04:38 +01:00
|
|
|
|
|
|
|
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()
|