commit 7859fe4fbcb250564c4e6492fb244aceda754473 Author: s3lph Date: Tue Feb 7 09:04:38 2023 +0100 initial commit, so far untested diff --git a/README.md b/README.md new file mode 100644 index 0000000..a742680 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Ansible Collection - s3lph.nextcloud + +Documentation for the collection. diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000..eafd031 --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,69 @@ +### REQUIRED +# The namespace of the collection. This can be a company/brand/organization or product namespace under which all +# content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with +# underscores or numbers and cannot contain consecutive underscores +namespace: s3lph + +# The name of the collection. Has the same character restrictions as 'namespace' +name: nextcloud + +# The version of the collection. Must be compatible with semantic versioning +version: 0.0.1 + +# The path to the Markdown (.md) readme file. This path is relative to the root of the collection +readme: README.md + +# A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) +# @nicks:irc/im.site#channel' +authors: +- s3lph + + +### OPTIONAL but strongly recommended +# A short summary description of the collection +description: Install and configure Nextcloud and PHP-FPM + +# Either a single license or a list of licenses for content inside of a collection. Ansible Galaxy currently only +# accepts L(SPDX,https://spdx.org/licenses/) licenses. This key is mutually exclusive with 'license_file' +license: +- MIT + +# A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character +# requirements as 'namespace' and 'name' +tags: + - nextcloud + - php + +# Collections that this collection requires to be installed for it to be usable. The key of the dict is the +# collection label 'namespace.name'. The value is a version range +# L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version +# range specifiers can be set and are separated by ',' +dependencies: + community.mysql: '1.2.0' + s3lph.webserver: '>=0.0.1' + +# The URL of the originating SCM repository +repository: https://git.kabelsalat.ch/s3lph/ansible-collection-nextcloud + +# The URL to any online docs +documentation: https://git.kabelsalat.ch/s3lph/ansible-collection-nextcloud + +# The URL to the homepage of the collection/project +homepage: https://git.kabelsalat.ch/s3lph/ansible-collection-nextcloud + +# The URL to the collection issue tracker +issues: https://git.kabelsalat.ch/s3lph/ansible-collection-nextcloud/issues + +# A list of file glob-like patterns used to filter any files or directories that should not be included in the build +# artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This +# uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', +# and '.git' are always filtered. Mutually exclusive with 'manifest' +build_ignore: [] + +# A dict controlling use of manifest directives used in building the collection artifact. The key 'directives' is a +# list of MANIFEST.in style +# L(directives,https://packaging.python.org/en/latest/guides/using-manifest-in/#manifest-in-commands). The key +# 'omit_default_directives' is a boolean that controls whether the default directives are used. Mutually exclusive +# with 'build_ignore' +# manifest: null + diff --git a/meta/runtime.yml b/meta/runtime.yml new file mode 100644 index 0000000..20f709e --- /dev/null +++ b/meta/runtime.yml @@ -0,0 +1,52 @@ +--- +# Collections must specify a minimum required ansible version to upload +# to galaxy +# requires_ansible: '>=2.9.10' + +# Content that Ansible needs to load from another location or that has +# been deprecated/removed +# plugin_routing: +# action: +# redirected_plugin_name: +# redirect: ns.col.new_location +# deprecated_plugin_name: +# deprecation: +# removal_version: "4.0.0" +# warning_text: | +# See the porting guide on how to update your playbook to +# use ns.col.another_plugin instead. +# removed_plugin_name: +# tombstone: +# removal_version: "2.0.0" +# warning_text: | +# See the porting guide on how to update your playbook to +# use ns.col.another_plugin instead. +# become: +# cache: +# callback: +# cliconf: +# connection: +# doc_fragments: +# filter: +# httpapi: +# inventory: +# lookup: +# module_utils: +# modules: +# netconf: +# shell: +# strategy: +# terminal: +# test: +# vars: + +# Python import statements that Ansible needs to load from another location +# import_redirection: +# ansible_collections.ns.col.plugins.module_utils.old_location: +# redirect: ansible_collections.ns.col.plugins.module_utils.new_location + +# Groups of actions/modules that take a common set of options +# action_groups: +# group_name: +# - module1 +# - module2 diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 0000000..34cd30a --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,31 @@ +# Collections Plugins Directory + +This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that +is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that +would contain module utils and modules respectively. + +Here is an example directory of the majority of plugins currently supported by Ansible: + +``` +└── plugins + ├── action + ├── become + ├── cache + ├── callback + ├── cliconf + ├── connection + ├── filter + ├── httpapi + ├── inventory + ├── lookup + ├── module_utils + ├── modules + ├── netconf + ├── shell + ├── strategy + ├── terminal + ├── test + └── vars +``` + +A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.14/plugins/plugins.html). diff --git a/plugins/modules/app.py b/plugins/modules/app.py new file mode 100644 index 0000000..1a2b08c --- /dev/null +++ b/plugins/modules/app.py @@ -0,0 +1,209 @@ +#!/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 occ config. + +options: + name: + description: Name of the app or apps to install/remove/enable/disable. + required: true + type: list + elements: str + state: + description: State the app or apps should be in. + required: false + default: enabled + type: str + choices: + - enabled + - disabled + - absent + force: + description: Install and enable apps that are not compatible with the current Nextcloud version. + 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.app + +author: + - s3lph +''' + + +EXAMPLES = r''' +- name: Install and enable photos app + s3lph.nextcloud.app: + name: photos + +- name: Install and enable PIM apps + s3lph.nextcloud.app: + name: + - contacts + - calendar + - mail + +- name: Disable resource hungry dashboard + s3lph.nextcloud.app: + name: dashboard + state: disabled +''' + + +RETURN = r''' +''' + + +from ansible.module_utils.basic import AnsibleModule + +import json +import subprocess + + +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'), + name=dict(required=True, type='list'), + state=dict(required=False, default='enabled', type='str'), + 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, + ) + + # 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, + ) + + state = module.params['state'] + if state not in ['enabled', 'disabled', 'absent']: + module.fail_json(msg='state must be one of enabled, disabled or absent', **result) + + apps = module.params['name'] + if isinstance(apps, str): + apps = [apps] + + # 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 app is not available.', **result) + + # Gather Nextcloud app list + ac = subprocess.run([module.params['php'], 'occ', 'app:list', '--output=json'], + cwd=module.params['webroot'], + capture_output=True, encoding='utf-8') + if ac.returncode != 0: + result['stdout'] = ac.stdout + result['stderr'] = ac.stderr + module.fail_json(msg='occ app:list returned non-zero exit code. Run with -vvv to view the output', **result) + + app_status = json.loads(ac.stdout) + + # Apply app configuration changes + for app in apps: + if state == 'absent' and (app in app_status['enabled'] or app in app_status['disabled']): + result['changed'] = True + if not module.check_mode: + c = subprocess.run([module.params['php'], 'occ', 'app:remove', '--keep-data', app], + cwd=module.params['webroot'], + capture_output=True, encoding='utf-8') + if c.returncode != 0: + result['stdout'] = c.stdout + result['stderr'] = c.stderr + module.fail_json(msg='occ app:remove returned non-zero exit code. Run with -vvv to view the output', **result) + elif state in ['enabled', 'disabled'] and app not in app_status['enabled'] and app not in app_status['disabled']: + result['changed'] = True + if not module.check_mode: + cmdline = [module.params['php'], 'occ', 'app:install'] + if state == 'disabled': + cmdline.append('--keep-disabled') + if module.params['force']: + cmdline.append('--force') + cmdline.append(app) + c = subprocess.run(cmdline, + cwd=module.params['webroot'], + capture_output=True, encoding='utf-8') + if c.returncode != 0: + result['stdout'] = c.stdout + result['stderr'] = c.stderr + module.fail_json(msg='occ app:install returned non-zero exit code. Run with -vvv to view the output', **result) + elif state == 'enabled' and app in app_status['disabled']: + result['changed'] = True + if not module.check_mode: + cmdline = [module.params['php'], 'occ', 'app:enable'] + if module.params['force']: + cmdline.append('--force') + cmdline.append(app) + c = subprocess.run(cmdline, + cwd=module.params['webroot'], + capture_output=True, encoding='utf-8') + if c.returncode != 0: + result['stdout'] = c.stdout + result['stderr'] = c.stderr + module.fail_json(msg='occ app:enable returned non-zero exit code. Run with -vvv to view the output', **result) + elif state == 'disabled' and app in app_status['enabled']: + result['changed'] = True + if not module.check_mode: + c = subprocess.run([module.params['php'], 'occ', 'app:disable', app], + cwd=module.params['webroot'], + capture_output=True, encoding='utf-8') + if c.returncode != 0: + result['stdout'] = c.stdout + result['stderr'] = c.stderr + module.fail_json(msg='occ app:disable returned non-zero exit code. Run with -vvv to view the output', **result) + + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/config.py b/plugins/modules/config.py new file mode 100644 index 0000000..e1c5eb9 --- /dev/null +++ b/plugins/modules/config.py @@ -0,0 +1,255 @@ +#!/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 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, tree=None, value=None): + if tree is None: + tree = [] + if value is None: + value = module.params['system'] + changed = False + # Recursively iterate the options tree + for k, v in value.items(): + sublist = tree + [k] + if isinstance(v, dict): + changed = changed or iter_system(module, subtree, v) + continue + if isinstance(v, list): + v = {str(i): v for i, v in enumerate(v)} + changed = changed or iter_system(module, 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 + changed = True + + 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) + + return changed + + +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'), + name=dict(required=True, type='list'), + state=dict(required=False, default='enabled', type='str'), + 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, + ) + + # 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 + result['changed'] = iter_system(module) + + # 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 + if old_value == v: + continue + changed = True + + 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() diff --git a/plugins/modules/install.py b/plugins/modules/install.py new file mode 100644 index 0000000..d730033 --- /dev/null +++ b/plugins/modules/install.py @@ -0,0 +1,223 @@ +#!/usr/bin/python + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: install + +short_description: Run occ maintenance:install + +# 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: Bootstrap Nextcloud with an invocation of occ maintenance:install. + +options: + database: + description: Supported database type. + required: false + default: sqlite + type: str + database_name: + description: Name of the database. + required: false + type: str + database_host: + description: Hostname of the database. + required: false + default: localhost + type: str + database_port: + description: Port the database is listening on. + required: false + type: int + database_user: + description: User name to connect to the database. + required: false + type: str + database_pass: + description: Password of the database user. + required: false + type: str + database_table_space: + description: Table space of the database (oci only). + required: false + type: str + admin_user: + description: User name of the admin account. + required: false + default: admin + type: str + admin_pass: + description: Password of the admin account. + required: true + type: str + admin_email: + description: E-Mail of the admin account. + required: false + type: str + data_dir: + description: Path to data directory. + required: false + default: /data + type: str + 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.install + +author: + - s3lph +''' + + +EXAMPLES = r''' +''' + + +RETURN = r''' +status: + type: dict + description: Parsed output from occ status. + returned: success + sample: + installed: true + version: "25.0.3.2" + versionstring: "25.0.3 + edition: "" + maintenance: false + needsDbUpgrade: false + productname: Nextcloud + extendedSupport: false +''' + + +from ansible.module_utils.basic import AnsibleModule + +import json +import os +import subprocess + + +def run_module(): + # define available arguments/parameters a user can pass to the module + module_args = dict( + database=dict(required=False, default='sqlite', type='str'), + database_name=dict(required=False, type='str'), + database_host=dict(required=False default='localhost', type='str'), + database_port=dict(required=False, type='int'), + database_user=dict(required=False, type='str'), + database_pass=dict(required=False, type='str'), + database_table_space=dict(required=False, type='str'), + admin_user=dict(required=False, default='admin', type='str'), + admin_pass=dict(required=True, type='str'), + admin_email=dict(required=False, type='str'), + data_dir=dict(required=False, default='./data', type='str'), + webroot=dict(required=False, default='/var/www/html', type='str'), + php=dict(required=False, default='/usr/bin/php', type='str'), + ) + + # 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, + ) + + # 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, + ) + + 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) + result['status'] = status + if status['installed']: + # Nextcloud installation has already been completed + module.exit_json(**result) + + # Build installation cmdline + cmdline = [module.params['php'], 'occ', 'maintenance:install', '--no-interaction', '--no-ansi'] + + datadir = os.path.normpath(os.path.join(module.params['webroot'], module.params['data_dir'])) + cmdline.append('--data-dir=' + datadir) + + cmdline.append('--database=' + module.params['database']) + if module.params['database_name'] is not None: + cmdline.append('--database-name=' + module.params['database_name']) + if module.params['database_host'] is not None: + cmdline.append('--database-host=' + module.params['database_host']) + if module.params['database_port'] is not None: + cmdline.append('--database-port=' + module.params['database_port']) + if module.params['database_user'] is not None: + cmdline.append('--database-user=' + module.params['database_user']) + if module.params['database_pass'] is not None: + cmdline.append('--database-pass=' + module.params['database_pass']) + if module.params['database_table_space'] is not None: + cmdline.append('--database-table-space=' + module.params['database_table_space']) + + cmdline.append('--admin-user=' + module.params['admin_user']) + cmdline.append('--admin-pass=' + module.params['admin_pass']) + if module.params['admin_email'] is not None: + cmdline.append('--admin-email=' + module.params['admin_email']) + + if not module.check_mode: + # Perform Nextcloud installation + ic = subprocess.run(cmdline, + cwd=module.params['webroot'], + capture_output=True, encoding='utf-8') + result['stdout'] = ic.stdout + result['stderr'] = ic.stderr + if ic.returncode != 0: + module.fail_json(msg='occ maintenance:install returned non-zero exit code. Run with -vvv to view the output', **result) + + result['changed'] = True + + # Get occ status once more + 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) + result['status'] = status + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/nextcloud_facts.py b/plugins/modules/nextcloud_facts.py new file mode 100644 index 0000000..75b7ea7 --- /dev/null +++ b/plugins/modules/nextcloud_facts.py @@ -0,0 +1,142 @@ +#!/usr/bin/python + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: nextcloud_facts + +short_description: Gather ansible_facts from Nextcloud. + +# 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: Gather ansible_facts from Nextcloud via occ status and occ config:list. + +options: + 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.nextcloud_facts + +author: + - s3lph +''' + + +EXAMPLES = r''' +''' + + +RETURN = r''' +ansible_facts.nextcloud_status: + type: dict + description: Parsed output from occ status. + returned: success + sample: + installed: true + version: "25.0.3.2" + versionstring: "25.0.3 + edition: "" + maintenance: false + needsDbUpgrade: false + productname: Nextcloud + extendedSupport: false +ansible_facts.nextcloud_config: + type: dict + description: Parsed output from occ status:list. Only present if installation was completed. + returned: success + sample: + system: + dbtype: sqlite3 + version: "25.0.3.2" + + apps: + activity: + installed_version: "2.17.0" + types: "filesystem" + enabled: "yes" +''' + + +from ansible.module_utils.basic import AnsibleModule + +import json +import subprocess + + +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'), + ) + + # 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, + ansible_facts={} + ) + + # 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) + result['ansible_facts']['nextcloud_status'] = status + + if not status['installed']: + module.exit_json(**result) + + # Gather Nextcloud configuration + cc = subprocess.run([module.params['php'], 'occ', 'config:list', '--output=json', '--private'], + cwd=module.params['webroot'], + capture_output=True, encoding='utf-8') + if cc.returncode != 0: + result['stdout'] = cc.stdout + result['stderr'] = cc.stderr + module.fail_json(msg='occ config:list returned non-zero exit code. Run with -vvv to view the output', **result) + + config = json.loads(cc.stdout) + result['ansible_facts']['nextcloud_config'] = config + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/roles/nextcloud/defaults/main.yml b/roles/nextcloud/defaults/main.yml new file mode 100644 index 0000000..4fe3100 --- /dev/null +++ b/roles/nextcloud/defaults/main.yml @@ -0,0 +1,29 @@ +--- + +nextcloud_major_version: "25" + +nextcloud_trusted_domains: + - "cloud.example.org" +nextcloud_cli_baseurl: "https://{{ nextcloud_trusted_domains[0] }}" + +nextcloud_redis_host: localhost +nextcloud_redis_port: 6379 +nextcloud_redis_dbindex: 0 + +nextcloud_db_engine: mysql +nextcloud_db_host: localhost +nextcloud_db_port: 3306 +nextcloud_db_user: nextcloud +nextcloud_db_name: nextcloud + +nextcloud_enabled_apps: [] +nextcloud_disabled_apps: [] +nextcloud_app_packages: >- + {%- set _apps = [] -%} + {%- for app in nextcloud_enabled_apps -%} + {%- set _ = _apps.apend('nextcloud-' + nextcloud_major_version + '-app-' + app.replace('_', '-')) -%} + {%- endfor -%} + {{- _apps -}} + +nextcloud_system_configuration: {} +nextcloud_apps_configuration: {} diff --git a/roles/nextcloud/tasks/config.yml b/roles/nextcloud/tasks/config.yml new file mode 100644 index 0000000..73e1b12 --- /dev/null +++ b/roles/nextcloud/tasks/config.yml @@ -0,0 +1,24 @@ +--- + +- name: Disable Nextcloud apps + become: true + become_user: www-data + s3lph.nextcloud.app: + webroot: /var/lib/nextcloud/webroot + name: "{{ nextcloud_disabled_apps }}" + state: disabled + +- name: Enable Nextcloud apps + become: true + become_user: www-data + s3lph.nextcloud.app: + webroot: /var/lib/nextcloud/webroot + name: "{{ nextcloud_enabled_apps }}" + +- name: Apply Nextcloud configuration + become: true + become_user: www-data + s3lph.nextcloud.config: + webroot: /var/lib/nextcloud/webroot + system: "{{ nextcloud_system_configuration }}" + apps: "{{ nextcloud_apps_configuration }}" diff --git a/roles/nextcloud/tasks/install.yml b/roles/nextcloud/tasks/install.yml new file mode 100644 index 0000000..1344be9 --- /dev/null +++ b/roles/nextcloud/tasks/install.yml @@ -0,0 +1,112 @@ +--- + +- name: Add repo.s3lph.me key + ansible.builtin.apt_key: + url: https://repo.s3lph.me/debian/repo.gpg + keyring: /etc/apt/trusted.gpg.d/repo.s3lph.me.gpg + +- name: Add repo.s3lph.me + ansible.builtin.apt_repository: + filename: repo.s3lph.me.list + repo: deb https://repo.s3lph.me/debian stable main + +- name: Install apache2 + ansible.builtin.apt: + name: apache2 + +- name: Install redis-server if using localhost + ansible.builtin.apt: + name: redis-server + when: "nextcloud_redis_host == 'localhost'" + +- name: Install Nextcloud package + ansible.builtin.apt: + name: "nextcloud-{{ nextcloud_major_version }}" + +- name: Remove die line from config.php + ansible.builtin.lineinfile: + path: /var/lib/nextcloud/webroot/config/config.php + regexp: "^die\s+'[^']*';" + state: absent + owner: www-data + group: www-data + mode: 0644 + +- name: Gather Nextcloud facts + become: true + become_user: www-data + s3lph.nextcloud.nextcloud_facts: + webroot: /var/lib/nextcloud/webroot + +- name: Initialize MariaDB database if Nextcloud is not installed yet + when: + - not ansible_facts.nextcloud_status.installed + - nextcloud_db_engine == 'mysql' + - nextcloud_db_host == 'localhost' + block: + + - name: Install MariaDB server + ansible.builtin.apt: + name: mariadb-server + + - name: Create MariaDB Database + community.mysql.mysql_db: + name: '{{ nextcloud_db_name }}' + login_unix_socket: /run/mysqld/mysqld.sock + check_implicit_admin: yes + + - name: Create nextcloud database user + community.mysql.mysql_user: + name: "{{ nextcloud_db_user }}" + host: "localhost" + password: "{{ nextcloud_db_pass }}" + priv: "{{ nextcloud_db_name }}.*:ALL" # grant all privileges (no grant) + login_unix_socket: /run/mysqld/mysqld.sock + check_implicit_admin: yes + +- name: Perform Nextcloud first-time setup + become: true + become_user: www-data + s3lph.nextcloud.install: + webroot: /var/lib/nextcloud/webroot + data_dir: /var/lib/nextcloud/data + database: "{{ nextcloud_db_engine }}" + database_host: "{{ nextcloud_db_host | default(omit) }}" + database_port: "{{ nextcloud_db_port | default(omit) }}" + database_user: "{{ nextcloud_db_user | default(omit) }}" + database_pass: "{{ nextcloud_db_pass | default(omit) }}" + database_name: "{{ nextcloud_db_name | default(omit) }}" + database_table_space: "{{ nextcloud_db_table_space | default(omit) }}" + admin_user: "{{ nextcloud_admin_user | default(omit) }}" + admin_pass: "{{ nextcloud_admin_pass }}" + admin_email: "{{ nextcloud_admin_email | default(omit) }}" + register: nextcloud_register_installation + +- name: Gather Nextcloud facts after completing the installation + become: true + become_user: www-data + s3lph.nextcloud.nextcloud_facts: + webroot: /var/lib/nextcloud/webroot + when: nextcloud_register_installation.changed + +- name: Set common Nextcloud options + become: true + become_user: www-data + s3lph.nextcloud.config: + webroot: /var/lib/nextcloud/webroot + system: + trusted_domains: "{{ nextcloud_trusted_domains }}" + cli_baseurl: "{{ nextcloud_cli_baseurl }}" + redis: + host: "{{ nextcloud_redis_host }}" + port: "{{ nextcloud_redis_port }}" + dbindex: "{{ nextcloud_redis_dbindex }}" + memcache.local: '\OC\Memcache\Redis' + memcache.distributed: '\OC\Memcache\Redis' + memcache.locking: '\OC\Memcache\Redis' + +- name: Install Nextcloud app packages + become: true + become_user: www-data + ansible.builtin.apt: + name: "{{ nextcloud_app_packages }}" diff --git a/roles/nextcloud/tasks/main.yml b/roles/nextcloud/tasks/main.yml new file mode 100644 index 0000000..492b58f --- /dev/null +++ b/roles/nextcloud/tasks/main.yml @@ -0,0 +1,11 @@ +--- + +- ansible.builtin.import_tasks: install.yml + tags: + - "role::nextcloud" + - "role::nextcloud:install" + +- ansible.builtin.import_tasks: config.yml + tags: + - "role::nextcloud" + - "role::nextcloud:config" diff --git a/roles/php/defaults/main.yml b/roles/php/defaults/main.yml new file mode 100644 index 0000000..358ddb0 --- /dev/null +++ b/roles/php/defaults/main.yml @@ -0,0 +1,15 @@ +--- + +php_version: "8.1" + +php_ini: + PHP: + memory_limit: "512M" + upload_max_filesize: "1G" + opcache: + opcache.enable: "1" + opcache.memory_consumption: "256" + opcache.interned_strings_buffer: "32" + opcache.max_accelerated_files: "10000" + opcache.revalidate_freq: "60" + opcache.save_comments: "1" diff --git a/roles/php/tasks/config.yml b/roles/php/tasks/config.yml new file mode 100644 index 0000000..674efd5 --- /dev/null +++ b/roles/php/tasks/config.yml @@ -0,0 +1,55 @@ +--- + +- name: Configure php-fpm + ansible.builtin.template: + src: "etc/php/conf.d/99-nextcloud.ini.j2" + dest: "/etc/php/{{ php_version }}/{{ item }}/conf.d/99-nextcloud.ini" + owner: root + group: root + mode: 0644 + loop: + - fpm + - cli + notify: + - restart php-fpm + +- name: Find enabled php-fpm apache2 config + ansible.builtin.find: + paths: ["/etc/apache2/conf-enabled/"] + patterns: ["php*-fpm.conf"] + file_type: file + register: php_register_conf_enabled + +- name: Enable wanted and disable unwanted php-fpm apache2 config + ansible.builtin.file: + path: "{{ item.path }}" + state: >- + {%- if item.path.endswith(wanted) -%} + link + {%- else -%} + absent + {%- endif -%} + src: "/etc/apache2/conf-available{{ wanted }}" + owner: root + group: root + mode: 0644 + vars: + wanted: "/php{{ php_version }}-fpm.conf" + loop: "{{ php_register_conf_enabled.files }}" + notify: + - restart apache2 + +- name: Enable apache2 modules + community.general.apache2_module: + name: "{{ item }}" + loop: + - proxy_fcgi + - setenvif + notify: + - restart apache2 + +- name: Start and enable php-fpm + ansible.builtin.service: + name: "php{{ php_version }}-fpm.service" + state: started + enabled: yes diff --git a/roles/php/tasks/install.yml b/roles/php/tasks/install.yml new file mode 100644 index 0000000..ae5b94c --- /dev/null +++ b/roles/php/tasks/install.yml @@ -0,0 +1,32 @@ +--- + +- name: Add packages.sury.org key + ansible.builtin.apt_key: + url: https://packages.sury.org/php/apt.gpg + keyring: /etc/apt/trusted.gpg.d/packages.sury.org-php.gpg + +- name: Add packages.sury.org + ansible.builtin.apt_repository: + filename: packages.sury.org-php.list + repo: "deb https://packages.sury.org/php/ {{ ansible_facts.distribution_release }} main" + +- name: Install dependencies + ansible.builtin.apt: + name: + - "{{ php }}" + - "{{ php }}-fpm" + - "{{ php }}-cli" + - "{{ php }}-bcmath" + - "{{ php }}-bz2" + - "{{ php }}-curl" + - "{{ php }}-gd" + - "{{ php }}-gmp" + - "{{ php }}-imagick" + - "{{ php }}-intl" + - "{{ php }}-mbstring" + - "{{ php }}-mysql" + - "{{ php }}-redis" + - "{{ php }}-xml" + - "{{ php }}-zip" + vars: + php: "php{{ php_version }}" diff --git a/roles/php/tasks/main.yml b/roles/php/tasks/main.yml new file mode 100644 index 0000000..d802466 --- /dev/null +++ b/roles/php/tasks/main.yml @@ -0,0 +1,11 @@ +--- + +- ansible.builtin.import_tasks: install.yml + tags: + - "role::php" + - "role::php:install" + +- ansible.builtin.import_tasks: config.yml + tags: + - "role::php" + - "role::php:config" diff --git a/roles/php/templates/etc/php/conf.d/99-nextcloud.ini.j2 b/roles/php/templates/etc/php/conf.d/99-nextcloud.ini.j2 new file mode 100644 index 0000000..266aeae --- /dev/null +++ b/roles/php/templates/etc/php/conf.d/99-nextcloud.ini.j2 @@ -0,0 +1,9 @@ +{{ ansible_managed | comment(decoration=';;') }} + +{% for name, section in php_ini.items() %} +[name] +{% endfor %}{% for key, value in section.items() %} +{{ key }}={{ value }} +{% endfor %} + +{% endfor %} \ No newline at end of file