├── plugins ├── action │ ├── app.py │ ├── group.py │ ├── user.py │ ├── app_info.py │ ├── group_list.py │ ├── user_list.py │ └── run_occ.py ├── doc_fragments │ ├── __init__.py │ └── occ_common_options.py ├── module_utils │ ├── __init__.py │ ├── exceptions.py │ └── nc_tools.py └── modules │ ├── run_occ.py │ ├── group_list.py │ ├── app_info.py │ ├── user_list.py │ ├── app.py │ └── group.py ├── tests ├── sanity │ ├── ignore-2.18.txt │ ├── ignore-2.19.txt │ └── ignore-2.17.txt ├── inventory ├── nextcloud_mysql.yml ├── nextcloud_mariadb.yml ├── nextcloud_psql.yml ├── nextcloud_nginx.yml └── unit │ └── modules │ ├── test_run_occ.py │ ├── test_user.py │ ├── test_app_info.py │ └── test_group.py ├── meta └── runtime.yml ├── .ansible-lint ├── .github ├── workflows │ ├── constraints.txt │ ├── release-drafter.yml │ ├── precommit-update.yml │ ├── collection-tests.yml │ └── tests_and_release.yml ├── dependabot.yml ├── release-drafter.yml └── labels.yml ├── roles ├── install_nextcloud │ ├── templates │ │ ├── concatenate.j2 │ │ ├── root-my.cnf.j2 │ │ ├── nginx_php_handler.j2 │ │ ├── apache2_nc.j2 │ │ └── nginx_nc.j2 │ ├── files │ │ ├── SAMLController.patch │ │ ├── nextcloud_choosing_version.png │ │ ├── mysql_nextcloud.cnf │ │ ├── nextcloud_file_name.xml │ │ └── nextcloud_custom_mimetypemapping.json │ ├── defaults │ │ ├── os_config_ref.yml │ │ └── main.yml │ ├── tasks │ │ ├── redis_server.yml │ │ ├── tls_installed.yml │ │ ├── websrv_install.yml │ │ ├── db_postgresql.yml │ │ ├── tls_selfsigned.yml │ │ ├── php_install.yml │ │ ├── nc_download.yml │ │ ├── nc_apps.yml │ │ ├── setup_env.yml │ │ ├── tls_signed.yml │ │ ├── http_apache.yml │ │ ├── http_nginx.yml │ │ ├── db_mysql.yml │ │ ├── main.yml │ │ └── nc_installation.yml │ ├── meta │ │ └── main.yml │ ├── vars │ │ └── main.yml │ └── handlers │ │ └── main.yml └── backup │ ├── tasks │ ├── fetching.yml │ ├── app_data.yml │ ├── finishing.yml │ ├── user_data.yml │ ├── database.yml │ ├── main.yml │ ├── files.yml │ └── nc_facts.yml │ ├── meta │ └── main.yml │ ├── vars │ └── main.yml │ ├── defaults │ └── main.yml │ └── README.md ├── molecule └── default │ ├── roles │ └── test_install_nextcloud │ │ └── tasks │ │ ├── main.yml │ │ └── test_config_php.yml │ ├── verify.yml │ ├── molecule.yml │ └── converge.yml ├── requirements.yml ├── CHANGELOG.rst ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── .yamllint.yml ├── LICENSE.md ├── galaxy-deploy.yml ├── galaxy.yml ├── .gitignore └── README.md /plugins/action/app.py: -------------------------------------------------------------------------------- 1 | run_occ.py -------------------------------------------------------------------------------- /plugins/action/group.py: -------------------------------------------------------------------------------- 1 | run_occ.py -------------------------------------------------------------------------------- /plugins/action/user.py: -------------------------------------------------------------------------------- 1 | run_occ.py -------------------------------------------------------------------------------- /plugins/doc_fragments/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/module_utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/action/app_info.py: -------------------------------------------------------------------------------- 1 | run_occ.py -------------------------------------------------------------------------------- /plugins/action/group_list.py: -------------------------------------------------------------------------------- 1 | run_occ.py -------------------------------------------------------------------------------- /plugins/action/user_list.py: -------------------------------------------------------------------------------- 1 | run_occ.py -------------------------------------------------------------------------------- /tests/sanity/ignore-2.18.txt: -------------------------------------------------------------------------------- 1 | ignore-2.17.txt -------------------------------------------------------------------------------- /tests/sanity/ignore-2.19.txt: -------------------------------------------------------------------------------- 1 | ignore-2.17.txt -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | --- 2 | requires_ansible: '>=2.15.0' 3 | -------------------------------------------------------------------------------- /tests/inventory: -------------------------------------------------------------------------------- 1 | localhost ansible_connection=local 2 | -------------------------------------------------------------------------------- /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | skip_list: 3 | - var-naming[no-role-prefix] 4 | -------------------------------------------------------------------------------- /.github/workflows/constraints.txt: -------------------------------------------------------------------------------- 1 | pip==25.3 2 | ansible==12.2.0 3 | netaddr==1.3.0 4 | yamllint==1.37.1 5 | -------------------------------------------------------------------------------- /roles/install_nextcloud/templates/concatenate.j2: -------------------------------------------------------------------------------- 1 | {% for item in input_files %} 2 | {{ lookup('file', item) }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /roles/install_nextcloud/templates/root-my.cnf.j2: -------------------------------------------------------------------------------- 1 | {{ ansible_managed | comment }} 2 | 3 | [client] 4 | user="root" 5 | password="{{ nextcloud_mysql_root_pwd }}" 6 | -------------------------------------------------------------------------------- /roles/install_nextcloud/files/SAMLController.patch: -------------------------------------------------------------------------------- 1 | if (isset($_SERVER['REMOTE_USER'])) { 2 | $_SERVER['REMOTE_USER'] = strtolower($_SERVER['REMOTE_USER']); 3 | } 4 | -------------------------------------------------------------------------------- /molecule/default/roles/test_install_nextcloud/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Include tasks for testing the config.php file" 3 | ansible.builtin.include_tasks: "test_config_php.yml" 4 | -------------------------------------------------------------------------------- /roles/install_nextcloud/files/nextcloud_choosing_version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextcloud/ansible-collection-nextcloud-admin/HEAD/roles/install_nextcloud/files/nextcloud_choosing_version.png -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - name: ansible.utils 4 | version: 3.0.0 5 | - name: community.general 6 | version: 8.2.0 7 | roles: 8 | - name: geerlingguy.php-versions 9 | version: 6.2.0 10 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | =================================== 2 | Nextcloud admin ansible collection Release Notes 3 | =================================== 4 | 5 | Full changelog can be found here: 6 | https://github.com/nextcloud/ansible-collection-nextcloud-admin/releases 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | - package-ecosystem: pip 9 | directory: "/.github/workflows" 10 | schedule: 11 | interval: daily 12 | -------------------------------------------------------------------------------- /roles/backup/tasks/fetching.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Fetch file from remote to local 3 | become: false # Using become may cause OOM errors. (https://docs.ansible.com/ansible/latest/collections/ansible/builtin/fetch_module.html#notes) 4 | ansible.builtin.fetch: 5 | src: "{{ nc_archive_path }}.{{ nextcloud_backup_format }}" 6 | dest: "{{ nextcloud_backup_fetch_local_path }}" 7 | -------------------------------------------------------------------------------- /roles/install_nextcloud/defaults/os_config_ref.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # facts specific for each distributions and versions 3 | # defaults keys are global for each family/distribution 4 | debian: 5 | defaults: 6 | mysql_daemon: mariadb 7 | # thoses 2 are globals for debian family: 8 | nextcloud_websrv_user: "www-data" 9 | nextcloud_websrv_group: "www-data" 10 | 11 | ubuntu: 12 | defaults: 13 | mysql_daemon: mysql 14 | -------------------------------------------------------------------------------- /roles/install_nextcloud/templates/nginx_php_handler.j2: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This file was generated by Ansible for {{ansible_fqdn}} 3 | # Do NOT modify this file by hand! 4 | ################################################################################ 5 | 6 | upstream php-handler { 7 | # server 127.0.0.1:9000; 8 | server unix:{{ php_socket }}; 9 | } 10 | -------------------------------------------------------------------------------- /roles/install_nextcloud/files/mysql_nextcloud.cnf: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This file was generated by Ansible 3 | # Do NOT modify this file by hand! 4 | ################################################################################ 5 | 6 | # Nextcloud mysql.cnf 7 | 8 | [mysqld] 9 | binlog_format = MIXED 10 | innodb_large_prefix=on 11 | innodb_file_format=barracuda 12 | innodb_file_per_table=true -------------------------------------------------------------------------------- /tests/sanity/ignore-2.17.txt: -------------------------------------------------------------------------------- 1 | plugins/modules/run_occ.py validate-modules:missing-gplv3-license 2 | plugins/modules/app_info.py validate-modules:missing-gplv3-license 3 | plugins/modules/app.py validate-modules:missing-gplv3-license 4 | plugins/modules/user_list.py validate-modules:missing-gplv3-license 5 | plugins/modules/user.py validate-modules:missing-gplv3-license 6 | plugins/modules/group_list.py validate-modules:missing-gplv3-license 7 | plugins/modules/group.py validate-modules:missing-gplv3-license -------------------------------------------------------------------------------- /molecule/default/verify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Verify 3 | hosts: all 4 | gather_facts: true 5 | vars: 6 | # load configurations references 7 | os_config_ref: "{{ lookup('ansible.builtin.template', '../../roles/install_nextcloud/defaults/os_config_ref.yml') | from_yaml }}" 8 | vars_files: 9 | - ../../roles/install_nextcloud/defaults/main.yml 10 | 11 | tasks: 12 | - name: "Include test_install_nextcloud" 13 | ansible.builtin.import_role: 14 | name: "test_install_nextcloud" 15 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/adrienverge/yamllint.git 4 | rev: v1.37.1 5 | hooks: 6 | - id: yamllint 7 | args: ["-c=.yamllint.yml", "."] 8 | - repo: https://github.com/ansible-community/ansible-lint.git 9 | rev: v25.6.1 10 | hooks: 11 | - id: ansible-lint 12 | files: \.(yaml|yml)$ 13 | - repo: https://github.com/psf/black 14 | rev: 25.1.0 15 | hooks: 16 | - id: black 17 | language_version: python3.13 18 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/redis_server.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: redis_server | Install redis server packages 3 | ansible.builtin.package: 4 | name: "{{ redis_deps }}" 5 | state: present 6 | vars: 7 | redis_deps: 8 | - redis-server 9 | - "php{{ php_ver }}-redis" 10 | notify: Start redis 11 | 12 | - name: redis_server | Configure redis server 13 | ansible.builtin.template: 14 | dest: /etc/redis/redis.conf 15 | src: templates/redis.conf.j2 16 | mode: 0640 17 | notify: Restart redis 18 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/tls_installed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: tls_installed | Define certificate path 3 | ansible.builtin.set_fact: 4 | nextcloud_tls_cert_file: "{{ nextcloud_tls_cert }}" 5 | 6 | - name: tls_installed | Define key path 7 | ansible.builtin.set_fact: 8 | nextcloud_tls_cert_key_file: "{{ nextcloud_tls_cert_key }}" 9 | 10 | - name: tls_installed | Define certificate chain path 11 | ansible.builtin.set_fact: 12 | nextcloud_tls_chain_file: "{{ nextcloud_tls_cert_chain }}" 13 | when: nextcloud_tls_cert_chain is defined 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to this ansible role 2 | ### Install 'pre-commit' dependency 3 | 4 | To enable automatic checks (like code linting) beforey each commit it is advised to install ***pre-commit***: 5 | 6 | Install it to your environment: 7 | 8 | `~/ansible_nextcloud$ pip install pre-commit` (for more options see [here](https://pre-commit.com/#installation)) 9 | 10 | Enable pre-commit within the repo: 11 | 12 | `~/ansible_nextcloud$ pre-commit install` 13 | 14 | (Optional) Run complete check once: 15 | 16 | `~/ansible_nextcloud$ pre-commit run --all-files` -------------------------------------------------------------------------------- /molecule/default/roles/test_install_nextcloud/tasks/test_config_php.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Get the config.php content" 3 | become: true 4 | nextcloud.admin.run_occ: 5 | nextcloud_path: "{{ nextcloud_webroot }}" 6 | command: config:list 7 | register: _config_php 8 | changed_when: _config_php.rc != 0 9 | 10 | - name: "Check values inside config.php" 11 | ansible.builtin.assert: 12 | that: 13 | - _config_php.stdout is regex('\"mysql\.utf8mb4\"[:] true,') 14 | success_msg: "All regular expressions/searches passed." 15 | fail_msg: "At least one check for patterns failed." 16 | -------------------------------------------------------------------------------- /roles/backup/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: aalaesar 4 | role_name: backup 5 | description: Create a backup of your nextcloud server with this ansible role 6 | 7 | license: BSD 8 | 9 | min_ansible_version: "2.14" 10 | 11 | platforms: 12 | - name: EL 13 | versions: 14 | - all 15 | - name: Debian 16 | versions: 17 | - all 18 | - name: Ubuntu 19 | versions: 20 | - all 21 | 22 | galaxy_tags: 23 | - backup 24 | - nextcloud 25 | - storage 26 | - selfhosted 27 | - privacy 28 | 29 | dependencies: [] 30 | -------------------------------------------------------------------------------- /roles/backup/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | nc_status: "{{ _nc_status.stdout | from_json }}" 3 | nc_id: "{{ _nc_id.stdout }}" 4 | nc_user_list: "{{ (_nc_user_list.stdout | from_json).keys() }}" 5 | nc_archive_name: >- 6 | {{ nextcloud_instance_name }}_nextcloud-{{ nc_status.versionstring }}_{{ ansible_date_time.iso8601_basic_short }}{{ nextcloud_backup_suffix }} 7 | 8 | # ansible parameters to switch to the www-data user with or without sudo 9 | occ_become_method: "{{ (__sudo_installed.rc == 0) | ternary('sudo', 'su') }}" 10 | occ_become_flags: "{{ (__sudo_installed.rc == 0) | ternary(omit, '-s /bin/sh') }}" 11 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Draft 3 | 4 | "on": 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | labeler: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out the repository 14 | uses: actions/checkout@v6 15 | 16 | - name: Run Labeler 17 | uses: crazy-max/ghaction-github-labeler@v5 18 | with: 19 | skip-delete: true 20 | 21 | draft_release: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: release-drafter/release-drafter@v6 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | role_name_check: 1 3 | dependency: 4 | name: galaxy 5 | driver: 6 | name: docker 7 | platforms: 8 | - name: instance 9 | image: "docker.io/geerlingguy/docker-${MOLECULE_DISTRO:-debian12}-ansible:latest" 10 | cgroupns_mode: host 11 | command: ${MOLECULE_COMMAND:-""} 12 | volumes: 13 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 14 | privileged: true 15 | pre_build_image: true 16 | network_mode: host 17 | provisioner: 18 | name: ansible 19 | playbooks: 20 | converge: "converge.yml" 21 | env: 22 | MOLECULE_NC: "${MOLECULE_NC:-latest}" 23 | verifier: 24 | name: ansible 25 | -------------------------------------------------------------------------------- /roles/install_nextcloud/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: aalaesar 4 | role_name: install_nextcloud 5 | description: Install Nextcloud server like you want ! Apache2 or Nginx ? MariaDB or PostgresQL ? You choose, you watch, it works ! 6 | 7 | license: BSD 8 | 9 | min_ansible_version: "2.14" 10 | 11 | platforms: 12 | - name: Ubuntu 13 | versions: 14 | - jammy 15 | - noble 16 | 17 | - name: Debian 18 | versions: 19 | - bookworm 20 | - bullseye 21 | galaxy_tags: 22 | - nextcloud 23 | - filesharing 24 | - installation 25 | - private 26 | - cloud 27 | 28 | dependencies: [] 29 | -------------------------------------------------------------------------------- /roles/backup/tasks/app_data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: The app_data backup folder exists 3 | ansible.builtin.file: 4 | path: "{{ nc_archive_path }}/data" 5 | state: directory 6 | owner: "{{ nextcloud_backup_owner }}" 7 | group: "{{ nextcloud_backup_group }}" 8 | mode: "{{ nextcloud_backup_dir_mode }}" 9 | 10 | - name: Backup applications data 11 | ansible.builtin.command: >- 12 | rsync -r {{ _exclude_folders }} 13 | {{ nextcloud_data_dir }}/appdata_{{ nc_id }} 14 | {{ nc_archive_path }}/data 15 | vars: 16 | _exclude_folders: "{% for _folder in nextcloud_backup_app_data_exclude_folder %}--exclude={{ _folder }} {% endfor %}" 17 | tags: 18 | - skip_ansible_lint 19 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/websrv_install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: websrv_install | Install web server packages 3 | ansible.builtin.package: 4 | name: "{{ item }}" 5 | state: present 6 | with_items: 7 | - "{{ nextcloud_websrv }}" 8 | 9 | - name: websrv_install | Install specific Apache web server packages 10 | ansible.builtin.package: 11 | name: "libapache2-mod-php{{ php_ver }}" 12 | state: present 13 | when: nextcloud_websrv in ["apache2"] 14 | notify: 15 | - Start http 16 | - Reload php-fpm 17 | 18 | - name: websrv_install | Install specific NGINX web server packages 19 | ansible.builtin.package: 20 | name: "php{{ php_ver }}-fpm" 21 | state: present 22 | when: nextcloud_websrv in ["nginx"] 23 | notify: 24 | - Start http 25 | - Reload php-fpm 26 | -------------------------------------------------------------------------------- /tests/nextcloud_mysql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test MySQL 3 | hosts: all 4 | become: true 5 | 6 | pre_tasks: 7 | - name: Update apt cache. 8 | ansible.builtin.apt: 9 | update_cache: true 10 | cache_valid_time: 600 11 | when: ansible_os_family == 'Debian' 12 | changed_when: false 13 | 14 | - name: Install required testing packages 15 | ansible.builtin.package: 16 | name: "{{ item }}" 17 | state: present 18 | with_items: 19 | - curl 20 | 21 | roles: 22 | - nextcloud.admin.install_nextcloud 23 | 24 | vars: 25 | nextcloud_db_backend: "mysql" 26 | nextcloud_apps: 27 | files_external: "" # enable files_external which is already installed in nextcloud 28 | calendar: "" # download and install calendar app 29 | nextcloud_disable_apps: 30 | - photos 31 | -------------------------------------------------------------------------------- /tests/nextcloud_mariadb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test MariaDB 3 | hosts: all 4 | become: true 5 | 6 | pre_tasks: 7 | - name: Update apt cache. 8 | ansible.builtin.apt: 9 | update_cache: true 10 | cache_valid_time: 600 11 | when: ansible_os_family == 'Debian' 12 | changed_when: false 13 | 14 | - name: Install required testing packages 15 | ansible.builtin.package: 16 | name: "{{ item }}" 17 | state: present 18 | with_items: 19 | - curl 20 | 21 | roles: 22 | - nextcloud.admin.install_nextcloud 23 | 24 | vars: 25 | nextcloud_db_backend: "mariadb" 26 | nextcloud_apps: 27 | files_external: "" # enable files_external which is already installed in nextcloud 28 | calendar: "" # download and install calendar app 29 | nextcloud_disable_apps: 30 | - photos 31 | -------------------------------------------------------------------------------- /tests/nextcloud_psql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test Postgresql 3 | hosts: all 4 | become: true 5 | 6 | pre_tasks: 7 | - name: Update apt cache. 8 | ansible.builtin.apt: 9 | update_cache: true 10 | cache_valid_time: 600 11 | when: ansible_os_family == 'Debian' 12 | changed_when: false 13 | 14 | - name: Install required testing packages 15 | ansible.builtin.package: 16 | name: "{{ item }}" 17 | state: present 18 | with_items: 19 | - curl 20 | 21 | roles: 22 | - nextcloud.admin.install_nextcloud 23 | 24 | vars: 25 | nextcloud_db_backend: "pgsql" 26 | nextcloud_apps: 27 | files_external: "" # enable files_external which is already installed in nextcloud 28 | calendar: "" # download and install calendar app 29 | nextcloud_disable_apps: 30 | - photos 31 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/db_postgresql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: db_postgresql | Install PostgreSQL packages 3 | ansible.builtin.package: 4 | name: "{{ pg_deps }}" 5 | state: "present" 6 | vars: 7 | pg_deps: 8 | - "postgresql" 9 | - "python3-psycopg2" 10 | 11 | - name: db_postgresql | Create PostgreSQL Nextcloud role 12 | community.postgresql.postgresql_user: 13 | name: "{{ nextcloud_db_admin }}" 14 | password: "{{ nextcloud_db_pwd }}" 15 | encrypted: true 16 | state: present 17 | role_attr_flags: CREATEDB 18 | become_user: postgres 19 | become: true 20 | 21 | - name: db_postgresql | Create PostgreSQL Nextcloud database 22 | community.postgresql.postgresql_db: 23 | name: "{{ nextcloud_db_name }}" 24 | state: present 25 | owner: "{{ nextcloud_db_admin }}" 26 | become_user: postgres 27 | become: true 28 | -------------------------------------------------------------------------------- /tests/nextcloud_nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Test Nginx 3 | hosts: all 4 | become: true 5 | 6 | pre_tasks: 7 | - name: Update apt cache. 8 | ansible.builtin.apt: 9 | update_cache: true 10 | cache_valid_time: 600 11 | when: ansible_os_family == 'Debian' 12 | changed_when: false 13 | 14 | - name: Install required testing packages 15 | ansible.builtin.package: 16 | name: "{{ item }}" 17 | state: present 18 | with_items: 19 | - curl 20 | 21 | roles: 22 | - nextcloud.admin.install_nextcloud 23 | 24 | vars: 25 | nextcloud_db_backend: "mariadb" 26 | nextcloud_websrv: "nginx" 27 | nextcloud_apps: 28 | files_external: "" # enable files_external which is already installed in nextcloud 29 | calendar: "" # download and install calendar app 30 | nextcloud_disable_apps: 31 | - photos 32 | -------------------------------------------------------------------------------- /roles/install_nextcloud/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # vars file for nextcloud 3 | nextcloud_dl_file_name: 4 | latest: "{{ ['latest', nextcloud_version_major] | reject('undefined') | join('-') }}" 5 | releases: "{{ ['nextcloud', nextcloud_version_full] | reject('undefined') | join('-') }}" 6 | prereleases: "nextcloud-{{ [nextcloud_version_full, nextcloud_version_special] | reject('undefined') | join() }}" 7 | daily: "nextcloud-{{ nextcloud_version_major | d('') }}-daily-{{ nextcloud_version_special | d('') }}" 8 | 9 | mysql_credential_file: 10 | debian: '/etc/mysql/debian.cnf' 11 | 12 | nextcloud_max_upload_size_in_bytes: "{{ nextcloud_max_upload_size | human_to_bytes }}" 13 | 14 | # load configurations references 15 | os_config_ref: "{{ lookup('ansible.builtin.template', [role_path, 'defaults', 'os_config_ref.yml'] | join('/')) | from_yaml }}" 16 | php_config_ref: "{{ lookup('ansible.builtin.template', [role_path, 'defaults', 'php_config_ref.yml'] | join('/')) | from_yaml }}" 17 | -------------------------------------------------------------------------------- /roles/backup/tasks/finishing.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create the archive 3 | community.general.archive: 4 | path: 5 | - "{{ nc_archive_path }}/*" 6 | dest: "{{ nc_archive_path }}.{{ nextcloud_backup_format }}" 7 | owner: "{{ nextcloud_backup_owner }}" 8 | group: "{{ nextcloud_backup_group }}" 9 | mode: "{{ nextcloud_backup_file_mode }}" 10 | format: "{{ nextcloud_backup_format | regex_replace('tgz', 'gz') }}" 11 | tags: 12 | - always 13 | 14 | - name: Cleanup the archive folder 15 | ansible.builtin.file: 16 | path: "{{ nc_archive_path }}" 17 | state: absent 18 | 19 | - name: Leave maintenance mode 20 | nextcloud.admin.run_occ: 21 | nextcloud_path: "{{ nextcloud_webroot }}" 22 | command: maintenance:mode --off" 23 | become: true 24 | register: __leave_maintenance 25 | changed_when: 26 | - __leave_maintenance.stdout | regex_search('already') == none 27 | when: nextcloud_exit_maintenance_mode 28 | tags: 29 | - always 30 | -------------------------------------------------------------------------------- /roles/backup/tasks/user_data.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: The data backup folder exists 3 | ansible.builtin.file: 4 | path: "{{ nc_archive_path }}/data" 5 | state: directory 6 | owner: "{{ nextcloud_backup_owner }}" 7 | group: "{{ nextcloud_backup_group }}" 8 | mode: "{{ nextcloud_backup_dir_mode }}" 9 | 10 | - name: Backup user data 11 | ansible.builtin.command: >- 12 | rsync -r {{ _exclude_folders }} 13 | {{ nextcloud_data_dir }}/{{ item }} 14 | {{ nc_archive_path }}/data 15 | loop: "{{ nc_user_list| difference(nextcloud_backup_exclude_users) }}" 16 | vars: 17 | _exclude_folders: >- 18 | {{ '' if nextcloud_backup_user_files_trashbin else '--exclude=files_trashbin' }} 19 | {{ '' if nextcloud_backup_user_files_versions else '--exclude=files_versions' }} 20 | {{ '' if nextcloud_backup_user_uploads else '--exclude=uploads' }} 21 | {{ '' if nextcloud_backup_user_cache else '--exclude=cache' }} 22 | tags: 23 | - skip_ansible_lint 24 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | template: | 3 | ## Changes 4 | 5 | $CHANGES 6 | 7 | categories: 8 | - title: ":boom: Breaking Changes" 9 | label: "breaking" 10 | - title: ":rocket: Features" 11 | label: "enhancement" 12 | - title: ":fire: Removals and Deprecations" 13 | label: "removal" 14 | - title: ":beetle: Fixes" 15 | label: "bug" 16 | - title: ":raising_hand: Help wanted" 17 | label: "help wanted" 18 | - title: ":racehorse: Performance" 19 | label: "performance" 20 | - title: ":rotating_light: Testing" 21 | label: "testing" 22 | - title: ":construction_worker: Continuous Integration" 23 | label: "ci" 24 | - title: ":books: Documentation" 25 | label: "documentation" 26 | - title: ":hammer: Refactoring" 27 | label: "refactoring" 28 | - title: ":lipstick: Style" 29 | label: "style" 30 | - title: ":package: Dependencies" 31 | labels: 32 | - "dependencies" 33 | - "build" 34 | 35 | exclude-labels: 36 | - "skip-changelog" 37 | -------------------------------------------------------------------------------- /roles/install_nextcloud/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # handlers file for nextcloud 3 | - name: Restart mysql 4 | ansible.builtin.service: 5 | name: "{{ mysql_daemon }}" 6 | state: restarted 7 | 8 | - name: Start http 9 | ansible.builtin.service: 10 | name: "{{ nextcloud_websrv }}" 11 | state: started 12 | 13 | - name: Restart http 14 | ansible.builtin.service: 15 | name: "{{ nextcloud_websrv }}" 16 | state: restarted 17 | 18 | - name: Reload http 19 | ansible.builtin.service: 20 | name: "{{ nextcloud_websrv }}" 21 | state: reloaded 22 | 23 | - name: Start php-fpm 24 | ansible.builtin.service: 25 | name: php{{ php_ver }}-fpm 26 | state: started 27 | 28 | - name: Reload php-fpm 29 | ansible.builtin.service: 30 | name: php{{ php_ver }}-fpm 31 | state: reloaded 32 | 33 | - name: Start redis 34 | ansible.builtin.service: 35 | name: redis-server 36 | state: started 37 | 38 | - name: Restart redis 39 | ansible.builtin.service: 40 | name: redis-server 41 | state: restarted 42 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ## Ansible Galaxy yamllint rules: https://github.com/ansible/galaxy/blob/devel/galaxy/importer/linters/yamllint.yaml 3 | extends: default 4 | ignore: | 5 | .ansible 6 | collections 7 | rules: 8 | braces: {max-spaces-inside: 1, level: error} 9 | brackets: {max-spaces-inside: 1, level: error} 10 | colons: {max-spaces-after: -1, level: error} 11 | commas: {max-spaces-after: -1, level: error} 12 | comments: disable 13 | comments-indentation: disable 14 | # document-start: disable 15 | empty-lines: {max: 3, level: error} 16 | hyphens: {level: error} 17 | # indentation: disable 18 | key-duplicates: enable 19 | # line-length: disable 20 | # new-line-at-end-of-file: disable 21 | new-lines: {type: unix} 22 | # trailing-spaces: disable 23 | # truthy: disable 24 | 25 | ## Additional rules with warnings 26 | document-start: {level: warning} 27 | indentation: {level: warning, spaces: 2, indent-sequences: consistent} 28 | line-length: {level: warning, max: 180} 29 | trailing-spaces: {level: warning} 30 | truthy: {level: warning, allowed-values: ['true', 'false']} 31 | -------------------------------------------------------------------------------- /.github/workflows/precommit-update.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pre-commit auto-update 3 | 4 | "on": 5 | # run once a week 6 | schedule: 7 | - cron: "0 0 * * 0" 8 | 9 | jobs: 10 | pre-commit-auto-update: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | 15 | - uses: actions/setup-python@v6 16 | 17 | - name: Update pre-commit hooks 18 | uses: browniebroke/pre-commit-autoupdate-action@main 19 | 20 | - name: Create pull request with update 21 | uses: peter-evans/create-pull-request@v8 22 | with: 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | branch: update/pre-commit-hooks 25 | title: Update pre-commit hooks 26 | commit-message: "chore: update pre-commit hooks" 27 | committer: "dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>" 28 | author: "dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>" 29 | signoff: true 30 | labels: | 31 | dependencies 32 | pre-commit 33 | body: Update versions of pre-commit hooks to latest version. 34 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/tls_selfsigned.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: tls_selfsigned | Define private certificate path 3 | ansible.builtin.set_fact: 4 | nextcloud_tls_cert_file: "/etc/ssl/{{ nextcloud_instance_name }}.crt" 5 | 6 | - name: tls_selfsigned | Define private key path 7 | ansible.builtin.set_fact: 8 | nextcloud_tls_cert_key_file: "/etc/ssl/{{ nextcloud_instance_name }}.key" 9 | 10 | - name: tls_selfsigned | Create self-signed SSL cert 11 | ansible.builtin.command: > 12 | openssl req -new -nodes -x509 13 | -subj "/C=US/ST=Oregon/L=Portland/O=IT/CN=${hostname --fqdn}" 14 | -days 365 15 | -keyout {{ nextcloud_tls_cert_key_file }} 16 | -out {{ nextcloud_tls_cert_file }} 17 | -extensions v3_ca 18 | args: 19 | creates: "{{ nextcloud_tls_cert_key_file }}" 20 | 21 | - name: tls_selfsigned | Check TLS certificate permissions 22 | ansible.builtin.file: 23 | path: "{{ nextcloud_tls_cert_file }}" 24 | mode: 0644 25 | group: "{{ nextcloud_websrv_group }}" 26 | 27 | - name: tls_selfsigned | Check TLS key permissions 28 | ansible.builtin.file: 29 | path: "{{ nextcloud_tls_cert_key_file }}" 30 | mode: 0640 31 | group: "{{ nextcloud_websrv_group }}" 32 | -------------------------------------------------------------------------------- /roles/backup/tasks/database.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: The app_data backup folder exists 3 | ansible.builtin.file: 4 | path: "{{ nc_archive_path }}" 5 | state: directory 6 | owner: "{{ nextcloud_backup_owner }}" 7 | group: "{{ nextcloud_backup_group }}" 8 | mode: "{{ nextcloud_backup_dir_mode }}" 9 | 10 | - name: Create a dump of the mysql database 11 | ansible.builtin.shell: >- 12 | mysqldump --single-transaction --default-character-set=utf8mb4 13 | -h {{ _nc_dbhost.stdout }} -u {{ _nc_dbuser.stdout }} -p{{ _nc_dbpassword.stdout }} 14 | {{ _nc_dbname.stdout }} > database_dump.bak.sql 15 | args: 16 | chdir: "{{ nc_archive_path }}" 17 | no_log: true 18 | when: 19 | - _nc_dbtype.stdout == 'mysql' 20 | register: output 21 | changed_when: output.rc != 0 22 | 23 | - name: Create a dump of the postgreSQL database 24 | ansible.builtin.command: >- 25 | pg_dump {{ _nc_dbname.stdout }} 26 | -h {{ _nc_dbhost.stdout }} 27 | -U {{ _nc_dbuser.stdout }} 28 | -f database_dump.bak.sql 29 | args: 30 | chdir: "{{ nc_archive_path }}" 31 | no_log: true 32 | environment: 33 | PGPASSWORD: "{{ _nc_dbpassword.stdout }}" 34 | when: 35 | - _nc_dbtype.stdout == 'pgsql' 36 | register: output 37 | changed_when: output.rc != 0 38 | -------------------------------------------------------------------------------- /roles/backup/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ### APPLICATION SETTINGS ## 3 | nextcloud_instance_name: "nextcloud" 4 | nextcloud_backup_target_dir: "/opt/nextcloud_backups" 5 | nextcloud_webroot: "/opt/nextcloud" 6 | # nextcloud_data_dir: "/var/ncdata" 7 | nextcloud_websrv_user: www-data 8 | 9 | nextcloud_exit_maintenance_mode: true 10 | 11 | ### ARCHIVE PROPERTIES ### 12 | nextcloud_backup_suffix: "" 13 | nextcloud_backup_owner: "{{ nextcloud_websrv_user }}" 14 | nextcloud_backup_group: "{{ nextcloud_websrv_user }}" 15 | nextcloud_backup_file_mode: "0640" 16 | nextcloud_backup_dir_mode: "0750" 17 | nextcloud_backup_format: "tgz" 18 | 19 | ### NEXTCLOUD SERVER ARCHIVE DOWNLOAD ### 20 | nextcloud_backup_download_server_archive: false 21 | 22 | ### APPS BACKUPS ### 23 | nextcloud_backup_app_data: true 24 | nextcloud_backup_app_data_exclude_folder: 25 | - preview 26 | 27 | ### USER BACKUPS ### 28 | nextcloud_backup_user: true 29 | nextcloud_backup_exclude_users: [] 30 | nextcloud_backup_user_files_trashbin: true 31 | nextcloud_backup_user_files_versions: true 32 | nextcloud_backup_user_uploads: true 33 | nextcloud_backup_user_cache: true 34 | 35 | ### DATABASE BACKUP ### 36 | nextcloud_backup_database: true 37 | 38 | ### FETCH TO LOCAL MACHINE ### 39 | nextcloud_backup_fetch_to_local: false 40 | nextcloud_backup_fetch_local_path: "/tmp/nextcloud_backup" 41 | -------------------------------------------------------------------------------- /roles/backup/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Gather some nextcloud configuration facts 3 | ansible.builtin.import_tasks: nc_facts.yml 4 | tags: 5 | - always 6 | - facts 7 | - name: Set archive name 8 | ansible.builtin.set_fact: 9 | nc_archive_path: "{{ nextcloud_backup_target_dir }}/{{ nc_archive_name }}" 10 | tags: 11 | - always 12 | - name: Enter maintenance mode 13 | nextcloud.admin.run_occ: 14 | nextcloud_path: "{{ nextcloud_webroot }}" 15 | command: maintenance:mode --on" 16 | become: true 17 | register: __goto_maintenance 18 | changed_when: 19 | - __goto_maintenance.stdout | regex_search('already') == none 20 | tags: 21 | - always 22 | 23 | - name: Run basic backup 24 | ansible.legacy.import_tasks: files.yml 25 | - name: Run users data backup 26 | ansible.legacy.import_tasks: user_data.yml 27 | when: nextcloud_backup_user 28 | - name: Run applications backups 29 | ansible.legacy.import_tasks: app_data.yml 30 | when: nextcloud_backup_user 31 | - name: Run database backup 32 | ansible.legacy.import_tasks: database.yml 33 | when: nextcloud_backup_database 34 | tags: 35 | - db_dump 36 | - name: Finish the backup 37 | ansible.legacy.import_tasks: finishing.yml 38 | - name: Fetch backup to local 39 | ansible.legacy.import_tasks: fetching.yml 40 | when: nextcloud_backup_fetch_to_local 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2016, Aalaesar 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge 3 | hosts: all 4 | become: true 5 | pre_tasks: 6 | - name: Set parameter for a specific version of Nextcloud. 7 | ansible.builtin.set_fact: 8 | nextcloud_version_major: "{{ molecule_nc[2:] }}" 9 | when: 10 | - molecule_nc not in [ "", "latest" ] 11 | - molecule_nc is match("nc[0-9]{2,3}") 12 | vars: 13 | molecule_nc: "{{ lookup('env', 'MOLECULE_NC') }}" 14 | - name: Update apt cache. 15 | ansible.builtin.apt: 16 | update_cache: true 17 | cache_valid_time: 600 18 | when: ansible_os_family == 'Debian' 19 | changed_when: false 20 | - name: Install gpg-agent 21 | ansible.builtin.package: 22 | name: gpg-agent 23 | state: present 24 | when: ansible_distribution == 'Ubuntu' 25 | vars: 26 | nextcloud_install_db: false 27 | nextcloud_db_name: nc 28 | nextcloud_db_admin: nc 29 | nextcloud_db_pwd: nc 30 | nextcloud_version_channel: "releases" 31 | nextcloud_get_latest: true 32 | nextcloud_admin_pwd: "The_Answer_Is_42!" 33 | roles: 34 | - role: nextcloud.admin.install_nextcloud 35 | post_tasks: 36 | - name: Get server Status. 37 | nextcloud.admin.run_occ: 38 | command: status 39 | nextcloud_path: "{{ nextcloud_webroot }}" 40 | changed_when: false 41 | register: _server_status 42 | - name: Display server Status. 43 | ansible.builtin.debug: 44 | var: _server_status 45 | -------------------------------------------------------------------------------- /plugins/action/run_occ.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2024, Marc Crébassa 5 | 6 | import copy 7 | import os 8 | from ansible.plugins.action import ActionBase 9 | 10 | 11 | class ActionModule(ActionBase): 12 | def run(self, tmp=None, task_vars=None): 13 | del tmp # tmp no longer has any effect 14 | new_module_args = copy.deepcopy(self._task.args) 15 | 16 | # missing occ common arguments fallback 17 | # argument value precedence: args > task_vars > environment 18 | nextcloud_path = ( 19 | self._task.args.get("nextcloud_path") 20 | or task_vars.get("nextcloud_path") 21 | or os.getenv("NEXTCLOUD_PATH") 22 | ) 23 | if nextcloud_path: 24 | new_module_args["nextcloud_path"] = nextcloud_path 25 | elif "nextcloud_path" in new_module_args: 26 | del new_module_args["nextcloud_path"] 27 | 28 | # argument value precedence: args > task_vars > environment > default 29 | php_runtime = ( 30 | self._task.args.get("php_runtime") 31 | or task_vars.get("nextcloud_php_runtime") 32 | or os.getenv("NEXTCLOUD_PHP_RUNTIME") 33 | ) 34 | if php_runtime: 35 | new_module_args["php_runtime"] = php_runtime 36 | elif "php_runtime" in new_module_args: 37 | del new_module_args["php_runtime"] 38 | 39 | return self._execute_module( 40 | module_name=self._task.action, 41 | module_args=new_module_args, 42 | task_vars=task_vars, 43 | ) 44 | -------------------------------------------------------------------------------- /roles/backup/tasks/files.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Backup target dir exists on the host 3 | ansible.builtin.file: 4 | path: "{{ nextcloud_backup_target_dir }}" 5 | state: directory 6 | owner: "{{ nextcloud_backup_owner }}" 7 | group: "{{ nextcloud_backup_group }}" 8 | mode: "{{ nextcloud_backup_dir_mode }}" 9 | 10 | - name: Backup folder exists on the host 11 | ansible.builtin.file: 12 | path: "{{ nc_archive_path }}" 13 | state: directory 14 | owner: "{{ nextcloud_backup_owner }}" 15 | group: "{{ nextcloud_backup_group }}" 16 | mode: "{{ nextcloud_backup_dir_mode }}" 17 | 18 | - name: Copy config in archive 19 | ansible.builtin.shell: "rsync -r --exclude='*.sample.*' {{nextcloud_webroot}}/config/ {{ nc_archive_path }}/config/" 20 | tags: 21 | - skip_ansible_lint 22 | 23 | - name: Download server files for current version 24 | vars: 25 | nc_server_dl_url: "https://download.nextcloud.com/server/releases/nextcloud-{{ nc_status.versionstring }}.zip" 26 | ansible.builtin.get_url: 27 | url: "{{ nc_server_dl_url }}" 28 | checksum: "sha512:{{ nc_server_dl_url }}.sha512" 29 | dest: "{{ nc_archive_path }}/nextcloud-{{ nc_status.versionstring }}.zip" 30 | owner: "{{ nextcloud_backup_owner }}" 31 | group: "{{ nextcloud_backup_group }}" 32 | mode: "{{ nextcloud_backup_file_mode }}" 33 | when: nextcloud_backup_download_server_archive 34 | 35 | - name: Export the list of apps in the backup 36 | ansible.builtin.copy: 37 | content: "{{ _nc_app_list.stdout | from_json | to_nice_json }}" 38 | dest: "{{ nc_archive_path }}/installed_apps.json" 39 | owner: "{{ nextcloud_backup_owner }}" 40 | group: "{{ nextcloud_backup_group }}" 41 | mode: "{{ nextcloud_backup_file_mode }}" 42 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/php_install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: php_install | Add external repository for PHP by using geerlingguy role 3 | ansible.builtin.import_role: 4 | name: geerlingguy.php-versions 5 | vars: 6 | php_version: "{{ php_ver }}" 7 | when: php_install_external_repos 8 | 9 | - name: php_install | Install required PHP packages for Nextcloud 10 | ansible.builtin.package: 11 | name: 12 | - imagemagick 13 | - libmagickcore-*-extra 14 | - smbclient 15 | - "php{{ php_ver }}-fpm" 16 | - "php{{ php_ver }}-gd" 17 | - "php{{ php_ver }}-ldap" 18 | - "php{{ php_ver }}-imap" 19 | - "php{{ php_ver }}-curl" 20 | - "php{{ php_ver }}-intl" 21 | - "php{{ php_ver }}-bz2" 22 | state: present 23 | 24 | - name: "[php-fpm] - setup fpm php.ini" 25 | ansible.builtin.lineinfile: 26 | dest: "{{ php_dir }}/fpm/php.ini" 27 | state: present 28 | regexp: "{{ item.regexp }}" 29 | line: "{{ item.line }}" 30 | backrefs: true 31 | with_items: 32 | - {regexp: 'opcache.interned_strings_buffer', line: 'opcache.interned_strings_buffer=16'} 33 | - {regexp: 'memory_limit', line: 'memory_limit={{ php_memory_limit }}'} 34 | notify: 35 | - Start php-fpm 36 | - Reload http 37 | 38 | - name: php_install | Install extra packages 39 | ansible.builtin.package: 40 | name: "{{ php_pkg_spe }}" 41 | state: present 42 | 43 | - name: php_install | Install php*-mysql package 44 | ansible.builtin.package: 45 | name: "php{{ php_ver }}-mysql" 46 | state: present 47 | when: (nextcloud_db_backend == "mysql") or (nextcloud_db_backend == "mariadb") 48 | 49 | - name: php_install | Install php*-pgsql package 50 | ansible.builtin.package: 51 | name: "php{{ php_ver }}-pgsql" 52 | state: present 53 | when: (nextcloud_db_backend == "pgsql") 54 | 55 | - name: php_install | Install PHP APCu pacakge 56 | ansible.builtin.package: 57 | name: "php{{ php_ver }}-apcu" 58 | state: present 59 | -------------------------------------------------------------------------------- /galaxy-deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Automated release playbook for Ansible Collections. 3 | # 4 | # Originally based on Ericsysmin's 2020 blog post. Meant to be used in a GitHub 5 | # Actions CI environment. 6 | # 7 | # Requires a ANSIBLE_GALAXY_TOKEN secret to be configured on the GitHub repo. 8 | # 9 | # Usage: 10 | # ansible-playbook -i 'localhost,' galaxy-deploy.yml \ 11 | # -e "github_tag=${{ github.ref }}" 12 | 13 | - name: Galaxy deployment playbook 14 | hosts: localhost 15 | connection: local 16 | gather_facts: false 17 | 18 | vars: 19 | # Requires github_tag to be set when calling playbook. 20 | release_tag: "{{ github_tag.split('/')[-1] }}" 21 | 22 | pre_tasks: 23 | - name: Ensure ANSIBLE_GALAXY_TOKEN is set. 24 | ansible.builtin.fail: 25 | msg: A valid ANSIBLE_GALAXY_TOKEN must be set. 26 | when: "lookup('env', 'ANSIBLE_GALAXY_TOKEN') | length == 0" 27 | 28 | - name: Ensure the ~/.ansible directory exists. 29 | ansible.builtin.file: 30 | path: ~/.ansible 31 | state: directory 32 | mode: 0755 33 | 34 | - name: Write the Galaxy token to ~/.ansible/galaxy_token 35 | ansible.builtin.copy: 36 | content: | 37 | token: {{ lookup('env', 'ANSIBLE_GALAXY_TOKEN') }} 38 | dest: ~/.ansible/galaxy_token 39 | mode: 0544 40 | no_log: true 41 | 42 | tasks: 43 | - name: Ensure the galaxy.yml tag is up to date. 44 | ansible.builtin.lineinfile: 45 | path: galaxy.yml 46 | regexp: "^version:" 47 | line: 'version: "{{ release_tag }}"' 48 | 49 | - name: Build the collection. 50 | ansible.builtin.command: ansible-galaxy collection build 51 | register: output 52 | changed_when: "output.rc == 0" 53 | 54 | - name: Publish the collection. 55 | ansible.builtin.command: > 56 | ansible-galaxy collection publish ./{{ ansible_namespace }}-{{ collection }}-{{ release_tag }}.tar.gz 57 | register: output 58 | changed_when: "output.rc == 0" 59 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/nc_download.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: nc_download | Install unzip package 3 | ansible.builtin.package: 4 | name: unzip 5 | state: present 6 | when: nextcloud_archive_format == "zip" 7 | 8 | - name: nc_download | Install bunzip2 package 9 | ansible.builtin.package: 10 | name: bzip2 11 | state: present 12 | when: nextcloud_archive_format == "tar.bz2" 13 | 14 | - name: nc_download | You must specify the major version 15 | ansible.builtin.assert: 16 | that: nextcloud_version_major is defined 17 | when: nextcloud_full_src is defined 18 | 19 | - name: nc_download | Create and set directory ownership & permissions for the webroot folder 20 | ansible.builtin.file: 21 | path: "{{ nextcloud_webroot }}" 22 | mode: "u=rwX,g=rX,o-rwx" 23 | recurse: true 24 | state: directory 25 | owner: "{{ nextcloud_websrv_user }}" 26 | group: "{{ nextcloud_websrv_group }}" 27 | 28 | - name: nc_download | Download and extract Nextcloud 29 | block: 30 | - name: nc_download | Download & extract Nextcloud to /tmp." 31 | ansible.builtin.unarchive: 32 | copy: "{{ (nextcloud_full_src is not url) if (nextcloud_full_src is defined) else false }}" 33 | src: "{{ nextcloud_full_src | default(nextcloud_calculated_url) }}" 34 | dest: "/tmp/" 35 | vars: 36 | nextcloud_calculated_url: "{{ nextcloud_repository }}/{{ nextcloud_version_channel }}/{{ nextcloud_calculated_file }}" 37 | nextcloud_calculated_file: "{{ [nextcloud_dl_file_name[just_a_dict_key], nextcloud_archive_format] | join('.') }}" 38 | just_a_dict_key: "{{ 'latest' if ((nextcloud_get_latest | bool) and (nextcloud_version_channel != 'prereleases')) else nextcloud_version_channel }}" 39 | 40 | - name: "nc_download | Move extracted files to {{ nextcloud_webroot }}" 41 | ansible.builtin.command: "cp -r /tmp/nextcloud/. {{ nextcloud_webroot }}/" 42 | when: nextcloud_webroot is not none 43 | register: output 44 | changed_when: "output.rc == 0" 45 | 46 | - name: nc_download | Remove nextcloud archive files 47 | ansible.builtin.file: 48 | path: /tmp/nextcloud 49 | state: absent 50 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Labels names are important as they are used by Release Drafter to decide 3 | # regarding where to record them in changelog or if to skip them. 4 | # 5 | # The repository labels will be automatically configured using this file and 6 | # the GitHub Action https://github.com/marketplace/actions/github-labeler. 7 | - name: breaking 8 | description: Breaking Changes 9 | color: "bfd4f2" 10 | - name: bug 11 | description: Something isn't working 12 | color: "d73a4a" 13 | - name: build 14 | description: Build System and Dependencies 15 | color: "bfdadc" 16 | - name: ci 17 | description: Continuous Integration 18 | color: "4a97d6" 19 | - name: dependencies 20 | description: Pull requests that update a dependency file 21 | color: "0366d6" 22 | - name: documentation 23 | description: Improvements or additions to documentation 24 | color: "0075ca" 25 | - name: duplicate 26 | description: This issue or pull request already exists 27 | color: "cfd3d7" 28 | - name: enhancement 29 | description: New feature or request 30 | color: "a2eeef" 31 | - name: github_actions 32 | description: Pull requests that update Github_actions code 33 | color: "000000" 34 | - name: good first issue 35 | description: Good for newcomers 36 | color: "7057ff" 37 | - name: help wanted 38 | description: Extra attention is needed 39 | color: "008672" 40 | - name: invalid 41 | description: This doesn't seem right 42 | color: "e4e669" 43 | - name: performance 44 | description: Performance 45 | color: "016175" 46 | - name: python 47 | description: Pull requests that update Python code 48 | color: "2b67c6" 49 | - name: question 50 | description: Further information is requested 51 | color: "d876e3" 52 | - name: refactoring 53 | description: Refactoring 54 | color: "ef67c4" 55 | - name: removal 56 | description: Removals and deprecations 57 | color: "9ae7ea" 58 | - name: style 59 | description: Style 60 | color: "c120e5" 61 | - name: testing 62 | description: Testing 63 | color: "b1fc6f" 64 | - name: wontfix 65 | description: This will not be worked on 66 | color: "ffffff" 67 | - name: skip-changelog 68 | description: This will not be added to release notes 69 | color: "dddddd" 70 | - name: pre-commit 71 | description: Pull requests that update pre-commit code 72 | color: "424242" 73 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/nc_apps.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: nc_apps | Parse the item values 3 | ansible.builtin.set_fact: 4 | nc_app_name: "{{ item.key }}" 5 | nc_app_source: "{{ item.value.source if item.value.source is defined else item.value }}" 6 | nc_app_cfg: "{{ item.value }}" 7 | 8 | - name: "nc_apps | Install from the official app-store app: \"{{ nc_app_name }}\"" 9 | become_user: "{{ nextcloud_websrv_user }}" 10 | become_flags: "{{ ansible_become_flags | default(omit) }}" 11 | become: true 12 | nextcloud.admin.app: 13 | name: "{{ nc_app_name }}" 14 | state: present 15 | nextcloud_path: "{{ nextcloud_webroot }}" 16 | when: 17 | - (nc_app_source is string) and (nc_app_source | length == 0) 18 | - nc_app_name not in nc_available_apps.disabled 19 | 20 | - name: "nc_apps | Download or copy Archive to apps folder from \"{{ nc_app_source }}\"" 21 | become_user: "{{ nextcloud_websrv_user }}" 22 | become_flags: "{{ ansible_become_flags | default(omit) }}" 23 | become: true 24 | ansible.builtin.unarchive: 25 | copy: "{{ nc_app_source is not url }}" 26 | src: "{{ nc_app_source }}" 27 | dest: "{{ nextcloud_webroot }}/apps/" 28 | owner: "{{ nextcloud_websrv_user }}" 29 | group: "{{ nextcloud_websrv_group }}" 30 | creates: "{{ nextcloud_webroot }}/apps/{{ nc_app_name }}" 31 | when: 32 | - (nc_app_source is string) and (nc_app_source | length > 0) 33 | - nc_app_name not in nc_available_apps.disabled 34 | 35 | - name: "nc_apps | Enable the application \"{{ nc_app_name }}\"" 36 | become_user: "{{ nextcloud_websrv_user }}" 37 | become_flags: "{{ ansible_become_flags | default(omit) }}" 38 | become: true 39 | nextcloud.admin.app: 40 | name: "{{ nc_app_name }}" 41 | state: present 42 | nextcloud_path: "{{ nextcloud_webroot }}" 43 | changed_when: true 44 | 45 | - name: "nc_apps | Configure the application \"{{ nc_app_name }}\"" 46 | become_user: "{{ nextcloud_websrv_user }}" 47 | become_flags: "{{ ansible_become_flags | default(omit) }}" 48 | become: true 49 | nextcloud.admin.run_occ: 50 | command: config:app:set {{ nc_app_name }} {{ item_cfg.key }} --value="{{ item_cfg.value }}" 51 | nextcloud_path: "{{ nextcloud_webroot }}" 52 | with_dict: "{{ nc_app_cfg.conf | default({}) }}" 53 | loop_control: 54 | loop_var: item_cfg 55 | when: nc_app_cfg.conf is defined 56 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/setup_env.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # additional setup and fixes for OS dependent environment 3 | - name: setup_env | Controls nextcloud_trusted_domain type 4 | ansible.builtin.fail: 5 | msg: "New versions require nextcloud_trusted_domain to be declared as a list." 6 | when: nextcloud_trusted_domain is string 7 | 8 | - name: setup_env | Update ca-certificate 9 | # needed for downloading from download.nextcloud.com as the site use letsencrypt certificates 10 | # letsencrypt may not be trusted on older OS 11 | ansible.builtin.apt: 12 | name: "{{ item }}" 13 | state: present 14 | update_cache: true 15 | cache_valid_time: 86400 16 | loop: 17 | - acl 18 | - ca-certificates 19 | when: ansible_os_family in [ "Debian" ] 20 | 21 | - name: setup_env | Upgrade all packages to latest version 22 | ansible.builtin.apt: 23 | upgrade: "yes" 24 | force_apt_get: true 25 | when: 26 | - ansible_os_family in [ "Debian" ] 27 | - upgrade_packages_first 28 | 29 | # fix for debian not using sudo : 30 | # finding out if sudo is installed or not 31 | - name: setup_env | Check if sudo is installed (only on Debian) 32 | ansible.builtin.command: "dpkg -l sudo" 33 | changed_when: false 34 | register: nc_sudo_installed_result 35 | failed_when: false 36 | when: ansible_distribution == "Debian" 37 | 38 | - name: setup_env | Deterime become_method or becom_flags 39 | when: 40 | - nc_sudo_installed_result.rc is defined 41 | - nc_sudo_installed_result.rc != 0 42 | block: 43 | - name: setup_env | Rolling back to su 44 | ansible.builtin.set_fact: 45 | ansible_become_method: "su" 46 | - name: setup_env | Force su to use /bin/sh as shell 47 | ansible.builtin.set_fact: 48 | ansible_become_flags: '-s /bin/sh' 49 | 50 | - name: setup_env | Generate database user password 51 | ansible.builtin.set_fact: 52 | nextcloud_db_pwd: "{{ lookup('ansible.builtin.password', 'nextcloud_instances/' + nextcloud_instance_name + '/db_admin.pwd') }}" 53 | when: nextcloud_db_pwd is not defined 54 | 55 | - name: setup_env | Generate database root password 56 | ansible.builtin.set_fact: 57 | nextcloud_mysql_root_pwd: "{{ lookup('ansible.builtin.password', 'nextcloud_instances/' + nextcloud_instance_name + '/db_root.pwd') }}" 58 | when: 59 | - nextcloud_db_backend in ["mysql", "mariadb"] 60 | - nextcloud_mysql_root_pwd is not defined 61 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/tls_signed.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: tls_signed | Define signed certificate-file with abolute path 3 | ansible.builtin.set_fact: 4 | nextcloud_tls_cert_file: "{{ nextcloud_tls_cert_file | default(cert_path + nextcloud_instance_name + \".crt\") }}" 5 | 6 | - name: tls_signed | Define signed certificate's key-file with absolute path 7 | ansible.builtin.set_fact: 8 | nextcloud_tls_cert_key_file: "{{ nextcloud_tls_cert_key_file | default(cert_path + nextcloud_instance_name + \".key\") }}" 9 | 10 | - name: tls_signed | Define certificate chain-file with absolute path 11 | ansible.builtin.set_fact: 12 | nextcloud_tls_chain_file: "{{ nextcloud_tls_chain_file | default(cert_path + nextcloud_instance_name + \".pem\") }}" 13 | when: nextcloud_tls_src_chain is defined 14 | 15 | - name: tls_signed | Copy certificate file for apache2 to the host 16 | ansible.builtin.copy: 17 | dest: "{{ nextcloud_tls_cert_file }}" 18 | src: "{{ nextcloud_tls_src_cert }}" 19 | owner: "{{ nextcloud_websrv_user }}" 20 | group: "{{ nextcloud_websrv_group }}" 21 | mode: "0640" 22 | force: true 23 | when: 24 | - nextcloud_websrv not in ["nginx"] 25 | 26 | - name: tls_signed | Copy and concatenate chained certificate file for nginx to host 27 | ansible.builtin.template: 28 | src: templates/concatenate.j2 29 | dest: "{{ nextcloud_tls_cert_file }}" 30 | owner: "{{ nextcloud_websrv_user }}" 31 | group: "{{ nextcloud_websrv_group }}" 32 | mode: "0640" 33 | vars: 34 | input_files: ["{{ nextcloud_tls_src_cert }}", "{{ nextcloud_tls_src_chain }}"] 35 | when: 36 | - nextcloud_tls_src_chain is defined 37 | - nextcloud_websrv in ["nginx"] 38 | 39 | - name: tls_signed | Copy certificate chain file for apache2 to the host 40 | ansible.builtin.copy: 41 | dest: "{{ nextcloud_tls_chain_file }}" 42 | src: "{{ nextcloud_tls_src_chain }}" 43 | owner: "{{ nextcloud_websrv_user }}" 44 | group: "{{ nextcloud_websrv_group }}" 45 | mode: 0400 46 | force: false 47 | when: 48 | - nextcloud_tls_src_chain is defined 49 | - nextcloud_websrv not in ["nginx"] 50 | 51 | - name: tls_signed | Copy key to the host 52 | ansible.builtin.copy: 53 | dest: "{{ nextcloud_tls_cert_key_file }}" 54 | src: "{{ nextcloud_tls_src_cert_key }}" 55 | owner: "{{ nextcloud_websrv_user }}" 56 | group: "{{ nextcloud_websrv_group }}" 57 | mode: 0400 58 | force: false 59 | -------------------------------------------------------------------------------- /plugins/doc_fragments/occ_common_options.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2022, Marc Crébassa 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | 27 | class ModuleDocFragment(object): 28 | # Standard documentation 29 | DOCUMENTATION = r""" 30 | options: 31 | nextcloud_path: 32 | description: 33 | - Specify the nextcloud instance's location in the host. 34 | - If not specified, the module will attempt to use the `nextcloud_path` var if it exists. 35 | - In last resort, read the `NEXTCLOUD_PATH` environment variable on the ansible host. 36 | type: str 37 | aliases: 38 | - path 39 | - nc_path 40 | - nc_dir 41 | required: true 42 | 43 | php_runtime: 44 | description: 45 | - Specify the php runtime used to run the occ tool. 46 | - Can be an absolute or relative path if the runtime is available in the remote host PATH. 47 | - If not specified, the module will attempt to use the `nextcloud_php_runtime` var if it exists, 48 | then the `NEXTCLOUD_PHP_RUNTIME` environment variable on the ansible host. 49 | type: str 50 | default: php 51 | aliases: 52 | - php 53 | """ 54 | -------------------------------------------------------------------------------- /tests/unit/modules/test_run_occ.py: -------------------------------------------------------------------------------- 1 | from ansible.module_utils import basic 2 | from ansible_collections.nextcloud.admin.plugins.modules import run_occ 3 | import unittest 4 | from unittest.mock import patch, MagicMock 5 | 6 | 7 | class TestRunOccModule(unittest.TestCase): 8 | 9 | @patch("ansible_collections.nextcloud.admin.plugins.modules.run_occ.run_occ") 10 | def test_successful_command_execution(self, mock_run_occ): 11 | # Mock the return value of run_occ 12 | mock_run_occ.return_value = (0, "Success", "") 13 | 14 | # Create a mock AnsibleModule object 15 | mock_module = MagicMock(spec=basic.AnsibleModule) 16 | mock_module.params = {"command": "status --output=json"} 17 | 18 | with patch( 19 | "ansible_collections.nextcloud.admin.plugins.modules.run_occ.AnsibleModule", 20 | return_value=mock_module, 21 | ): 22 | run_occ.main() 23 | 24 | # Check that exit_json was called with expected values 25 | mock_module.exit_json.assert_called_once_with( 26 | changed=True, 27 | command="status --output=json", 28 | rc=0, 29 | stdout="Success", 30 | stderr="", 31 | ) 32 | 33 | @patch("ansible_collections.nextcloud.admin.plugins.modules.run_occ.run_occ") 34 | def test_command_execution_failure(self, mock_run_occ): 35 | # Mock the return value of run_occ to simulate an error 36 | mock_run_occ.side_effect = run_occ.OccExceptions( 37 | msg="Error occurred", 38 | rc=1, 39 | stdout="", 40 | stderr="Error", 41 | occ_cmd="invalid-command", 42 | ) 43 | 44 | # Create a mock AnsibleModule object 45 | mock_module = MagicMock(spec=basic.AnsibleModule) 46 | mock_module.params = {"command": "invalid-command"} 47 | 48 | with patch( 49 | "ansible_collections.nextcloud.admin.plugins.modules.run_occ.AnsibleModule", 50 | return_value=mock_module, 51 | ): 52 | run_occ.main() 53 | 54 | # Check that fail_json was called with expected values 55 | mock_module.fail_json.assert_called_once_with( 56 | msg="Error occurred", 57 | exception_class="OccExceptions", 58 | command="invalid-command", 59 | occ_cmd="invalid-command", 60 | rc=1, 61 | stdout="", 62 | stderr="Error", 63 | ) 64 | 65 | 66 | if __name__ == "__main__": 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ### REQUIRED 3 | # The namespace of the collection. This can be a company/brand/organization or product namespace under which all 4 | # content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with 5 | # underscores or numbers and cannot contain consecutive underscores 6 | namespace: nextcloud 7 | 8 | # The name of the collection. Has the same character restrictions as 'namespace' 9 | name: admin 10 | 11 | # The version of the collection. Must be compatible with semantic versioning 12 | version: 2.3.0 13 | 14 | # The path to the Markdown (.md) readme file. This path is relative to the root of the collection 15 | readme: README.md 16 | 17 | # A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) 18 | # @nicks:irc/im.site#channel' 19 | authors: 20 | - Marc Crebassa 21 | 22 | ### OPTIONAL but strongly recommended 23 | # A short summary description of the collection 24 | description: Official Nextcloud Ansible collection of roles and modules to deploy and manage Nextcloud instances 25 | 26 | # The path to the license file for the collection. This path is relative to the root of the collection. This key is 27 | # mutually exclusive with 'license' 28 | license_file: 'LICENSE.md' 29 | 30 | # A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character 31 | # requirements as 'namespace' and 'name' 32 | tags: 33 | - cloud 34 | - self_hosted 35 | - nextcloud 36 | - administration 37 | - file_sharing 38 | 39 | # Collections that this collection requires to be installed for it to be usable. The key of the dict is the 40 | # collection label 'namespace.name'. The value is a version range 41 | # L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version 42 | # range specifiers can be set and are separated by ',' 43 | dependencies: {} 44 | 45 | # The URL of the originating SCM repository 46 | repository: https://github.com/nextcloud/ansible-collection-nextcloud-admin 47 | 48 | # The URL to the collection issue tracker 49 | issues: https://github.com/nextcloud/ansible-collection-nextcloud-admin/issues 50 | 51 | # A list of file glob-like patterns used to filter any files or directories that should not be included in the build 52 | # artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This 53 | # uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', 54 | # and '.git' are always filtered 55 | build_ignore: [] 56 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/http_apache.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: http_apache | Enable APC for php CLI 3 | ansible.builtin.lineinfile: 4 | dest: "{{ php_dir }}/cli/php.ini" 5 | line: "apc.enable_cli = 1" 6 | insertbefore: "^; End:$" 7 | state: present 8 | # validate: "/usr/sbin/{{ php_bin }} -t #%s" 9 | 10 | - name: http_apache | Enable PHP OPcache for php.ini 11 | ansible.builtin.lineinfile: 12 | dest: "{{ php_dir }}/apache2/php.ini" 13 | state: present 14 | regexp: "{{ item.regexp }}" 15 | line: "{{ item.line }}" 16 | backrefs: true 17 | with_items: 18 | - {regexp: 'opcache.enable=0', line: 'opcache.enable=1'} 19 | - {regexp: 'opcache.enable_cli', line: 'opcache.enable_cli=1'} 20 | - {regexp: 'opcache.interned_strings_buffer', line: 'opcache.interned_strings_buffer=16'} 21 | - {regexp: 'opcache.max_accelerated_files', line: 'opcache.max_accelerated_files=10000'} 22 | - {regexp: 'opcache.memory_consumption', line: 'opcache.memory_consumption=128'} 23 | - {regexp: 'opcache.save_comments', line: 'opcache.save_comments=1'} 24 | - {regexp: 'opcache.revalidate_freq', line: 'opcache.revalidate_freq=1'} 25 | - {regexp: 'memory_limit', line: 'memory_limit={{ php_memory_limit }}'} 26 | # validate: "/usr/sbin/{{ php_bin }} -t #%s" 27 | notify: Reload http 28 | 29 | - name: http_apache | Enable required Apache2 modules 30 | community.general.apache2_module: 31 | name: "{{ item }}" 32 | state: present 33 | with_items: 34 | - rewrite 35 | - headers 36 | - env 37 | - dir 38 | - mime 39 | notify: Restart http 40 | 41 | - name: http_apache | Enable ssl Apache2 module 42 | community.general.apache2_module: 43 | state: present 44 | name: "{{ item }}" 45 | with_items: 46 | - ssl 47 | when: (nextcloud_install_tls | bool) 48 | notify: Restart http 49 | 50 | - name: http_apache | Generate Nextcloud configuration for apache 51 | ansible.builtin.template: 52 | dest: /etc/apache2/sites-available/nc_{{ nextcloud_instance_name }}.conf 53 | src: "{{ nextcloud_websrv_template }}" 54 | mode: 0640 55 | notify: Reload http 56 | 57 | - name: http_apache | Enable Nextcloud site in apache conf 58 | ansible.builtin.file: 59 | path: /etc/apache2/sites-enabled/nc_{{ nextcloud_instance_name }}.conf 60 | src: /etc/apache2/sites-available/nc_{{ nextcloud_instance_name }}.conf 61 | state: link 62 | notify: Reload http 63 | 64 | - name: http_apache | Disable apache default site 65 | ansible.builtin.file: 66 | path: /etc/apache2/sites-enabled/000-default.conf 67 | state: absent 68 | when: nextcloud_disable_websrv_default_site | bool 69 | notify: Reload http 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Vim template 3 | # swap 4 | [._]*.s[a-w][a-z] 5 | [._]s[a-w][a-z] 6 | # session 7 | Session.vim 8 | # temporary 9 | .netrwhist 10 | *~ 11 | # auto-generated tag files 12 | tags 13 | ### Linux template 14 | *~ 15 | 16 | # temporary files which can be created if a process still has a handle open of a deleted file 17 | .fuse_hidden* 18 | 19 | # KDE directory preferences 20 | .directory 21 | 22 | # Linux trash folder which might appear on any partition or disk 23 | .Trash-* 24 | ### JetBrains template 25 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 26 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 27 | 28 | .idea/ 29 | 30 | # User-specific stuff: 31 | .idea/workspace.xml 32 | .idea/tasks.xml 33 | .idea/dictionaries 34 | .idea/vcs.xml 35 | .idea/jsLibraryMappings.xml 36 | 37 | # Sensitive or high-churn files: 38 | .idea/dataSources.ids 39 | .idea/dataSources.xml 40 | .idea/dataSources.local.xml 41 | .idea/sqlDataSources.xml 42 | .idea/dynamic.xml 43 | .idea/uiDesigner.xml 44 | 45 | # Gradle: 46 | .idea/gradle.xml 47 | .idea/libraries 48 | 49 | # Mongo Explorer plugin: 50 | .idea/mongoSettings.xml 51 | 52 | ## File-based project format: 53 | *.iws 54 | 55 | ## Plugin-specific files: 56 | 57 | # IntelliJ 58 | /out/ 59 | 60 | # mpeltonen/sbt-idea plugin 61 | .idea_modules/ 62 | 63 | # JIRA plugin 64 | atlassian-ide-plugin.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | ### SublimeText template 72 | # cache files for sublime text 73 | *.tmlanguage.cache 74 | *.tmPreferences.cache 75 | *.stTheme.cache 76 | 77 | # workspace files are user-specific 78 | *.sublime-workspace 79 | 80 | # project files should be checked into the repository, unless a significant 81 | # proportion of contributors will probably not be using SublimeText 82 | # *.sublime-project 83 | 84 | # sftp configuration file 85 | sftp-config.json 86 | 87 | # Package control specific files 88 | Package Control.last-run 89 | Package Control.ca-list 90 | Package Control.ca-bundle 91 | Package Control.system-ca-bundle 92 | Package Control.cache/ 93 | Package Control.ca-certs/ 94 | bh_unicode_properties.cache 95 | 96 | # Sublime-github package stores a github token in this file 97 | # https://packagecontrol.io/packages/sublime-github 98 | GitHub.sublime-settings 99 | 100 | # test credentials 101 | /molecule/default/nextcloud_instances 102 | **/__pycache__/ 103 | 104 | # Environments 105 | .venv 106 | .vscode 107 | 108 | # ansible-test ouputs 109 | tests/output 110 | .ansible 111 | ansible.cfg 112 | collections/ -------------------------------------------------------------------------------- /roles/backup/tasks/nc_facts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if host has sudo 3 | ansible.builtin.command: "dpkg-query -s sudo" 4 | changed_when: false 5 | register: __sudo_installed 6 | failed_when: false 7 | 8 | - name: Get nextcloud status 9 | nextcloud.admin.run_occ: 10 | nextcloud_path: "{{ nextcloud_webroot }}" 11 | command: status --output=json 12 | become: true 13 | register: _nc_status 14 | changed_when: false 15 | 16 | - name: Get Nextcloud instance ID 17 | nextcloud.admin.run_occ: 18 | nextcloud_path: "{{ nextcloud_webroot }}" 19 | command: config:system:get instanceid 20 | become: true 21 | register: _nc_id 22 | changed_when: false 23 | when: nextcloud_backup_app_data or nextcloud_backup_user 24 | 25 | - name: Find the data folder if required 26 | when: 27 | - nextcloud_backup_app_data or nextcloud_backup_user 28 | - nextcloud_data_dir|d('') == '' 29 | block: 30 | - name: Get the data directory 31 | nextcloud.admin.run_occ: 32 | nextcloud_path: "{{ nextcloud_webroot }}" 33 | command: config:system:get datadirectory 34 | become: true 35 | register: nc_data_dir 36 | changed_when: false 37 | - name: Set missing fact 38 | ansible.builtin.set_fact: 39 | nextcloud_data_dir: "{{ nc_data_dir.stdout }}" 40 | 41 | - name: Get the list of apps installed 42 | nextcloud.admin.run_occ: 43 | nextcloud_path: "{{ nextcloud_webroot }}" 44 | command: app:list --output=json 45 | become: true 46 | register: _nc_app_list 47 | changed_when: false 48 | 49 | - name: Get the list of users 50 | nextcloud.admin.run_occ: 51 | nextcloud_path: "{{ nextcloud_webroot }}" 52 | command: user:list --output=json 53 | become: true 54 | register: _nc_user_list 55 | changed_when: false 56 | when: 57 | - nextcloud_backup_user 58 | 59 | - name: Find the DB credential if required 60 | when: 61 | - nextcloud_backup_database 62 | become: true 63 | block: 64 | - name: Get database type 65 | nextcloud.admin.run_occ: 66 | nextcloud_path: "{{ nextcloud_webroot }}" 67 | command: config:system:get dbtype 68 | register: _nc_dbtype 69 | changed_when: false 70 | - name: Get DB host 71 | nextcloud.admin.run_occ: 72 | nextcloud_path: "{{ nextcloud_webroot }}" 73 | command: config:system:get dbhost 74 | register: _nc_dbhost 75 | changed_when: false 76 | - name: Get DB name 77 | nextcloud.admin.run_occ: 78 | nextcloud_path: "{{ nextcloud_webroot }}" 79 | command: config:system:get dbname 80 | register: _nc_dbname 81 | changed_when: false 82 | - name: Get DB user 83 | nextcloud.admin.run_occ: 84 | nextcloud_path: "{{ nextcloud_webroot }}" 85 | command: config:system:get dbuser 86 | register: _nc_dbuser 87 | changed_when: false 88 | - name: Get DB password 89 | nextcloud.admin.run_occ: 90 | nextcloud_path: "{{ nextcloud_webroot }}" 91 | command: config:system:get dbpassword 92 | register: _nc_dbpassword 93 | changed_when: false 94 | no_log: true 95 | -------------------------------------------------------------------------------- /roles/install_nextcloud/files/nextcloud_file_name.xml: -------------------------------------------------------------------------------- 1 | 7V1bj6s4Ev41kWYfOgpgbo+nu8/ZfZgeraZXuztPR07iJMwQYME5nZ5fvzbYxNhOIMSQiyZSt0JhjCl/n8tVLpyJ87Ld/z2H2eYtXaJ4Ys+W+4nzOrHtwArIfyr4rASu41eCdR4tK5F1ELxHfyImnDHpLlqiolEQp2mMo6wpXKRJgha4IYN5nn40i63SuHnXDK6RInhfwFiV/ida4g2TWrPZ4cQ/ULTesFsHLjsxh4s/1nm6S9j9JrazKj/V6S3kdbHyxQYu0w9B5HydOC95muLq23b/gmKqWq626rpvR87W7c5Rgrtc4HvVFT9gvEO8yWXD8CdXRvk4iF4wmzjPH5sIo/cMLujZD9L7RLbB25gcWeTrKorjlzROc3KcpAkp9LyExaa8nJ5nt0M5RvujbbZqTRCAoXSLcP5JirALgMeUx8BlO+z4Q+gqruCN0Eu2z4SQwWNd131QEfnCtHREY8H9acyfXVVj4f1pLLA7aMwZTGPg/jTmdNEYGEpjlg5jXozpg0Y/yNc1Lh+zEu1kwZwLfkUxggWipmUDiW2JeQnSgLl8FZEpNRGZ5oalSOg97387OsI/r9IEPxWl+ftCCoTZ/nCO17HdUbsXV21KI9LDvNH58DfndWQ5kkVFBhMuS9AeL+J0t/xOMFREafJdVZ9YXhBrapYf4xLkU0xHxLB/iaN1QmQ4zfR8OIJ3DSs6U8DyO1LAM8AA2zHDgJ8hRgW+PuznZL6HSsTcAtjp5RfXq2fMGuHvsaz0I2SpmvHIHJInd1oO6aYqRjiks7s9OPQGfydqoa1UWcQGyHHJ1ItFnUnB3Td+V6uNAj2fQrUy20rRf9HGDa5JG/cc2ij2pMb/t11ZbTeG6MzSUKypBe7zm64JIgpr4XSrKxoluqKZtla82Gia8HpKCY9G8FUJiXviN2RHC0JmlA9GeNmlH5fwuqhRDzv5nqFFBGnNxW61ivYt9B7BUK5yhMqAYV6z99EoVXCl3xOrhiGRHOXRkkgX5TFBImuEQGKB8/QPxIUT2/EWAZqv6jM8uO2Y0WcdymH6BJaqT9/V6DM0MSjZV9BnYM8dzxtLn7qY2mD6tPwr6DP0fAeOps86YDMKPkNFfWi5Ru/sMM3xJl2nCYy/HqTPTQULykT7CP9X+P4bLTJ16VFCGlafogeHc78jjD/Zchvc4ZSIDvf9OaVDbXlZU/v2Ke0X6S5fIK7fSoZhvkasmM2GOfqsJ/soRzHE0Y/mCtxFClcBXJCWYbUb4jjKCtSOYEhtJ30uMlehnVJaYN6BtECG8oi0E+W0jihZs34zAt2gCV1Xha7OUjkGkOs4QyCXoZVh1xKROxsfuYGKXK46c8gtL/2S5/BTKJClUYILoeZ/UoEQVXZA0wj4rtR5VY2Hrqyb1q13wfC9O7vB3rVvsneBa7h33UF697jVuZHeta5mdTTTeueb0gk4j2CyjjsYnTzFpIUp9YWerKDskDSP/iSWB8bm7ItjN3HoMByK9kWbIWDCvjz+zIjbkgZG/WthlLdGwGheLXwX/SdH4lzINYFIOc1Hg0hPg0hgAJFAdXWGReT4oyYfIUVEgquNmrw1AiKzHD3dPiodMCIqZ6oeHm2ctDXj5NU8SN4aAZWvMIo/bxeOwBkPjq6ac5WkQyBU9SW6orBUd1XMavMwVcSKASuayBss0GKhi1rNAxe4JydiDYxroiRu17kA6/en2dRzQrYMc56HorggviMZWi+chuKnWWHVbFbHAS3nOj5AWrG2g1ACn+D4HK7m1aerVYHwRIbrec6Rqy5cj4VgE5ZfRvd4CNZ4XK5zNoKJ8Qz7IPhSqAGW9T4m1IDqFqaZJhRp2J4PNseUkIWspYt8HbLqiH4nZAHdrNQzbf97ocgPLGnm5xxHUR+EqFPgXovqNEllup1mLZkiZ66nV6KfLGtKkPO37mvjfSdKBuZGsl0bdaquZhJ+/eW1vzpaVx6aDAVmNBjK62UjLjqAsYNCQw6P7B0wzfA4Kz+dh0dPMzyCmxgeQyCzrTE8tpf3zAa+QUuOUns+jWXpEmp+fSED7L45uPZPQtLfY44wbLnLRQ6oMGc07ZAGfE5dL7E7ow26rhrW7G1Be1jPHoliP1mz+7ClniV5a5pMlKG61fPuxxJczSXj7pdoGTzjlqEzEw29jLSFBU2iNT2NPXsI0HKzV1tuiMPA9UbjsG+pjz4shw+HFwQNw1EprIkLmp/cdaawLv1vXHe0l0G9YwfV9sLxjKou2iDrIll+oZtpTOp8zRMTyWPMsxQitxJq6aJgCXSEqtNtj2q/1wK7oG9d9ieXXRpnd6XudqV+rAYDJbKu5gxJFblyEoahEL2ybYQ7O9kupXwzU00pL7+0dnb5lvYoSwzN8he7mJ5ulvNoHOq6+DoSh6Q3NoAcaurKIYKNKXFWfQuw/81qAZiKJ4dZApP5wh+mK79AC/5dKWf47PIt7VHWVRyz/HLVIOqnLhfkVnORz3YGLyFt6/qc3ZHIwvqc5ft9WHvx+twpHA21FKyGC//CWies6bIZumbOC1jjxn9krJ2cEwyENc8fAlfdnWHrHgJaPHh1G96wp5oiwdPtEOefZfvKCT3yku/uslrc59/I5+nt7en1telpt+yd0Nf9FXFhKAVdXjoYMz7lBuNQ8vLENTWuNdpQz4f1W1x9DJrYMe7qaTy7W8LHeBiwb2lYdjRByjt42QbI73V7I75sc5/vJ8kqA5pknqFUBgZ5h04k/5XfftVm3d3GwK5k3YWnYxJKWsm55Q2noXB3o2OMcBHDoogWTcSovXoKK30zM6WObo0MVhb2WoFAV8JF72A6kFZFuwbT+2BBXXPR5QCTERFLQywiPgCclwVojzPSkNJuuSGYZtcpeRudbbRcluNSDOcofq63KdclqQmTi6CGh2ZzYLb7OmvYpB5rReAER1DCappNAeD7Q3MGGsHHk2XrajXqxwf243D7ejw2FND3ZuHpigzyWF33eXwe24/LY1fH4x77VyrZSpfuW9k1x6Ko+m6otMW+MaKj04gLcpY0O4oNFRMK1JjfO4oJUejGPqRq8vcL311vImxb+kInShu2h3yOp2YGBjLrZ5sGWsI4EaMV1owS1aZ5RbUJ0b9K9+AJSGGDcKK+RUCPv8EtfcXVef03ypcwgadGjjN+tMNpBmgsoAb36s690IEjh4dfUKnGg8Ov1Dhf/w8= -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/http_nginx.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: http_nginx | Remove some commented line in php-fpm conf 3 | ansible.builtin.lineinfile: 4 | dest: "{{ php_dir }}/fpm/pool.d/www.conf" 5 | regexp: '^\;env' 6 | state: absent 7 | # validate: "/usr/sbin/{{ php_bin }} -t #%s" 8 | notify: Reload php-fpm 9 | 10 | - name: http_nginx | Add path variable to php-fpm 11 | ansible.builtin.blockinfile: 12 | dest: "{{ php_dir }}/fpm/pool.d/www.conf" 13 | insertafter: '^; Default Value: clean env$' 14 | marker: "; {mark} ANSIBLE MANAGED BLOCK" 15 | block: | 16 | env[HOSTNAME] = $HOSTNAME 17 | env[PATH] = $PATH 18 | env[TMP] = /tmp 19 | env[TMPDIR] = /tmp 20 | env[TEMP] = /tmp 21 | notify: Reload php-fpm 22 | 23 | - name: http_nginx | Enable APC for php CLI 24 | ansible.builtin.lineinfile: 25 | dest: "{{ php_dir }}/cli/php.ini" 26 | line: "apc.enable_cli = 1" 27 | insertbefore: "^; End:$" 28 | state: present 29 | # validate: "/usr/sbin/{{ php_bin }} -t #%s" 30 | notify: Reload php-fpm 31 | 32 | - name: http_nginx | Enable PHP OPcache for php.ini 33 | ansible.builtin.lineinfile: 34 | dest: "{{ php_dir }}/fpm/php.ini" 35 | state: present 36 | regexp: "{{ item.regexp }}" 37 | line: "{{ item.line }}" 38 | backrefs: true 39 | with_items: 40 | - { regexp: 'opcache.enable=0', line: 'opcache.enable=1' } 41 | - { regexp: 'opcache.enable_cli', line: 'opcache.enable_cli=1' } 42 | - { regexp: 'opcache.interned_strings_buffer', line: 'opcache.interned_strings_buffer=16' } 43 | - { regexp: 'opcache.max_accelerated_files', line: 'opcache.max_accelerated_files=10000' } 44 | - { regexp: 'opcache.memory_consumption', line: 'opcache.memory_consumption=128' } 45 | - { regexp: 'opcache.save_comments', line: 'opcache.save_comments=1' } 46 | - { regexp: 'opcache.revalidate_freq', line: 'opcache.revalidate_freq=1' } 47 | - { regexp: 'memory_limit', line: 'memory_limit={{ php_memory_limit }}'} 48 | # validate: "/usr/sbin/{{ php_bin }} -t #%s" 49 | notify: Reload php-fpm 50 | 51 | 52 | - name: http_nginx | Generate Public Diffie-Hellman parameter (This might take a while) 53 | ansible.builtin.command: "openssl dhparam -out {{ nextcloud_tls_dhparam }} 2048" 54 | args: 55 | creates: "{{ nextcloud_tls_dhparam }}" 56 | 57 | - name: http_nginx | Configure php handler 58 | ansible.builtin.template: 59 | dest: /etc/nginx/sites-available/php_handler.cnf 60 | src: templates/nginx_php_handler.j2 61 | mode: 0640 62 | notify: Reload http 63 | 64 | - name: http_nginx | Enable php handler 65 | ansible.builtin.file: 66 | path: /etc/nginx/sites-enabled/php_handler 67 | src: /etc/nginx/sites-available/php_handler.cnf 68 | state: link 69 | notify: Reload http 70 | 71 | - name: http_nginx | Generate Nextcloud configuration for nginx 72 | ansible.builtin.template: 73 | dest: /etc/nginx/sites-available/nc_{{ nextcloud_instance_name }}.cnf 74 | src: "{{ nextcloud_websrv_template }}" 75 | mode: 0640 76 | notify: Reload http 77 | 78 | - name: http_nginx | Enable Nextcloud in nginx conf 79 | ansible.builtin.file: 80 | path: /etc/nginx/sites-enabled/nc_{{ nextcloud_instance_name }} 81 | src: /etc/nginx/sites-available/nc_{{ nextcloud_instance_name }}.cnf 82 | state: link 83 | notify: Reload http 84 | 85 | - name: http_nginx | Disable nginx default site 86 | ansible.builtin.file: 87 | path: /etc/nginx/sites-enabled/default 88 | state: absent 89 | when: nextcloud_disable_websrv_default_site | bool 90 | notify: Reload http 91 | -------------------------------------------------------------------------------- /roles/install_nextcloud/templates/apache2_nc.j2: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This file was generated by Ansible for {{ansible_fqdn}} 3 | # Do NOT modify this file by hand! 4 | ################################################################################ 5 | 6 | {% if nextcloud_install_tls and nextcloud_tls_enforce %} 7 | {% for domain in nextcloud_trusted_domain %} 8 | 9 | ServerName {{ domain }} 10 | Redirect permanent / https://{{ domain | ansible.utils.ipwrap }}/ 11 | 12 | {% endfor %} 13 | {% else %} 14 | 15 | ServerName {{ nextcloud_trusted_domain[0] }} 16 | {% for index in range(1, nextcloud_trusted_domain|length) %} 17 | ServerAlias {{ nextcloud_trusted_domain[index]}} 18 | {% endfor %} 19 | DocumentRoot {{ nextcloud_webroot }} 20 | {% if (nextcloud_max_upload_size_in_bytes|int) <= 2147483647-%} 21 | LimitRequestBody {{ nextcloud_max_upload_size_in_bytes }} 22 | {% endif -%} 23 | 24 | Allow from all 25 | Satisfy Any 26 | Options +FollowSymlinks 27 | AllowOverride All 28 | 29 | 30 | Dav off 31 | 32 | 33 | SetEnv HOME {{ nextcloud_webroot }} 34 | SetEnv HTTP_HOME {{ nextcloud_webroot }} 35 | 36 | 37 | 38 | {% endif %} 39 | 40 | {% if nextcloud_install_tls %} 41 | 42 | ServerName {{ nextcloud_trusted_domain[0] }} 43 | {% for index in range(1, nextcloud_trusted_domain|length) %} 44 | ServerAlias {{ nextcloud_trusted_domain[index]}} 45 | {% endfor %} 46 | DocumentRoot {{ nextcloud_webroot }} 47 | {% if (nextcloud_max_upload_size_in_bytes|int) <= 2147483647-%} 48 | LimitRequestBody {{ nextcloud_max_upload_size_in_bytes }} 49 | LimitRequestFieldsize 32768 50 | {% endif -%} 51 | SSLEngine on 52 | SSLCertificateFile {{ nextcloud_tls_cert_file }} 53 | SSLCertificateKeyFile {{ nextcloud_tls_cert_key_file }} 54 | {% if nextcloud_tls_chain_file is defined %} 55 | SSLCertificateChainFile {{ nextcloud_tls_chain_file }} 56 | {% endif %} 57 | 58 | # enable HTTP/2, if available 59 | Protocols h2 http/1.1 60 | 61 | {% if nextcloud_hsts is string %} 62 | 63 | Header always set Strict-Transport-Security "{{ nextcloud_hsts }}" 64 | 65 | {% endif %} 66 | 67 | 68 | Allow from all 69 | Satisfy Any 70 | Options +FollowSymlinks 71 | AllowOverride All 72 | 73 | 74 | Dav off 75 | 76 | 77 | SetEnv HOME {{ nextcloud_webroot }} 78 | SetEnv HTTP_HOME {{ nextcloud_webroot }} 79 | 80 | 81 | 82 | {% endif %} 83 | 84 | {% if nextcloud_install_tls %} 85 | {% if nextcloud_mozilla_modern_ssl_profile %} 86 | # modern configuration, tweak to your needs 87 | SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 -TLSv1.2 88 | {% else %} 89 | # intermediate configuration, tweak to your needs 90 | SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 91 | SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 92 | {% endif %} 93 | 94 | SSLHonorCipherOrder off 95 | # SSLSessionTickets off 96 | 97 | SSLCompression off 98 | 99 | # OCSP stapling 100 | SSLUseStapling on 101 | SSLStaplingResponderTimeout 5 102 | SSLStaplingReturnResponderErrors off 103 | SSLStaplingCache shmcb:/var/run/ocsp(128000) 104 | {% endif %} 105 | -------------------------------------------------------------------------------- /.github/workflows/collection-tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: collection-tests 3 | "on": 4 | workflow_call: 5 | inputs: 6 | distros: 7 | required: false 8 | description: a list of debian based distribution 9 | type: string 10 | default: "['debian12', 'ubuntu2404']" 11 | nc_versions: 12 | required: false 13 | description: a list of nextcloud server version 14 | type: string 15 | default: "['latest']" 16 | env: 17 | PYTHON_VERSION: '3.12' 18 | jobs: 19 | lint: 20 | name: linters 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v6 24 | with: 25 | fetch-depth: 0 26 | - name: Run ansible-lint 27 | uses: ansible/ansible-lint@v25 28 | 29 | - name: Set up Python 3. 30 | uses: actions/setup-python@v6 31 | with: 32 | python-version: ${{ env.PYTHON_VERSION }} 33 | 34 | - name: Upgrade pip 35 | run: | 36 | pip install --constraint=.github/workflows/constraints.txt pip 37 | pip --version 38 | 39 | - name: Upgrade yamllint 40 | run: | 41 | pip install --constraint=.github/workflows/constraints.txt yamllint 42 | 43 | - name: Lint code. 44 | run: | 45 | yamllint -c .yamllint.yml . 46 | 47 | ansible-tests: 48 | name: ansible-tests 49 | needs: 50 | - lint 51 | runs-on: ubuntu-latest 52 | defaults: 53 | run: 54 | working-directory: ./code/ansible_collections/nextcloud/admin/ 55 | steps: 56 | - name: Checkout repository 57 | uses: actions/checkout@v6 58 | with: 59 | path: ./code/ansible_collections/nextcloud/admin/ 60 | fetch-depth: 0 61 | - name: Set up Python 62 | uses: actions/setup-python@v6 63 | with: 64 | python-version: ${{ env.PYTHON_VERSION }} 65 | 66 | - name: Install dependencies 67 | run: | 68 | python -m pip install --constraint=.github/workflows/constraints.txt --upgrade pip 69 | pip install --constraint=.github/workflows/constraints.txt ansible pytest 70 | 71 | - name: Run units tests 72 | run: | 73 | ansible-test units --requirements --docker 74 | 75 | - name: Run sanity tests 76 | run: | 77 | ansible-test sanity --requirements --docker 78 | 79 | molecule_test: 80 | defaults: 81 | run: 82 | working-directory: "nextcloud.ansible-collection-nextcloud-admin" 83 | name: molecule-test 84 | runs-on: ubuntu-latest 85 | strategy: 86 | fail-fast: false 87 | matrix: 88 | distros: ${{ fromJson(inputs.distros)}} 89 | nc_versions: ${{ fromJson(inputs.nc_versions)}} 90 | steps: 91 | - name: Check out the repository 92 | uses: actions/checkout@v6 93 | with: 94 | path: "nextcloud.ansible-collection-nextcloud-admin" 95 | 96 | - name: Set up Python 97 | uses: actions/setup-python@v6 98 | with: 99 | python-version: ${{ env.PYTHON_VERSION }} 100 | 101 | - name: Upgrade pip 102 | run: | 103 | python3 -m pip install --constraint=.github/workflows/constraints.txt pip 104 | python3 -m pip --version 105 | 106 | - name: Install test dependencies 107 | run: | 108 | python3 -m pip install --constraint=.github/workflows/constraints.txt ansible 'molecule-plugins[docker]' docker netaddr PyMySQL 109 | 110 | - name: precreate mysql database 111 | run: | 112 | sudo systemctl start mysql.service 113 | ansible localhost -m community.mysql.mysql_db -a "name=nc login_user=root login_password=root state=present" 114 | ansible localhost -m community.mysql.mysql_user -a "name=nc password=nc priv=nc.*:ALL login_user=root login_password=root state=present" 115 | 116 | - name: Run Molecule tests 117 | run: molecule test 118 | env: 119 | PY_COLORS: "1" 120 | ANSIBLE_FORCE_COLOR: "1" 121 | MOLECULE_DISTRO: ${{ matrix.distros }} 122 | MOLECULE_NC: ${{ matrix.nc_versions }} 123 | -------------------------------------------------------------------------------- /.github/workflows/tests_and_release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Collection tests 3 | "on": 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | check_version_change: 13 | name: check-new-version 14 | runs-on: ubuntu-latest 15 | outputs: 16 | new_version: ${{ steps.check-galaxy-version.outputs.new_version }} 17 | steps: 18 | - name: Check out the repository 19 | uses: actions/checkout@v6 20 | with: 21 | fetch-depth: 2 22 | 23 | - name: Check if 'version' has changed in galaxy.yml 24 | id: check-galaxy-version 25 | shell: bash 26 | run: | 27 | compare_semver() { 28 | version_test=(${1//./ }) 29 | version_ref=(${2//./ }) 30 | for ((i=0; i<${#version_test[@]}; i++)); do 31 | if [[ -z ${version_ref[i]} ]]; then 32 | return 0 33 | elif [[ ${version_test[i]} -gt ${version_ref[i]} ]]; then 34 | return 0 35 | elif [[ ${version_test[i]} -lt ${version_ref[i]} ]]; then 36 | return 1 37 | fi 38 | done 39 | return 1 40 | } 41 | set -x 42 | current_version=$(git diff HEAD~ HEAD -- galaxy.yml | grep '\-version:' | awk '{ print $2}' || true) 43 | galaxy_version=$(git diff HEAD~ HEAD -- galaxy.yml | grep '+version:' | awk '{ print $2}' || true) 44 | if [ -z "$galaxy_version" ]; then 45 | echo "new_version=false" >> $GITHUB_OUTPUT 46 | elif $(compare_semver "$galaxy_version" "$current_version"); then 47 | echo "new_version=true" >> $GITHUB_OUTPUT 48 | else 49 | echo "new_version=false" >> $GITHUB_OUTPUT 50 | fi 51 | 52 | large_collection_tests: 53 | name: release tests 54 | uses: ./.github/workflows/collection-tests.yml 55 | needs: check_version_change 56 | if: (needs.check_version_change.outputs.new_version == 'true') && (github.ref_name == 'main') 57 | with: 58 | distros: "['debian12', 'debian11', 'ubuntu2204', 'ubuntu2404']" 59 | nc_versions: "['latest', 'nc31', 'nc30']" 60 | 61 | collection_tests: 62 | name: normal tests 63 | uses: ./.github/workflows/collection-tests.yml 64 | needs: check_version_change 65 | if: (needs.check_version_change.outputs.new_version != 'true') || (github.ref_name != 'main') 66 | 67 | tag_release: 68 | name: new-tag 69 | runs-on: ubuntu-latest 70 | needs: large_collection_tests 71 | steps: 72 | - name: Check out the repository 73 | uses: actions/checkout@v6 74 | with: 75 | fetch-depth: 2 76 | - name: Detect and tag new version 77 | id: check-version 78 | uses: salsify/action-detect-and-tag-new-version@v2 79 | with: 80 | tag-template: "{VERSION}" 81 | version-command: | 82 | cat galaxy.yml | grep version: | cut -d' ' -f2 83 | 84 | - name: Publish the release notes 85 | uses: release-drafter/release-drafter@v6 86 | with: 87 | publish: ${{ steps.check-version.outputs.tag != '' }} 88 | tag: ${{ steps.check-version.outputs.tag }} 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | publish_to_galaxy: 92 | name: publish 93 | runs-on: ubuntu-latest 94 | needs: tag_release 95 | steps: 96 | - name: Check out the repository 97 | uses: actions/checkout@v6 98 | with: 99 | fetch-depth: 0 100 | 101 | - name: Set up Python 102 | uses: actions/setup-python@v6 103 | with: 104 | python-version: ${{ env.PYTHON_VERSION }} 105 | 106 | - name: install ansible 107 | run: | 108 | pip install --upgrade --constraint=.github/workflows/constraints.txt pip 109 | pip --version 110 | pip install --upgrade --constraint=.github/workflows/constraints.txt ansible 111 | 112 | - name: Release to Ansible Galaxy. 113 | env: 114 | ANSIBLE_GALAXY_TOKEN: ${{ secrets.GALAXY_TOKEN }} 115 | run: >- 116 | ansible-playbook -i 'localhost,' galaxy-deploy.yml 117 | -e "github_tag=$(cat galaxy.yml|grep version:|cut -d' ' -f2)" 118 | -e "ansible_namespace=$(echo ${{ github.repository }} 119 | | cut -d/ -f1)" 120 | -e "collection=admin" 121 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/db_mysql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: db_mysql | Service is installed 3 | ansible.builtin.package: 4 | name: "{{ 'default-' if ((ansible_distribution | lower) == 'debian' and nextcloud_db_backend == 'mysql') else '' }}{{ nextcloud_db_backend }}-server" 5 | state: present 6 | register: nc_mysql_db_install 7 | 8 | - name: db_mysql | Check if MySQL packages were installed 9 | ansible.builtin.set_fact: 10 | mysql_install_packages: "{{ nc_mysql_db_install is defined and nc_mysql_db_install.changed }}" 11 | 12 | - name: db_mysql | Get MySQL version 13 | ansible.builtin.command: 'mysql --version' 14 | register: mysql_cli_version 15 | changed_when: false 16 | check_mode: false 17 | 18 | - name: db_mysql | Install packages for MySQL 19 | ansible.builtin.package: 20 | name: "{{ nc_mysql_deps }}" 21 | state: present 22 | vars: 23 | nc_mysql_deps: 24 | - "python3-pymysql" 25 | 26 | - name: db_mysql | Ensure MySQL is started and enabled on boot 27 | ansible.builtin.service: 28 | name: "{{ mysql_daemon }}" 29 | state: started 30 | enabled: "{{ nextcloud_db_enabled_on_startup }}" 31 | register: mysql_service_configuration 32 | 33 | - name: db_mysql | Get list of hosts for the root user 34 | ansible.builtin.command: mysql -NBe 35 | "SELECT Host 36 | FROM mysql.user 37 | WHERE User = 'root' 38 | ORDER BY (Host='localhost') ASC" 39 | register: mysql_root_hosts 40 | changed_when: false 41 | check_mode: false 42 | when: mysql_install_packages | bool or nextcloud_mysql_root_pwd_update 43 | 44 | # Note: We do not use mysql_user for this operation, as it doesn't always update 45 | # the root password correctly. See: https://goo.gl/MSOejW 46 | - name: db_mysql | Update MySQL root password for localhost root account (5.7.x) 47 | ansible.builtin.shell: > 48 | mysql -u root -NBe 49 | 'ALTER USER "root"@"{{ item }}" 50 | IDENTIFIED WITH mysql_native_password BY "{{ nextcloud_mysql_root_pwd }}"; FLUSH PRIVILEGES;' 51 | with_items: "{{ mysql_root_hosts.stdout_lines | default([]) }}" 52 | when: > 53 | ((mysql_install_packages | bool) or nextcloud_mysql_root_pwd_update) 54 | and ('5.7.' in mysql_cli_version.stdout or '8.0.' in mysql_cli_version.stdout) 55 | register: output 56 | changed_when: "output.rc == 0" 57 | 58 | - name: db_mysql | Update MySQL root password for localhost root account (< 5.7.x) 59 | ansible.builtin.shell: > 60 | mysql -NBe 61 | 'SET PASSWORD FOR "root"@"{{ item }}" = PASSWORD("{{ nextcloud_mysql_root_pwd }}"); FLUSH PRIVILEGES;' 62 | with_items: "{{ mysql_root_hosts.stdout_lines | default([]) }}" 63 | when: > 64 | ((mysql_install_packages | bool) or nextcloud_mysql_root_pwd_update) 65 | and ('5.7.' not in mysql_cli_version.stdout and '8.0.' not in mysql_cli_version.stdout) 66 | register: output 67 | changed_when: "output.rc == 0" 68 | 69 | - name: db_mysql | Copy .my.cnf file with root password credentials 70 | ansible.builtin.template: 71 | src: "root-my.cnf.j2" 72 | dest: "/root/.my.cnf" 73 | owner: root 74 | group: root 75 | mode: 0600 76 | when: mysql_install_packages | bool or nextcloud_mysql_root_pwd_update 77 | 78 | - name: db_mysql | Get list of hosts for the anonymous user 79 | ansible.builtin.command: mysql -NBe 'SELECT Host FROM mysql.user WHERE User = ""' 80 | register: mysql_anonymous_hosts 81 | changed_when: false 82 | check_mode: false 83 | 84 | - name: db_mysql | Remove anonymous MySQL users 85 | community.mysql.mysql_user: 86 | name: "" 87 | host: "{{ item }}" 88 | state: absent 89 | with_items: "{{ mysql_anonymous_hosts.stdout_lines | default([]) }}" 90 | 91 | - name: db_mysql | Remove MySQL test database 92 | community.mysql.mysql_db: 93 | name: 'test' 94 | state: absent 95 | 96 | - name: db_mysql | Set mysql config option for Nextcloud 97 | ansible.builtin.copy: 98 | dest: /etc/mysql/conf.d/nextcloud.cnf 99 | src: files/mysql_nextcloud.cnf 100 | mode: 0600 101 | notify: Restart mysql 102 | 103 | - name: db_mysql | Add Database {{ nextcloud_db_name }}" 104 | community.mysql.mysql_db: 105 | name: "{{ nextcloud_db_name }}" 106 | login_user: root 107 | login_password: "{{ nextcloud_mysql_root_pwd }}" 108 | config_file: "{{ mysql_credential_file[(ansible_os_family | lower)] | default(omit) }}" 109 | state: present 110 | 111 | - name: db_mysql | Configure the database user 112 | community.mysql.mysql_user: 113 | name: "{{ nextcloud_db_admin }}" 114 | password: "{{ nextcloud_db_pwd }}" 115 | priv: "{{ nextcloud_db_name }}.*:ALL" 116 | login_user: root 117 | login_password: "{{ nextcloud_mysql_root_pwd }}" 118 | config_file: "{{ mysql_credential_file[(ansible_os_family | lower)] | default(omit) }}" 119 | state: present 120 | -------------------------------------------------------------------------------- /roles/backup/README.md: -------------------------------------------------------------------------------- 1 | # Ansible role: backup 2 | 3 | An ansible role that creates a backup of a Nextcloud server. The backup is kept on the server (unless you [fetch it](#fetching-backup-from-remote-to-local-machine)). 4 | 5 | ## Requirements 6 | 7 | The roles requires the following tools to be available on the host: 8 | - tar 9 | - gzip 10 | - rsync 11 | - a mysql or postgreSQL client if the database has to be dumped. 12 | 13 | You'll need enough space on the target file system, depending on the size of your nextcloud server. 14 | 15 | ## Role Variables 16 | 17 | ### Locating the nextcloud server 18 | 19 | The role has to know where the server files are, how to access it and where to store the backup. 20 | 21 | ```yaml 22 | nextcloud_backup_target_dir: "/opt/nextcloud_backups" 23 | nextcloud_webroot: "/opt/nextcloud" 24 | # nextcloud_data_dir: "/var/ncdata" # optional. 25 | nextcloud_websrv_user: "www-data" # you may need to change this to the nextcloud file owner depending of your setup and OS 26 | ``` 27 | 28 | ### Adjusting the backup owner 29 | The backup owner can be adjusted with. This may be useful when operating user is different than nextcloud's process owner. 30 | 31 | ```yaml 32 | nextcloud_backup_owner: "www-data" # user name who will get owner on backup_target_dir and final archive 33 | nextcloud_backup_group: "www-data" # user group who will get owner on backup_target_dir and final archive 34 | ``` 35 | 36 | ### Adjusting the backup name 37 | The backup name can be adjusted with 38 | 39 | ```yaml 40 | nextcloud_instance_name: "nextcloud" # a human identifier for the server 41 | nextcloud_backup_suffix: "" # some arbitrary information at the end of the archive name 42 | nextcloud_backup_format: "tgz" # extension of the archive. use a supported format used by the archive module (Choices: bz2, gz, tar, xz, zip) 43 | ``` 44 | 45 | Or you can change it completely by redefining 46 | 47 | ```yaml 48 | nc_archive_name: "{{ nextcloud_instance_name }}_nextcloud-{{ nc_status.versionstring }}_{{ ansible_date_time.iso8601_basic_short }}{{ nextcloud_backup_suffix }}" 49 | ``` 50 | 51 | ### Adjusting the backup content 52 | 53 | The role will __always__: 54 | - backup the server's config 55 | - create a list of installed & enabled applications(along with the version numbers) 56 | 57 | You can adjust the scope of the backup by enabling/disabling some flags defined in default: 58 | 59 | ```yaml 60 | nextcloud_backup_download_server_archive: true 61 | nextcloud_backup_app_data: true 62 | nextcloud_backup_user: true 63 | nextcloud_backup_database: true 64 | ``` 65 | 66 | ### Adjusting nextcloud-server archive included in backup 67 | Role can download the proper server archive from the nextcloud download site and add it to backup archive. 68 | It can be turned on using: `nextcloud_backup_download_server_archive` variable. 69 | 70 | ### Adjusting app data backup 71 | 72 | You may want to exclude some app_data folders from the backup. 73 | But you cannot target a specific app to backup, this requires knowledge of the app's code. 74 | 75 | ```yaml 76 | nextcloud_backup_app_data_exclude_folder: 77 | - preview 78 | ``` 79 | 80 | By default the preview folder is excluded from the backup as it can be notoriously __large__ 81 | 82 | ### Adjusting user backup 83 | 84 | You can exclude a list of user(s) from the backup 85 | 86 | ```yaml 87 | nextcloud_backup_exclude_users: [] 88 | ``` 89 | 90 | You can also decide to include or not some side-folders. 91 | 92 | ```yaml 93 | nextcloud_backup_user_files_trashbin: true 94 | nextcloud_backup_user_files_versions: true 95 | nextcloud_backup_user_uploads: true 96 | nextcloud_backup_user_cache: true 97 | ``` 98 | 99 | ### Fetching backup from remote to local machine 100 | 101 | You can fetch created backup from remote by setting these variables. 102 | WARNING: user which you are used in Ansible has to be set as [backup owner](#adjusting-the-backup-owner) due to Ansible limitation on using `become` with `ansible.builtin.fetch` 103 | 104 | ```yaml 105 | nextcloud_backup_fetch_to_local: true 106 | nextcloud_backup_fetch_local_path: "/local_path/nextcloud_backup" 107 | ``` 108 | 109 | ### Other 110 | 111 | You can leave the server in maintenance mode at the end of the process by turning false 112 | 113 | ```yaml 114 | nextcloud_exit_maintenance_mode: true 115 | ``` 116 | 117 | ## The Dependencies 118 | 119 | None 120 | 121 | ## Example Playbook 122 | 123 | ### Running a full backup of your nextcloud server 124 | 125 | ```yaml 126 | - hosts: nextcloud 127 | roles: 128 | - role: nextcloud.admin.backup 129 | ``` 130 | 131 | ### Making a partial backup with only the app_data 132 | 133 | ```yaml 134 | - hosts: nextcloud 135 | roles: 136 | - role: nextcloud.admin.backup 137 | vars: 138 | nextcloud_backup_suffix: _only_app_data 139 | nextcloud_backup_user: false 140 | nextcloud_backup_database: false 141 | ``` 142 | 143 | ## Contributing 144 | 145 | We encourage you to contribute to this role! Please check out the 146 | [contributing guide](../CONTRIBUTING.md) for guidelines about how to proceed. 147 | 148 | ## License 149 | 150 | BSD 151 | -------------------------------------------------------------------------------- /plugins/modules/run_occ.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2022, Marc Crébassa 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | DOCUMENTATION = r""" 27 | 28 | module: run_occ 29 | 30 | short_description: Run the occ command line tool with given arguments 31 | 32 | author: 33 | - "Marc Crébassa (@aalaesar)" 34 | 35 | description: 36 | - Run admin commands on a Nextcloud instance using the OCC command line tool. 37 | - Pass only arguments understood by the occ tool. 38 | - Check https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html 39 | for more details about the available commands. 40 | - Don't support check mode. 41 | - Always returns state as 'changed'. 42 | - This module requires to be run with advanced privileges 43 | unless it is run as the user that own the occ tool. 44 | 45 | extends_documentation_fragment: 46 | - nextcloud.admin.occ_common_options 47 | 48 | options: 49 | command: 50 | description: 51 | - The string passed directly to the occ command line. 52 | - Shell variable expansion is not available. 53 | - Shell pipelining is not supported. 54 | required: true 55 | type: str 56 | aliases: 57 | - args 58 | 59 | requirements: 60 | - "python >=3.6" 61 | """ 62 | 63 | EXAMPLES = r""" 64 | - name: get nextcloud basic info status 65 | nextcloud.admin.run_occ: 66 | command: status --output=json 67 | nextcloud_path: /var/lib/www/nextcloud 68 | changed_when: false 69 | register: nc_status 70 | 71 | - name: install an application 72 | nextcloud.admin.run_occ: 73 | command: app:install notes 74 | nextcloud_path: /var/lib/www/nextcloud 75 | """ 76 | RETURN = r""" 77 | command: 78 | description: The complete line of arguments given to the occ tool. 79 | returned: always 80 | type: str 81 | rc: 82 | description: The return code given by the occ tool. 83 | returned: always 84 | type: int 85 | stdout: 86 | description: The complete normal return of the occ tool in one string. All new lines will be replaced by \\n. 87 | returned: always 88 | type: str 89 | stdout_lines: 90 | description: The complete normal return of the occ tool. Each line is a element of a list. 91 | returned: always 92 | type: list 93 | elements: str 94 | stderr: 95 | description: The complete error return of the occ tool in one string. All new lines will be replaced by \\n. 96 | returned: always 97 | type: str 98 | stderr_lines: 99 | description: The complete error return of the occ tool. Each line is a element of a list. 100 | returned: always 101 | type: list 102 | elements: str 103 | """ 104 | 105 | from ansible.module_utils.basic import AnsibleModule 106 | from ansible_collections.nextcloud.admin.plugins.module_utils.nc_tools import ( 107 | run_occ, 108 | extend_nc_tools_args_spec, 109 | ) 110 | from ansible_collections.nextcloud.admin.plugins.module_utils.exceptions import ( 111 | OccExceptions, 112 | ) 113 | 114 | module_args_spec = dict( 115 | command=dict( 116 | type="str", 117 | required=True, 118 | aliases=["args"], 119 | ), 120 | ) 121 | 122 | 123 | def main(): 124 | global module 125 | 126 | module = AnsibleModule( 127 | argument_spec=extend_nc_tools_args_spec(module_args_spec), 128 | supports_check_mode=False, 129 | ) 130 | command = module.params.get("command") 131 | result = dict(command=command) 132 | try: 133 | returnCode, stdOut, stdErr = run_occ(module, command)[0:3] 134 | result.update( 135 | dict( 136 | rc=returnCode, 137 | stdout=stdOut, 138 | stderr=stdErr, 139 | ) 140 | ) 141 | except OccExceptions as e: 142 | e.fail_json(module, **result) 143 | 144 | module.exit_json( 145 | changed=True, 146 | **result, 147 | ) 148 | 149 | 150 | if __name__ == "__main__": 151 | main() 152 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # tasks file for nextcloud 3 | 4 | - name: Load os specific variables 5 | ansible.builtin.include_tasks: setup_env.yml 6 | tags: 7 | - install_apps 8 | 9 | - name: Install PHP packages 10 | ansible.builtin.include_tasks: php_install.yml 11 | when: nextcloud_install_php 12 | tags: 13 | - install_apps 14 | 15 | - name: Install web server packages 16 | when: nextcloud_install_websrv 17 | ansible.builtin.include_tasks: websrv_install.yml 18 | 19 | - name: Install certificates 20 | when: nextcloud_install_tls 21 | block: 22 | - name: Verify permission for installed TLS certificates 23 | ansible.builtin.include_tasks: tls_installed.yml 24 | when: nextcloud_tls_cert_method == "installed" 25 | 26 | - name: Install given signed certificates 27 | ansible.builtin.include_tasks: tls_signed.yml 28 | when: nextcloud_tls_cert_method == "signed" 29 | 30 | - name: Configure self signed TLS certificates 31 | ansible.builtin.include_tasks: tls_selfsigned.yml 32 | when: nextcloud_tls_cert_method == "self-signed" 33 | 34 | - name: Configure web server 35 | when: nextcloud_install_websrv 36 | block: 37 | - name: Configure Nginx web server. 38 | ansible.builtin.include_tasks: http_nginx.yml 39 | when: nextcloud_websrv in ["nginx"] 40 | 41 | - name: Configure Apache web server 42 | ansible.builtin.include_tasks: http_apache.yml 43 | when: nextcloud_websrv in ["apache2"] 44 | 45 | - name: Configure Redis server 46 | ansible.builtin.include_tasks: redis_server.yml 47 | when: (nextcloud_install_redis_server | bool) 48 | 49 | - name: Configure DB 50 | when: nextcloud_install_db 51 | block: 52 | - name: Configure mysql/mariadb database 53 | ansible.builtin.include_tasks: db_mysql.yml 54 | when: nextcloud_db_backend in ["mysql", "mariadb"] 55 | 56 | - name: Configure PostgreSQL database 57 | ansible.builtin.include_tasks: db_postgresql.yml 58 | when: nextcloud_db_backend in ["pgsql"] 59 | 60 | - name: Check Nextcloud installed 61 | ansible.builtin.stat: 62 | path: "{{ nextcloud_webroot }}/index.php" 63 | register: nc_nextcloud_installed 64 | 65 | - name: Downloading Nextcloud 66 | ansible.builtin.include_tasks: nc_download.yml 67 | when: not nc_nextcloud_installed.stat.exists 68 | 69 | - name: Check Nextcloud configuration exists. 70 | ansible.builtin.stat: 71 | path: "{{ nextcloud_webroot }}/config/config.php" 72 | register: nc_nextcloud_conf 73 | 74 | - name: Check Nextcloud is configured 75 | ansible.builtin.command: grep -q "{{ nextcloud_trusted_domain | first }}" {{ nextcloud_webroot }}/config/config.php 76 | failed_when: false 77 | changed_when: false 78 | register: nc_nextcloud_configured 79 | when: nc_nextcloud_conf.stat.exists 80 | 81 | - name: Nextcloud installation 82 | ansible.builtin.include_tasks: nc_installation.yml 83 | when: | 84 | (not nc_nextcloud_conf.stat.exists) or 85 | (nc_nextcloud_configured.rc is defined and nc_nextcloud_configured.rc != 0) 86 | 87 | - name: Install Nextcloud Apps 88 | when: 89 | - nextcloud_apps is defined 90 | - nextcloud_apps is mapping 91 | tags: 92 | - install_apps 93 | - patch_user_saml_app 94 | block: 95 | - name: Lists the number of apps available in the instance 96 | nextcloud.admin.run_occ: 97 | command: app:list --output=json_pretty --no-warnings 98 | nextcloud_path: "{{ nextcloud_webroot }}" 99 | become: true 100 | changed_when: false 101 | check_mode: false 102 | register: nc_apps_list 103 | 104 | - name: Convert list to yaml 105 | ansible.builtin.set_fact: 106 | nc_available_apps: "{{ nc_apps_list.stdout | from_json }}" 107 | 108 | - name: Install apps 109 | ansible.builtin.include_tasks: nc_apps.yml 110 | # do if the app is not enabled and ( (archive path is not "") or (app is disabled) ) 111 | when: 112 | - item.key not in nc_available_apps.enabled 113 | - ansible_run_tags is not search('patch_user_saml_app') 114 | with_dict: "{{ nextcloud_apps }}" 115 | 116 | 117 | - name: Add indices 118 | nextcloud.admin.run_occ: 119 | nextcloud_path: "{{ nextcloud_webroot }}" 120 | command: db:add-missing-indices 121 | become: true 122 | register: nc_indices_cmd 123 | changed_when: '"Done" not in nc_indices_cmd.stdout' 124 | when: nextcloud_install_db 125 | tags: 126 | - molecule-idempotence-notest 127 | 128 | - name: Main | Patch from the app 'user_saml' the file SAMLController.php 129 | when: 130 | - nextcloud_patch_user_saml_app 131 | - nc_available_apps.enabled['user_saml'] is defined or nc_available_apps.disabled['user_saml'] is defined 132 | tags: 133 | - patch_user_saml_app 134 | vars: 135 | pattern: '.*\$this->session->set\(.user_saml\.samlUserData.,\s+\$_SERVER\);.*' 136 | file2patch: "{{ nextcloud_webroot + '/apps/user_saml/lib/Controller/SAMLController.php' }}" 137 | block: 138 | - name: Main | Check if SAMLController.php can be patched 139 | ansible.builtin.slurp: 140 | src: "{{ file2patch }}" 141 | register: df_file 142 | 143 | - name: Main | Patch file SAMController.php 144 | ansible.builtin.blockinfile: 145 | block: "{{ lookup('ansible.builtin.file', 'files/SAMLController.patch') }}" 146 | path: "{{ file2patch }}" 147 | insertbefore: "{{ pattern }}" 148 | marker: " /** {mark} ANSIBLE MANAGED BLOCK */" 149 | when: df_file.content | b64decode | regex_search(pattern) 150 | -------------------------------------------------------------------------------- /tests/unit/modules/test_user.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch, MagicMock 3 | from ansible_collections.nextcloud.admin.plugins.modules import user 4 | from ansible.module_utils import basic 5 | from ansible_collections.nextcloud.admin.plugins.module_utils.identities import ( 6 | idState, 7 | ) 8 | 9 | 10 | class TestUserModule(TestCase): 11 | def setUp(self): 12 | self.user_id = "test_user" 13 | 14 | self.module_patcher = patch( 15 | "ansible_collections.nextcloud.admin.plugins.modules.user.AnsibleModule" 16 | ) 17 | self.mock_module = MagicMock(spec=basic.AnsibleModule) 18 | self.mock_module_obj = self.module_patcher.start() 19 | self.mock_module.check_mode = False 20 | self.mock_module_obj.return_value = self.mock_module 21 | self.mock_module.params = { 22 | "nextcloud_path": "/path/to/nextcloud", 23 | "php_runtime": "/usr/bin/php", 24 | "id": self.user_id, 25 | "state": "present", 26 | "password": "test_password", 27 | } 28 | 29 | self.user_patcher = patch( 30 | "ansible_collections.nextcloud.admin.plugins.modules.user.User" 31 | ) 32 | self.mock_user = MagicMock() 33 | self.mock_user_obj = self.user_patcher.start() 34 | 35 | self.group_patcher = patch( 36 | "ansible_collections.nextcloud.admin.plugins.modules.user.Group" 37 | ) 38 | self.mock_group_obj = self.group_patcher.start() 39 | self.mock_group_add = MagicMock() 40 | self.mock_group_add.state = idState.PRESENT 41 | self.mock_group_remove = MagicMock() 42 | self.mock_group_remove.state = idState.PRESENT 43 | self.mock_group_obj.side_effect = [self.mock_group_add, self.mock_group_remove] 44 | 45 | self.fake_result = dict( 46 | changed=False, 47 | ) 48 | self.mock_user_obj.return_value = self.mock_user 49 | 50 | def tearDown(self): 51 | self.module_patcher.stop() 52 | self.user_patcher.stop() 53 | self.group_patcher.stop() 54 | 55 | def test_user_creation(self): 56 | self.mock_user.state = idState.ABSENT 57 | self.fake_result["changed"] = True 58 | 59 | user.main() 60 | 61 | self.mock_user.add.assert_called_once_with( 62 | False, self.mock_module.params["password"], None, None, None 63 | ) 64 | self.mock_module.exit_json.assert_called_with(**self.fake_result) 65 | 66 | def test_user_creation_with_display_name(self): 67 | self.mock_module.params.update({"display_name": "Engineering Team"}) 68 | self.mock_user.state = idState.ABSENT 69 | self.fake_result["changed"] = True 70 | 71 | user.main() 72 | 73 | self.mock_user.add.assert_called_once_with( 74 | False, 75 | self.mock_module.params["password"], 76 | self.mock_module.params["display_name"], 77 | None, 78 | None, 79 | ) 80 | self.mock_module.exit_json.assert_called_with(**self.fake_result) 81 | 82 | def test_user_deletion(self): 83 | self.mock_user.state = idState.PRESENT 84 | self.mock_module.params["state"] = "absent" 85 | self.fake_result["changed"] = True 86 | 87 | user.main() 88 | 89 | self.mock_user.delete.assert_called_once() 90 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 91 | 92 | def test_no_operation_on_existing_user(self): 93 | self.mock_user.state = idState.PRESENT 94 | 95 | user.main() 96 | 97 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 98 | 99 | def test_no_operation_on_absent_user(self): 100 | self.mock_user.state = idState.ABSENT 101 | self.mock_module.params["state"] = "absent" 102 | 103 | user.main() 104 | 105 | self.mock_module.exit_json.assert_called_with(**self.fake_result) 106 | 107 | def test_user_membership_exact_match_groups_list(self): 108 | self.mock_module.params.update({"groups": ["Ateam", "admin"]}) 109 | self.mock_user.groups = ["Ateam", "Bteam"] 110 | self.mock_user.state = idState.PRESENT 111 | self.fake_result["changed"] = True 112 | 113 | user.main() 114 | 115 | self.mock_group_add.add_user.assert_called_with(self.user_id) 116 | self.mock_group_remove.remove_user.assert_called_with(self.user_id) 117 | self.mock_module.exit_json.assert_called_with(**self.fake_result) 118 | 119 | def test_ignore_missing_groups(self): 120 | self.mock_module.params.update( 121 | {"groups": ["Ateam", "admin"], "ignore_missing_groups": True} 122 | ) 123 | self.mock_user.groups = ["Ateam"] 124 | 125 | self.mock_user.state = idState.PRESENT 126 | self.mock_group_add.state = idState.ABSENT 127 | 128 | user.main() 129 | 130 | # Since ignore_missing_users is True, we expect no raise a warning and exit normally 131 | self.mock_module.warn.assert_called_once() 132 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 133 | 134 | def test_reset_password(self): 135 | self.mock_module.params.update({"reset_password": True}) 136 | self.fake_result["changed"] = True 137 | 138 | user.main() 139 | 140 | self.mock_user.reset_password.assert_called_with( 141 | self.mock_module.params["password"] 142 | ) 143 | self.mock_module.exit_json.assert_called_with(**self.fake_result) 144 | -------------------------------------------------------------------------------- /plugins/modules/group_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2025, Marc Crébassa 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | 27 | DOCUMENTATION = r""" 28 | --- 29 | module: group_list 30 | short_description: List configured groups on the server with optional group infos. 31 | author: 32 | - Marc Crébassa (@aalaesar) 33 | description: 34 | - Return the list of groups present in the server instance. 35 | - Optionally retrieve more infos 36 | - This module requires elevated privileges unless it is run as the group that owns the occ tool. 37 | extends_documentation_fragment: 38 | - nextcloud.admin.occ_common_options 39 | options: 40 | infos: 41 | description: 42 | - Whether to fetch group infos or not. 43 | aliases: [show_details] 44 | type: bool 45 | default: False 46 | limit: 47 | description: 48 | - Number of groups to retrieve. 49 | required: false 50 | type: int 51 | default: 500 52 | offset: 53 | description: 54 | - Offset for retrieving groups. 55 | required: false 56 | type: int 57 | default: 0 58 | requirements: 59 | - python >= 3.12 60 | """ 61 | 62 | EXAMPLES = r""" 63 | - name: get the list of configured groups 64 | nextcloud.admin.group_list: 65 | nextcloud_path: /var/lib/www/nextcloud 66 | register: nc_groups 67 | 68 | - name: get infos of all groups 69 | nextcloud.admin.group_list: 70 | infos: true 71 | nextcloud_path: /var/lib/www/nextcloud 72 | """ 73 | 74 | RETURN = r""" 75 | groups: 76 | description: 77 | - Dictionary of groups found in the Nextcloud instance. 78 | - The structure depends on the value of the C(infos) parameter. 79 | returned: always 80 | type: dict 81 | contains: 82 | : 83 | description: 84 | - If C(infos) is false, each key is a group_id and the value the list of members. 85 | - If C(infos) is true, each key is a group_id and the value is a dictionary containing 86 | detailed group information (e.g. displayNames, users, backend, etc.). 87 | type: raw 88 | sample: 89 | simple: 90 | admin: ["admin"] 91 | users: ["bob", "alice"] 92 | detailed: 93 | admin: 94 | displayName: "admin" 95 | backends: ["Database"] 96 | users: ["admin"] 97 | users: 98 | displayname: "Normal users" 99 | backends: ["Database"] 100 | users: ["bob", "alice"] 101 | """ 102 | 103 | from ansible.module_utils.basic import AnsibleModule 104 | from ansible_collections.nextcloud.admin.plugins.module_utils.nc_tools import ( 105 | run_occ, 106 | extend_nc_tools_args_spec, 107 | ) 108 | from ansible_collections.nextcloud.admin.plugins.module_utils.exceptions import ( 109 | OccExceptions, 110 | ) 111 | import json 112 | 113 | module_args_spec = dict( 114 | infos=dict( 115 | type="bool", 116 | required=False, 117 | aliases=["show_details"], 118 | default=False, 119 | ), 120 | limit=dict( 121 | type="int", 122 | required=False, 123 | default=500, 124 | ), 125 | offset=dict( 126 | type="int", 127 | required=False, 128 | default=0, 129 | ), 130 | ) 131 | 132 | 133 | def main(): 134 | global module 135 | module = AnsibleModule( 136 | argument_spec=extend_nc_tools_args_spec(module_args_spec), 137 | supports_check_mode=True, 138 | ) 139 | limit = module.params.get("limit", 500) 140 | offset = module.params.get("offset", 0) 141 | get_infos = module.params.get("infos") 142 | 143 | occ_command = list( 144 | filter( 145 | None, 146 | [ 147 | "group:list", 148 | "--output=json", 149 | f"--offset={offset}", 150 | f"--limit={limit}", 151 | "--info" if get_infos else None, 152 | ], 153 | ) 154 | ) 155 | 156 | try: 157 | stdout = run_occ(module, occ_command)[1] 158 | groups = json.loads(stdout) 159 | 160 | except OccExceptions as e: 161 | e.fail_json(module) 162 | except json.JSONDecodeError: 163 | module.fail_json(msg="Unable to understand the server answer.", stdout=stdout) 164 | 165 | module.exit_json( 166 | changed=False, 167 | groups=groups, 168 | ) 169 | 170 | 171 | if __name__ == "__main__": 172 | main() 173 | -------------------------------------------------------------------------------- /roles/install_nextcloud/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults file for nextcloud 3 | # [DOWNLOAD] 4 | # An URL will be generated following naming rules used by nextcloud's repository 5 | # Not following this rules correctly will make the role unable to download nextcloud. 6 | nextcloud_version_channel: "releases" # mandatory # (releases/prereleases/daily) 7 | # channel releases requires version_full. 8 | # channel prereleases requires version_full. Optional: version_special. 9 | # channel daily requires requires version_full & version_special. 10 | nextcloud_get_latest: true # mandatory # specify if the latest archive should be downloaded. 11 | # Override generated file name for channels: releases/daily. 12 | # optional : version_major. 13 | # nextcloud_version_major: 25 # (24/25/26) for releases or for daily (master/stable25/stable26...) 14 | # nextcloud_version_full: "25.0.3" # full version string 15 | # nextcloud_version_special: "" # For prereleases: "RCn/beta" or for daily "YYYY-MM-DD" 16 | nextcloud_repository: "https://download.nextcloud.com/server" # Domain URL where to download Nextcloud. 17 | nextcloud_archive_format: "zip" # zip/tar.bz2 18 | # nextcloud_full_src: "https://download.nextcloud.com/server/releases/nextcloud-25.0.0.zip" # specify directly a full URL to the archive or a path on the control host 19 | 20 | 21 | # [PHP CONFIG AND EXTENSIONS] 22 | # PHP configs 23 | # by default, use references stored in defaults/php_configs.yml 24 | nextcloud_install_php: true 25 | php_install_external_repos: true 26 | php_ver: "8.2" 27 | php_dir: "/etc/php/{{ php_ver }}" 28 | php_bin: "php-fpm{{ php_ver }}" 29 | php_pkg_spe: 30 | - "php{{ php_ver }}-bcmath" 31 | - "php{{ php_ver }}-gmp" 32 | - "php{{ php_ver }}-imagick" 33 | - "php{{ php_ver }}-mbstring" 34 | - "php{{ php_ver }}-xml" 35 | - "php{{ php_ver }}-zip" 36 | php_socket: "/run/php/php{{ php_ver }}-fpm.sock" 37 | php_memory_limit: 512M 38 | 39 | # [NEXTCLOUD CONFIG] 40 | nextcloud_trusted_domain: 41 | - "{{ ansible_fqdn }}" 42 | - "{{ ansible_default_ipv4.address }}" 43 | 44 | nextcloud_ipv6: false 45 | 46 | nextcloud_trusted_proxies: [] 47 | 48 | nextcloud_instance_name: "{{ nextcloud_trusted_domain | first }}" 49 | 50 | nextcloud_install_websrv: true 51 | nextcloud_websrv: "apache2" # "apache2"/"nginx" 52 | nextcloud_websrv_user: "{{ os_config_ref[ansible_os_family | lower].defaults.nextcloud_websrv_user }}" 53 | nextcloud_websrv_group: "{{ os_config_ref[ansible_os_family | lower].defaults.nextcloud_websrv_group }}" 54 | nextcloud_disable_websrv_default_site: false 55 | nextcloud_websrv_template: "templates/{{ nextcloud_websrv }}_nc.j2" 56 | nextcloud_webroot: "/opt/nextcloud" 57 | nextcloud_data_dir: "/var/ncdata" 58 | nextcloud_admin_name: "admin" 59 | # nextcloud_admin_pwd: "secret" 60 | 61 | nextcloud_install_redis_server: true 62 | nextcloud_redis_host: '/var/run/redis/redis.sock' 63 | nextcloud_redis_port: 0 64 | 65 | nextcloud_redis_settings: 66 | - { name: 'redis host', value: "{{ nextcloud_redis_host }}" } 67 | - { name: 'redis port', value: "{{ nextcloud_redis_port }}" } 68 | - { name: 'memcache.locking', value: '\OC\Memcache\Redis' } 69 | 70 | nextcloud_background_cron: true 71 | 72 | ## Custom nextcloud settings 73 | ## https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/config_sample_php_parameters.html 74 | nextcloud_config_settings: 75 | - { name: 'default_phone_region', value: 'DE' } # set a country code using ISO 3166-1 76 | - { name: 'overwrite.cli.url', value: 'https://{{ nextcloud_trusted_domain | first }}' } 77 | - { name: 'memcache.local', value: '\OC\Memcache\APCu' } 78 | - { name: 'mysql.utf8mb4', value: true } 79 | - { name: 'updater.release.channel', value: 'production' } # production/stable/daily/beta 80 | 81 | # [DATABASE] 82 | nextcloud_install_db: true 83 | nextcloud_db_host: "127.0.0.1" 84 | nextcloud_db_backend: "mysql" # mysql/mariadb/pgsql 85 | mysql_daemon: >- 86 | {{ 87 | os_config_ref[ansible_distribution | lower][ansible_distribution_release | lower].mysql_daemon | 88 | default(os_config_ref[ansible_distribution | lower].defaults.mysql_daemon) 89 | }} 90 | nextcloud_db_enabled_on_startup: true 91 | nextcloud_db_name: "nextcloud" 92 | nextcloud_db_admin: "ncadmin" 93 | # nextcloud_db_pwd: "secret" 94 | # Set this to `true` to forcibly update the root password. 95 | nextcloud_mysql_root_pwd_update: false 96 | 97 | # [TLS] parameters used in the apache2 & nginx templates 98 | ## max file's size allowed to be uploaded on the server 99 | nextcloud_max_upload_size: 512m # in Byte or human readable size notation (g/m/k) 100 | nextcloud_install_tls: true 101 | nextcloud_tls_enforce: true 102 | nextcloud_mozilla_modern_ssl_profile: false # when false, intermediate profile is used 103 | nextcloud_tls_cert_method: "self-signed" # self-signed/signed/installed 104 | nextcloud_tls_dhparam: "/etc/ssl/dhparam.pem" 105 | nextcloud_hsts: false # recommended >= 15552000 106 | # nextcloud_tls_cert: /path/to/cert 107 | # nextcloud_tls_cert_key: /path/to/cert/key 108 | # nextcloud_tls_cert_chain: /path/to/cert/chain 109 | # cert_path: /path/where/cert/to/copy # Default: /etc/ssl/ 110 | # nextcloud_tls_chain_file: /path/to/cert/chain/chain.pem 111 | # nextcloud_tls_cert_file: /path/to/cert/cert.crt 112 | # nextcloud_tls_cert_key_file: /path/to/cert/key/cert.key 113 | # nextcloud_tls_src_chain: /path/to/cert/chain 114 | # nextcloud_tls_src_cert: /path/to/cert 115 | # nextcloud_tls_src_cert_key: /path/to/cert/key 116 | nextcloud_tls_session_cache_size: 50m # in Byte or human readable size notation (g/m/k) 117 | 118 | # [APPS] 119 | nextcloud_apps: {} 120 | nextcloud_disable_apps: [] 121 | nextcloud_patch_user_saml_app: false # Apply Workaround to lower-case REALM for REMOTE_USER environment-variable. 122 | 123 | # [SYSTEM] 124 | # nextcloud_mysql_root_pwd: "secret" 125 | upgrade_packages_first: false 126 | cert_path: "/etc/ssl/" 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Lint status](https://github.com/nextcloud/ansible-collection-nextcloud-admin/actions/workflows/lint.yml/badge.svg)](https://github.com/nextcloud/ansible-collection-nextcloud-admin/actions?workflow=Lint) 2 | [![Tests for all supported versions](https://github.com/nextcloud/ansible-collection-nextcloud-admin/actions/workflows/tests.yml/badge.svg)](https://github.com/nextcloud/ansible-collection-nextcloud-admin/actions?workflow=Tests) 3 | [![Tests for latest](https://github.com/nextcloud/ansible-collection-nextcloud-admin/actions/workflows/tests_latest.yml/badge.svg)](https://github.com/nextcloud/ansible-collection-nextcloud-admin/actions?workflow=Tests%20latest) 4 | 5 | # Ansible collection for nextcloud administration 6 | 7 | This repository hosts the `nextcloud.admin` Ansible Collection. 8 | 9 | The collection includes a variety of Ansible content to help automate the management of nextcloud, as well as provisioning and maintenance of instances of nextcloud. 10 | 11 | 12 | ## Ansible version compatibility 13 | 14 | This collection has been tested against following Ansible versions: **>=2.15.0**. 15 | 16 | Plugins and modules within a collection may be tested with only specific Ansible versions. 17 | 18 | 19 | ## Python Support 20 | 21 | * Collection tested on 3.12+ 22 | 23 | ## Supported nextcloud version 24 | 25 | This collection supports Nextcloud versions: `30`, `31`, `32(latest)` 26 | The community makes it's best efforts to keep tested versions updated with [Nextcloud release schedule](https://github.com/nextcloud/server/wiki/Maintenance-and-Release-Schedule). 27 | 28 | ## Included content 29 | 30 | 31 | ### Modules 32 | 33 | Name | Description 34 | --- | --- 35 | nextcloud.admin.run_occ|Run the occ command line tool with given arguments. 36 | nextcloud.admin.app_info| Return state, version, updates and path of one external application. 37 | nextcloud.admin.app | Manage nextcloud external applications (install, remove, disable, etc) 38 | nextcloud.admin.user_list | List configured users on the server with optional user infos 39 | nextcloud.admin.user | short_description: Manage a Nextcloud user. 40 | nextcloud.admin.group_list | List configured groups on the server with optional group infos 41 | nextcloud.admin.group | Manage Nextcloud groups. 42 | 43 | ### Roles 44 | 45 | Name | Description 46 | --- | --- 47 | nextcloud.admin.backup (**beta**)|Create a backup of a Nextcloud server 48 | nextcloud.admin.install_nextcloud | Install and configure an Nextcloud instance for a Debian/Ubuntu server 49 | 50 | 51 | 52 | ## Installation and Usage 53 | 54 | ### Dependencies 55 | 56 | #### netaddr Python Library 57 | 58 | Content in this collection requires the [network address manipulation library](https://pypi.org/project/netaddr/) to manipulate network address. You can install it with: 59 | ``` 60 | pip3 install netaddr 61 | ``` 62 | 63 | #### required standalone roles 64 | 65 | By default, some roles in this collection are dependant of standalone roles from other namespaces. (this can be disabled). 66 | 67 | Due to some limitations, ansible-galaxy does not install them automatically, them need to be installed afterward. 68 | 69 | Once the collection is installed, run the command `ansible-galaxy role install -r /requirements.yml`. 70 | 71 | Alternatively, you can also add the content of [this file](requirements.yml) in your own `requirements.yml` file prior to installing the collection 72 | 73 | ### Installing the Collection from Ansible Galaxy 74 | 75 | Before using the nextcloud collection, you need to install it with the Ansible Galaxy CLI: 76 | 77 | ansible-galaxy collection install nextcloud.admin 78 | 79 | You can also include it in a `requirements.yml` file and install it via `ansible-galaxy collection install -r requirements.yml`, using the format: 80 | 81 | ```yaml 82 | --- 83 | collections: 84 | - name: nextcloud.admin 85 | version: 2.3.0 86 | ``` 87 | 88 | ### Using modules from the Nextcloud Collection in your playbooks 89 | 90 | It's preferable to use content in this collection using their Fully Qualified Collection Namespace (FQCN), for example `nextcloud.admin.run_occ`: 91 | 92 | ```yaml 93 | --- 94 | - hosts: nextcloud_host 95 | gather_facts: false 96 | become: true 97 | tasks: 98 | - name: list installed apps 99 | nextcloud.admin.run_occ: 100 | nextcloud_path: /var/www/nextcloud 101 | command: app:list 102 | ``` 103 | 104 | If upgrading older playbooks from <2.0.0, you can minimise your changes by defining `collections` in your play and refer to this collection's role as `install_nextcloud`, instead of `nextcloud.admin.install_nextcloud`, as in this example: 105 | 106 | ```yaml 107 | --- 108 | - hosts: localhost 109 | gather_facts: false 110 | connection: local 111 | 112 | collections: 113 | - nextcloud.admin 114 | 115 | tasks: 116 | - name: deploy nextcloud and dependencies 117 | include_role: 118 | name: install_nextcloud 119 | ``` 120 | 121 | For documentation on how to use: 122 | - __individual modules__: please use `ansible-doc` command after installing this collection. 123 | - __included roles__: as per ansible standard, ansible role are documented in their own README file. 124 | 125 | ## Testing and Development 126 | 127 | If you want to develop new content for this collection or improve what's already here, the easiest way to work on the collection is to clone it into one of the configured [`COLLECTIONS_PATHS`](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths), and work on it there. 128 | 129 | ### Testing with `molecule` 130 | 131 | The `tests` directory contains playbooks for running integration tests on various scenarios. 132 | There are also integration tests in the `molecule` directory 133 | 134 | ## Publishing New Versions 135 | 136 | Releases are automatically built and pushed to Ansible Galaxy for any new tag. 137 | 138 | ## License 139 | 140 | BSD 141 | 142 | See LICENCE to see the full text. 143 | -------------------------------------------------------------------------------- /tests/unit/modules/test_app_info.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch, MagicMock, ANY 3 | from ansible_collections.nextcloud.admin.plugins.modules import app_info 4 | from ansible.module_utils import basic 5 | 6 | testAppInfos = { 7 | "author": { 8 | "@attributes": {"mail": "skjnldsv@protonmail.com"}, 9 | "@value": "John Molakvoæ", 10 | }, 11 | "bugs": "https://github.com/nextcloud/photos/issues", 12 | "category": ["multimedia"], 13 | "commands": ["OCA\\Photos\\Command\\AlbumCreateCommand"], 14 | "dependencies": {"backend": [], "nextcloud": {}}, 15 | "description": "Your memories under your control", 16 | "id": "photos", 17 | "info": [], 18 | "licence": "agpl", 19 | "name": "Photos", 20 | "namespace": "Photos", 21 | "public": [], 22 | "remote": [], 23 | "repository": "https://github.com/nextcloud/photos.git", 24 | "summary": "Your memories under your control", 25 | "two-factor-providers": [], 26 | "version": "4.0.0-dev.1", 27 | "website": "https://github.com/nextcloud/photos", 28 | "settings": { 29 | "admin": [], 30 | "admin-section": [], 31 | "personal": [], 32 | "personal-section": [], 33 | }, 34 | } 35 | 36 | 37 | class TestAppInfoModuleWithoutSettings(TestCase): 38 | 39 | def setUp(self, show_settings=False): 40 | self.mock_module = MagicMock(spec=basic.AnsibleModule) 41 | self.expected_result = {"changed": False} 42 | self.facts_collected = { 43 | "state": "present", 44 | "is_shipped": True, 45 | "version": "1.2.3", 46 | } 47 | self.mock_module.params = {"name": "photos", "show_settings": show_settings} 48 | self.mock_module._debug = False 49 | self.mock_module._verbosity = 0 50 | self.app_infos_mock = testAppInfos 51 | self.module_patcher = patch( 52 | "ansible_collections.nextcloud.admin.plugins.modules.app_info.AnsibleModule" 53 | ) 54 | self.mock_module_class = self.module_patcher.start() 55 | self.mock_module_class.return_value = self.mock_module 56 | self.app_patcher = patch( 57 | "ansible_collections.nextcloud.admin.plugins.modules.app_info.app" 58 | ) 59 | self.mock_app_class = self.app_patcher.start() 60 | self.mock_app_class.return_value.infos = testAppInfos 61 | 62 | def tearDown(self): 63 | # Stop the patchers after each test 64 | self.module_patcher.stop() 65 | self.app_patcher.stop() 66 | 67 | def test_app_absent(self): 68 | """ 69 | Test the behavior of the app_info module when the app is absent. 70 | The expected result is that the module exits properly with a state indicating 71 | the app is not present. 72 | """ 73 | # Mocking app instance with state 'absent' 74 | mock_app_instance = self.mock_app_class.return_value 75 | mock_app_instance.get_facts.return_value = {"state": "absent"} 76 | mock_app_instance.state = "absent" 77 | 78 | app_info.main() 79 | 80 | self.mock_module.exit_json.assert_called_with( 81 | **self.expected_result, state="absent" 82 | ) 83 | 84 | def test_app_present_with_low_verbosity(self): 85 | """ 86 | Test the behavior of the app_info module when the app is present 87 | and the requested verbosity is not high. 88 | The expected result is that the module exits properly with a state indicating 89 | the app is present and display only a subset of the appInfos. 90 | """ 91 | mock_app_instance = self.mock_app_class.return_value 92 | mock_app_instance.get_facts.return_value = self.facts_collected 93 | app_info.main() 94 | 95 | self.mock_module.exit_json.assert_called_with( 96 | **self.expected_result, 97 | **self.facts_collected, 98 | AppInfos=ANY, # Ignore the contents of AppInfos here 99 | ) 100 | actual_args = self.mock_module.exit_json.call_args[1] 101 | app_infos = actual_args["AppInfos"] 102 | expected_keys = { 103 | "id", 104 | "name", 105 | "summary", 106 | "description", 107 | "licence", 108 | "author", 109 | "bugs", 110 | "category", 111 | "types", 112 | "dependencies", 113 | } 114 | self.assertEqual( 115 | set(app_infos.keys()), expected_keys 116 | ) # Check if all expected keys are present 117 | 118 | def test_app_present_with_verbosity_3_or_more(self): 119 | """ 120 | Test the behavior of the app_info module when the app is present 121 | and the requested verbosity is high. 122 | The expected result is that the module exits properly with a state indicating 123 | the app is present and display all the appInfos available. 124 | """ 125 | self.mock_module._debug = False 126 | self.mock_module._verbosity = 3 127 | mock_app_instance = self.mock_app_class.return_value 128 | mock_app_instance.get_facts.return_value = self.facts_collected 129 | 130 | app_info.main() 131 | 132 | self.mock_module.exit_json.assert_called_with( 133 | **self.expected_result, 134 | **self.facts_collected, 135 | AppInfos=testAppInfos, 136 | ) 137 | 138 | 139 | class TestAppInfoModuleWithSettings(TestAppInfoModuleWithoutSettings): 140 | def setUp(self): 141 | """ 142 | This is just a slight variation of the normal tests when the arg show_settings 143 | is set to true. 144 | Adjust expected results and facts accordingly 145 | """ 146 | super().setUp(show_settings=True) 147 | self.mock_app_class.return_value.current_settings = {} 148 | self.expected_result.update(current_settings={}) 149 | self.mock_app_class.return_value.default_settings = {} 150 | self.facts_collected.update(default_settings={}) 151 | -------------------------------------------------------------------------------- /plugins/module_utils/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2025, Marc Crébassa 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | 27 | class NextcloudException(Exception): 28 | """ 29 | Base class for all Nextcloud-related exceptions 30 | 31 | Attributes: 32 | msg (str): Human-readable message describing the error. 33 | rc (int, optional): Return code of the command that caused the exception, if applicable. 34 | stdout (str, optional): Standard output from the command. 35 | stderr (str, optional): Standard error output from the command. 36 | """ 37 | 38 | def __init__(self, msg="", rc=None, stdout=None, stderr=None, **kwargs): 39 | super().__init__(msg) 40 | self.rc = rc 41 | self.stdout = stdout 42 | self.stderr = stderr 43 | for key, value in kwargs.items(): 44 | setattr(self, key, value) 45 | 46 | def fail_json(self, module, **result): 47 | """ 48 | Fail the Ansible module with relevant debug information. 49 | 50 | Args: 51 | module: The AnsibleModule instance. 52 | result: Any extra result data to include. 53 | """ 54 | module.fail_json( 55 | msg=str(self), 56 | exception_class=type(self).__name__, 57 | **result, 58 | **self.__dict__, 59 | ) 60 | 61 | 62 | class OccExceptions(NextcloudException): 63 | """ 64 | Exception raised for errors related to occ command execution. 65 | 66 | Attributes: 67 | occ_cmd (str): The occ command that triggered the error. 68 | """ 69 | 70 | def __init__(self, occ_cmd=None, **kwargs): 71 | if "msg" not in kwargs: 72 | kwargs["msg"] = "Failure when executing provided occ command." 73 | super().__init__(**kwargs) 74 | if occ_cmd: 75 | self.occ_cmd = occ_cmd 76 | 77 | 78 | class OccFileNotFoundException(OccExceptions): 79 | """Raised when the occ command file is not found.""" 80 | 81 | pass 82 | 83 | 84 | class OccNoCommandsDefined(OccExceptions): 85 | """Raised when the command passed to occ is not defined in the Nextcloud instance.""" 86 | 87 | pass 88 | 89 | 90 | class OccOptionNotDefined(OccExceptions): 91 | """Raised when an invalid option is passed to an occ command.""" 92 | 93 | pass 94 | 95 | 96 | class OccNotEnoughArguments(OccExceptions): 97 | """Raised when not enough arguments are supplied to an occ command.""" 98 | 99 | pass 100 | 101 | 102 | class OccOptionRequiresValue(OccExceptions): 103 | """Raised when an option passed to occ requires a value but none was provided.""" 104 | 105 | pass 106 | 107 | 108 | class OccAuthenticationException(OccExceptions): 109 | """Raised when authentication fails during occ command execution.""" 110 | 111 | pass 112 | 113 | 114 | class PhpInlineExceptions(NextcloudException): 115 | """Base exception for php run in the nextcloud server.""" 116 | 117 | pass 118 | 119 | 120 | class PhpScriptException(PhpInlineExceptions): 121 | """Raised when a php script return an error.""" 122 | 123 | pass 124 | 125 | 126 | class PhpResultJsonException(PhpInlineExceptions): 127 | """Exception raised for php script results failing python's json deserialization.""" 128 | 129 | pass 130 | 131 | 132 | class AppExceptions(NextcloudException): 133 | """ 134 | Base exception for app-related errors in Nextcloud. 135 | 136 | Attributes: 137 | app_name (str): The name of the app that triggered the error. 138 | """ 139 | 140 | def __init__(self, **kwargs): 141 | if "app_name" not in kwargs: 142 | raise KeyError("Missing required 'app_name' for AppExceptions") 143 | app_name = kwargs["app_name"] 144 | if "msg" not in kwargs: 145 | if "dft_msg" in kwargs: 146 | base_msg = kwargs.pop("dft_msg") 147 | err_full_msg = f"{base_msg} for app '{app_name}'" 148 | else: 149 | err_full_msg = f"Unexpected error for app '{app_name}'" 150 | super().__init__(msg=err_full_msg, **kwargs) 151 | else: 152 | super().__init__(**kwargs) 153 | 154 | 155 | class AppPSR4InfosUnavailable(AppExceptions): 156 | """Raised when an app does not expose proper PSR4 Infos""" 157 | 158 | def __init__(self, **kwargs): 159 | super().__init__(dft_msg="PSR-4 infos not available", **kwargs) 160 | 161 | 162 | class AppPSR4InfosNotReadable(AppExceptions): 163 | """Raised when an app's getForm() method returns invalid JSON.""" 164 | 165 | def __init__(self, **kwargs): 166 | super().__init__(dft_msg="PSR-4 infos are invalid JSON", **kwargs) 167 | 168 | 169 | class IdentityNotPresent(OccExceptions): 170 | """Raised when a user or group doesn't exist while it was expected to""" 171 | 172 | def __init__(self, namespace: str, ident_id: str, **kwargs): 173 | super().__init__( 174 | msg=f"{namespace.capitalize()} {ident_id} not found.", **kwargs 175 | ) 176 | -------------------------------------------------------------------------------- /plugins/modules/app_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2023, Marc Crébassa 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | DOCUMENTATION = r""" 27 | 28 | module: app_info 29 | 30 | short_description: Display informations about an application installed in a Nextcloud server 31 | 32 | author: 33 | - "Marc Crébassa (@aalaesar)" 34 | 35 | description: 36 | - Return a set of facts about the requested application. 37 | - Always returns state as 'OK'. 38 | - This module requires to be run with advanced privileges 39 | unless it is run as the user that own the occ tool. 40 | 41 | extends_documentation_fragment: 42 | - nextcloud.admin.occ_common_options 43 | 44 | options: 45 | name: 46 | description: Collect informations for a specified nextcloud application. 47 | type: str 48 | required: true 49 | aliases: ["id"] 50 | show_settings: 51 | description: 52 | - EXPERIMENTAL. 53 | - Display application settings - default values and current values. 54 | type: bool 55 | required: false 56 | default: false 57 | 58 | requirements: 59 | - "python >=3.6" 60 | """ 61 | 62 | EXAMPLES = r""" 63 | - name: get the list of applications installed 64 | nextcloud.admin.app_info: 65 | nextcloud_path: /var/lib/www/nextcloud 66 | register: nc_apps_list 67 | 68 | - name: get configuration information about an application 69 | nextcloud.admin.app_info: 70 | nextcloud_path: /var/lib/www/nextcloud 71 | name: photos 72 | """ 73 | RETURN = r""" 74 | nextcloud_application: 75 | description: The informations collected for the application requested. 76 | returned: always 77 | type: dict 78 | contains: 79 | state: 80 | description: 81 | - Either `present`, `disabled` or `absent` when the application is installed and enabled, installed and disabled or not installed. 82 | - If `absent`, other fields are not returned. 83 | returned: always 84 | type: str 85 | is_shipped: 86 | description: If the application was shipped with the nextcloud server initially. 87 | type: bool 88 | returned: success 89 | version: 90 | description: Current version Installed for this application. 91 | type: str 92 | returned: success 93 | update_available: 94 | description: If the application has an available update. 95 | type: bool 96 | returned: success 97 | version_available: 98 | description: What is the version proposed for update. 99 | type: str 100 | returned: success 101 | app_path: 102 | description: The full path to the application folder. 103 | type: str 104 | returned: success 105 | appInfos: 106 | description: Infos exposed by the application's manifest. 107 | type: dict 108 | returned: success 109 | default_settings: 110 | description: All the application setting and their default values. 111 | type: dict 112 | returned: When show_settings is True 113 | current_settings: 114 | description: 115 | - The app config returned by the server. 116 | - Content depends on the implementation of nextcloud AppConfig APIs by the developpers. 117 | type: dict 118 | returned: When show_settings is True 119 | 120 | """ 121 | 122 | from ansible.module_utils.basic import AnsibleModule 123 | from ansible_collections.nextcloud.admin.plugins.module_utils.app import app 124 | from ansible_collections.nextcloud.admin.plugins.module_utils.nc_tools import ( 125 | extend_nc_tools_args_spec, 126 | ) 127 | from ansible_collections.nextcloud.admin.plugins.module_utils.exceptions import ( 128 | AppExceptions, 129 | ) 130 | 131 | module_arg_spec = dict( 132 | name=dict(type="str", required=True, aliases=["id"]), 133 | show_settings=dict(type="bool", required=False, default=False), 134 | ) 135 | 136 | 137 | def main(): 138 | global module 139 | 140 | module = AnsibleModule( 141 | argument_spec=extend_nc_tools_args_spec(module_arg_spec), 142 | supports_check_mode=True, 143 | ) 144 | info_keys = [ 145 | "id", 146 | "name", 147 | "summary", 148 | "description", 149 | "licence", 150 | "author", 151 | "bugs", 152 | "category", 153 | "types", 154 | "dependencies", 155 | ] 156 | result = dict(changed=False) 157 | try: 158 | nc_app = app(module, module.params.get("name")) 159 | result.update(nc_app.get_facts()) 160 | if nc_app.state != "absent": 161 | # Display all appInfo if in debug or only a small list of usefull infos. 162 | if module._debug or module._verbosity >= 3: 163 | result["AppInfos"] = nc_app.infos 164 | else: 165 | result["AppInfos"] = {k: nc_app.infos.get(k) for k in info_keys} 166 | 167 | if module.params.get("show_settings"): 168 | result["current_settings"] = nc_app.current_settings 169 | if nc_app.state != "absent": 170 | result["default_settings"] = nc_app.default_settings 171 | except AppExceptions as e: 172 | e.fail_json(module, **result) 173 | 174 | module.exit_json(**result) 175 | 176 | 177 | if __name__ == "__main__": 178 | main() 179 | -------------------------------------------------------------------------------- /tests/unit/modules/test_group.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import patch, MagicMock 3 | from ansible_collections.nextcloud.admin.plugins.modules import group 4 | from ansible.module_utils import basic 5 | from ansible_collections.nextcloud.admin.plugins.module_utils.exceptions import ( 6 | IdentityNotPresent, 7 | ) 8 | from ansible_collections.nextcloud.admin.plugins.module_utils.identities import ( 9 | idState, 10 | ) 11 | 12 | 13 | class TestGroupModule(TestCase): 14 | def setUp(self): 15 | self.group_id = "test_group" 16 | 17 | self.module_patcher = patch( 18 | "ansible_collections.nextcloud.admin.plugins.modules.group.AnsibleModule" 19 | ) 20 | self.mock_module = MagicMock(spec=basic.AnsibleModule) 21 | self.mock_module_obj = self.module_patcher.start() 22 | self.mock_module.check_mode = False 23 | self.mock_module_obj.return_value = self.mock_module 24 | self.mock_module.params = { 25 | "nextcloud_path": "/path/to/nextcloud", 26 | "php_runtime": "/usr/bin/php", 27 | "id": self.group_id, 28 | } 29 | 30 | self.group_patcher = patch( 31 | "ansible_collections.nextcloud.admin.plugins.modules.group.Group" 32 | ) 33 | self.mock_group = MagicMock() 34 | self.mock_group_obj = self.group_patcher.start() 35 | self.fake_result = dict( 36 | changed=False, 37 | added_users=[], 38 | removed_users=[], 39 | ) 40 | self.mock_group_obj.return_value = self.mock_group 41 | 42 | def tearDown(self): 43 | self.module_patcher.stop() 44 | self.group_patcher.stop() 45 | 46 | def test_group_creation(self): 47 | self.mock_group.state = idState.ABSENT 48 | self.fake_result["changed"] = True 49 | 50 | group.main() 51 | 52 | self.mock_group.add.assert_called_once_with(None) 53 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 54 | 55 | def test_group_creation_with_display_name(self): 56 | self.mock_module.params.update( 57 | {"state": "present", "display_name": "Engineering Team"} 58 | ) 59 | self.mock_group.state = idState.ABSENT 60 | self.fake_result["changed"] = True 61 | 62 | group.main() 63 | 64 | self.mock_group.add.assert_called_once_with("Engineering Team") 65 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 66 | 67 | def test_group_deletion(self): 68 | self.mock_group.state = idState.PRESENT 69 | self.mock_module.params["state"] = "absent" 70 | self.fake_result["changed"] = True 71 | 72 | group.main() 73 | 74 | self.mock_group.delete.assert_called_once() 75 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 76 | 77 | def test_no_operation_on_existing_group(self): 78 | self.mock_group.state = idState.PRESENT 79 | 80 | group.main() 81 | 82 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 83 | 84 | def test_no_operation_on_absent_group(self): 85 | self.mock_group.state = idState.ABSENT 86 | self.mock_module.params["state"] = "absent" 87 | 88 | group.main() 89 | 90 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 91 | 92 | def test_group_exact_match_users(self): 93 | self.mock_module.params.update({"users": ["alice", "bob"], "state": "present"}) 94 | self.mock_group.users = ["alice", "john"] 95 | self.mock_group.state = idState.PRESENT 96 | self.fake_result["changed"] = True 97 | self.fake_result["added_users"] = ["bob"] 98 | self.fake_result["removed_users"] = ["john"] 99 | 100 | group.main() 101 | 102 | self.mock_group.add_user.assert_called_once_with("bob") 103 | self.mock_group.remove_user.assert_called_once_with("john") 104 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 105 | 106 | def test_append_users_to_group(self): 107 | self.mock_module.params.update({"users": ["charlie"], "state": "append_users"}) 108 | self.mock_group.users = ["alice", "bob"] 109 | self.mock_group.state = idState.PRESENT 110 | self.fake_result["changed"] = True 111 | self.fake_result["added_users"] = ["charlie"] 112 | 113 | group.main() 114 | 115 | self.mock_group.add_user.assert_called_once_with("charlie") 116 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 117 | 118 | def test_remove_users_from_group(self): 119 | self.mock_module.params.update({"users": ["dave"], "state": "remove_users"}) 120 | self.mock_group.users = ["dave", "eve"] 121 | self.mock_group.state = idState.PRESENT 122 | self.fake_result["changed"] = True 123 | self.fake_result["removed_users"] = ["dave"] 124 | 125 | group.main() 126 | 127 | self.mock_group.remove_user.assert_called_once_with("dave") 128 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 129 | 130 | def test_ignore_missing_users_on_add(self): 131 | self.mock_module.params.update( 132 | {"users": ["frank"], "ignore_missing_users": True, "state": "append_users"} 133 | ) 134 | self.mock_group.users = ["alice"] 135 | self.mock_group.state = idState.PRESENT 136 | self.mock_group.add_user.side_effect = IdentityNotPresent("user", "frank") 137 | 138 | group.main() 139 | 140 | # Since ignore_missing_users is True, we expect no raise and exit normally 141 | self.mock_module.exit_json.assert_called_once_with(**self.fake_result) 142 | 143 | def test_fail_on_missing_users_on_add(self): 144 | self.mock_module.params.update( 145 | { 146 | "users": ["george"], 147 | "ignore_missing_users": False, 148 | "state": "append_users", 149 | } 150 | ) 151 | self.mock_group.users = ["alice"] 152 | self.mock_group.state = idState.PRESENT 153 | self.mock_group.add_user.side_effect = IdentityNotPresent("user", "george") 154 | 155 | group.main() 156 | 157 | self.mock_module.fail_json.assert_called_once() 158 | 159 | def test_error_on_missing_group_with_users(self): 160 | self.mock_module.params.update( 161 | {"users": ["harry"], "error_on_missing": True, "state": "present"} 162 | ) 163 | self.mock_group.state = idState.ABSENT 164 | 165 | group.main() 166 | 167 | self.mock_module.fail_json.assert_called_once_with( 168 | msg=f"Group {self.group_id} is absent while trying to manage its users." 169 | ) 170 | -------------------------------------------------------------------------------- /plugins/modules/user_list.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2025, Marc Crébassa 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | # DOCUMENTATION = r""" 27 | 28 | # module: user_list 29 | 30 | # short_description: List configured users on the server with optional user infos. 31 | 32 | # author: 33 | # - "Marc Crébassa (@aalaesar)" 34 | 35 | # description: 36 | # - Return the list of users present in the server instance 37 | # - Optionally recovery infos of one or more user 38 | # - This module requires to be run with advanced privileges 39 | # unless it is run as the user that own the occ tool. 40 | 41 | # extends_documentation_fragment: 42 | # - nextcloud.admin.occ_common_options 43 | 44 | # options: 45 | # infos: 46 | # description: 47 | # - Weither to fetch user infos or not 48 | # aliases: ['show_details'] 49 | # type: bool 50 | 51 | # limit: 52 | # description: 53 | # - Number of users to retrieve 54 | # required: false 55 | # type: int 56 | # default: 500 57 | 58 | # offset: 59 | # description: 60 | # - Offset for retrieving users 61 | # required: false 62 | # type: int 63 | # default: 0 64 | 65 | # requirements: 66 | # - "python >=3.12" 67 | # """ 68 | 69 | DOCUMENTATION = r""" 70 | --- 71 | module: user_list 72 | short_description: List configured users on the server with optional user infos. 73 | author: 74 | - Marc Crébassa (@aalaesar) 75 | description: 76 | - Return the list of users present in the server instance. 77 | - Optionally retrieve infos of one or more users. 78 | - This module requires elevated privileges unless it is run as the user that owns the occ tool. 79 | extends_documentation_fragment: 80 | - nextcloud.admin.occ_common_options 81 | options: 82 | infos: 83 | description: 84 | - Whether to fetch user infos or not. 85 | aliases: [show_details] 86 | type: bool 87 | default: False 88 | limit: 89 | description: 90 | - Number of users to retrieve. 91 | required: false 92 | type: int 93 | default: 500 94 | offset: 95 | description: 96 | - Offset for retrieving users. 97 | required: false 98 | type: int 99 | default: 0 100 | requirements: 101 | - python >= 3.12 102 | """ 103 | 104 | EXAMPLES = r""" 105 | - name: get the list of configured users 106 | nextcloud.admin.user_list: 107 | nextcloud_path: /var/lib/www/nextcloud 108 | register: nc_users 109 | 110 | - name: get infos of all users 111 | nextcloud.admin.user_list: 112 | infos: true 113 | nextcloud_path: /var/lib/www/nextcloud 114 | """ 115 | 116 | RETURN = r""" 117 | users: 118 | description: 119 | - Dictionary of users found in the Nextcloud instance. 120 | - The structure depends on the value of the C(infos) parameter. 121 | returned: always 122 | type: dict 123 | contains: 124 | : 125 | description: 126 | - If C(infos) is false, each key is a user_id and the value its display name. 127 | - If C(infos) is true, each key is a user_id and the value is a dictionary containing detailed user information (e.g. email, quota, last login, etc.). 128 | type: raw 129 | sample: 130 | simple: 131 | alice: "Alice Dupont" 132 | bob: "Bob Martin" 133 | detailed: 134 | alice: 135 | email: "alice@example.com" 136 | displayname: "Alice Dupont" 137 | quota: "1 GB" 138 | bob: 139 | email: "bob@example.com" 140 | displayname: "Bob Martin" 141 | quota: "500 MB" 142 | """ 143 | 144 | from ansible.module_utils.basic import AnsibleModule 145 | from ansible_collections.nextcloud.admin.plugins.module_utils.nc_tools import ( 146 | run_occ, 147 | extend_nc_tools_args_spec, 148 | ) 149 | from ansible_collections.nextcloud.admin.plugins.module_utils.exceptions import ( 150 | OccExceptions, 151 | ) 152 | import json 153 | 154 | module_args_spec = dict( 155 | infos=dict( 156 | type="bool", 157 | required=False, 158 | aliases=["show_details"], 159 | default=False, 160 | ), 161 | limit=dict( 162 | type="int", 163 | required=False, 164 | default=500, 165 | ), 166 | offset=dict( 167 | type="int", 168 | required=False, 169 | default=0, 170 | ), 171 | ) 172 | 173 | 174 | def main(): 175 | global module 176 | module = AnsibleModule( 177 | argument_spec=extend_nc_tools_args_spec(module_args_spec), 178 | supports_check_mode=True, 179 | ) 180 | limit = module.params.get("limit", 500) 181 | offset = module.params.get("offset", 0) 182 | get_infos = module.params.get("infos") 183 | 184 | occ_command = list( 185 | filter( 186 | None, 187 | [ 188 | "user:list", 189 | "--output=json", 190 | f"--offset={offset}", 191 | f"--limit={limit}", 192 | "--info" if get_infos else None, 193 | ], 194 | ) 195 | ) 196 | 197 | try: 198 | stdout = run_occ(module, occ_command)[1] 199 | users = json.loads(stdout) 200 | 201 | except OccExceptions as e: 202 | e.fail_json(module) 203 | except json.JSONDecodeError: 204 | module.fail_json(msg="Unable to understand the server answer.", stdout=stdout) 205 | 206 | module.exit_json( 207 | changed=False, 208 | users=users, 209 | ) 210 | 211 | 212 | if __name__ == "__main__": 213 | main() 214 | -------------------------------------------------------------------------------- /roles/install_nextcloud/tasks/nc_installation.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ######### 3 | # Run command line installation. 4 | # the web server must be running by now in order to launch the installation 5 | - name: nc_installation | Trigger all pending handlers 6 | ansible.builtin.meta: flush_handlers 7 | 8 | - name: nc_installation | Set directory ownership & permissions for the data folder 9 | ansible.builtin.file: 10 | path: "{{ nextcloud_data_dir }}" 11 | mode: "u=rwX,g=rX,o-rwx" 12 | recurse: true 13 | state: directory 14 | owner: "{{ nextcloud_websrv_user }}" 15 | group: "{{ nextcloud_websrv_group }}" 16 | 17 | - name: "nc_installation | Generate password {{ nextcloud_admin_name }}" 18 | ansible.builtin.set_fact: 19 | nextcloud_admin_pwd: "{{ lookup('password', 'nextcloud_instances/' + nextcloud_instance_name + '/web_admin.pwd') }}" 20 | when: nextcloud_admin_pwd is not defined 21 | 22 | - name: nc_installation | Set temporary permissions for command line installation 23 | ansible.builtin.file: 24 | path: "{{ nextcloud_webroot }}" 25 | state: directory 26 | recurse: true 27 | owner: "{{ nextcloud_websrv_user }}" 28 | group: "{{ nextcloud_websrv_group }}" 29 | 30 | - name: nc_installation | Configuration 31 | block: 32 | - name: nc_installation | Remove possibly old or incomplete config.php 33 | ansible.builtin.file: 34 | path: "{{ nextcloud_webroot }}/config/config.php" 35 | state: absent 36 | 37 | - name: nc_installation | Run occ installation command 38 | become: true 39 | nextcloud.admin.run_occ: 40 | nextcloud_path: "{{ nextcloud_webroot }}" 41 | command: >- 42 | maintenance:install 43 | --database {{ nextcloud_tmp_backend }} 44 | --database-host {{ nextcloud_db_host }} 45 | --database-name {{ nextcloud_db_name }} 46 | --database-user {{ nextcloud_db_admin }} 47 | --database-pass {{ nextcloud_db_pwd }} 48 | --admin-user {{ nextcloud_admin_name }} 49 | --admin-pass {{ nextcloud_admin_pwd }} 50 | --data-dir {{ nextcloud_data_dir }} 51 | vars: 52 | # mariadb is equal to mysql for occ 53 | nextcloud_tmp_backend: "{{ 'mysql' if nextcloud_db_backend == 'mariadb' else nextcloud_db_backend }}" 54 | notify: Reload http 55 | 56 | - name: nc_installation | Verify config.php - check filesize 57 | ansible.builtin.stat: 58 | path: "{{ nextcloud_webroot }}/config/config.php" 59 | register: nc_installation_confsize 60 | failed_when: nc_installation_confsize.stat.size is undefined or nc_installation_confsize.stat.size <= 100 61 | 62 | - name: nc_installation | Verify config.php - php syntax check 63 | ansible.builtin.command: "php -l {{ nextcloud_webroot }}/config/config.php" 64 | register: nc_installation_confphp 65 | changed_when: false 66 | failed_when: 67 | - nc_installation_confphp.rc is defined 68 | - nc_installation_confphp.rc != 0 69 | 70 | rescue: 71 | # - name: Unfix su issue with occ 72 | # include_tasks: tasks/unfix_su.yml 73 | # when: ansible_become_method == "su" 74 | - name: nc_installation | Remove config.php when occ fail 75 | ansible.builtin.file: 76 | path: "{{ nextcloud_webroot }}/config/config.php" 77 | state: absent 78 | failed_when: true 79 | 80 | - name: nc_installation | Set Trusted Domains 81 | become: true 82 | nextcloud.admin.run_occ: 83 | nextcloud_path: "{{ nextcloud_webroot }}" 84 | command: >- 85 | config:system:set 86 | trusted_domains {{ item.0 }} 87 | --value={{ item.1 | ansible.utils.ipwrap }} 88 | with_indexed_items: "{{ nextcloud_trusted_domain }}" 89 | 90 | - name: nc_installation | Set Trusted Proxies 91 | become: true 92 | nextcloud.admin.run_occ: 93 | nextcloud_path: "{{ nextcloud_webroot }}" 94 | command: >- 95 | config:system:set 96 | trusted_proxies {{ item.0 }} 97 | --value={{ item.1 }} 98 | with_indexed_items: "{{ nextcloud_trusted_proxies }}" 99 | 100 | - name: nc_installation | Set Nextcloud settings in config.php 101 | become: true 102 | nextcloud.admin.run_occ: 103 | nextcloud_path: "{{ nextcloud_webroot }}" 104 | command: >- 105 | config:system:set 106 | {{ item.name }} 107 | --value {{ item.value }}{% if item.value | type_debug == 'bool' %} --type=boolean{% endif %} 108 | with_items: 109 | - "{{ nextcloud_config_settings }}" 110 | 111 | - name: nc_installation | Set Redis Server 112 | become: true 113 | nextcloud.admin.run_occ: 114 | nextcloud_path: "{{ nextcloud_webroot }}" 115 | command: >- 116 | config:system:set {{ item.name }} 117 | --value {{ item.value }}{% if item.value | type_debug == 'bool' %} --type=boolean{% endif %} 118 | with_items: 119 | - "{{ nextcloud_redis_settings }}" 120 | when: (nextcloud_install_redis_server | bool) 121 | 122 | - name: nc_installation | Configure Cron 123 | when: (nextcloud_background_cron | bool) 124 | block: 125 | - name: nc_installation | Check Cron package 126 | ansible.builtin.package: 127 | name: "cron" 128 | state: present 129 | 130 | - name: nc_installation | Install Cronjob 131 | ansible.builtin.cron: 132 | name: "Nextcloud Cronjob" 133 | minute: "*/10" 134 | user: "{{ nextcloud_websrv_user }}" 135 | job: "php {{ nextcloud_webroot }}/cron.php" 136 | cron_file: "nextcloud" 137 | 138 | - name: nc_installation | Set Cron method to Crontab 139 | become: true 140 | nextcloud.admin.run_occ: 141 | nextcloud_path: "{{ nextcloud_webroot }}" 142 | command: background:cron 143 | when: (nextcloud_background_cron | bool) 144 | 145 | - name: nc_installation | Set Custom Mimetype 146 | ansible.builtin.copy: 147 | dest: "{{ nextcloud_webroot }}/config/mimetypemapping.json" 148 | src: files/nextcloud_custom_mimetypemapping.json 149 | mode: 0640 150 | 151 | - name: nc_installation | Ensure Nextcloud directories are 0750 152 | ansible.builtin.command: find {{ nextcloud_data_dir }} -type d -exec chmod -c 0750 {} \; 153 | register: nc_installation_chmod_result 154 | changed_when: "nc_installation_chmod_result.stdout != \"\"" 155 | 156 | - name: nc_installation | Ensure Nextcloud files are 0640 157 | ansible.builtin.command: find {{ nextcloud_data_dir }} -type f -exec chmod -c 0640 {} \; 158 | register: nc_installation_chmod_result 159 | changed_when: "nc_installation_chmod_result.stdout != \"\"" 160 | 161 | - name: nc_installation | Set stronger directory ownership 162 | ansible.builtin.file: 163 | path: "{{ nextcloud_webroot }}/{{ item }}/" 164 | recurse: true 165 | owner: "{{ nextcloud_websrv_user }}" 166 | group: "{{ nextcloud_websrv_group }}" 167 | state: directory 168 | with_items: 169 | - apps 170 | - config 171 | - themes 172 | - updater 173 | 174 | - name: nc_installation | Disable Nextcloud apps 175 | nextcloud.admin.app: 176 | nextcloud_path: "{{ nextcloud_webroot }}" 177 | state: disabled 178 | name: "{{ item }}" 179 | become: true 180 | with_items: "{{ nextcloud_disable_apps }}" 181 | -------------------------------------------------------------------------------- /plugins/modules/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2023, Marc Crébassa 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | DOCUMENTATION = r""" 27 | 28 | module: app 29 | 30 | short_description: Manage external applications in a Nextcloud server 31 | 32 | author: 33 | - "Marc Crébassa (@aalaesar)" 34 | 35 | description: 36 | - Install, remove, disable, update external applications in a Nextcloud instance 37 | - This module requires to be run with advanced privileges 38 | unless it is run as the user that own the occ tool. 39 | 40 | extends_documentation_fragment: 41 | - nextcloud.admin.occ_common_options 42 | 43 | options: 44 | name: 45 | description: 46 | - Manage the following nextcloud application from the [nextcloud app store](https://apps.nextcloud.com/). 47 | - Attention! Use the application `technical` name (available at the end of the app's page url). 48 | type: str 49 | required: true 50 | aliases: 51 | - "id" 52 | 53 | state: 54 | description: 55 | - The desired state for the application. 56 | - If set to `present`, the application is installed and set to enabled. 57 | - If set to `absent` or `removed`, the application will be removed. 58 | - If set to `disabled`, disable the application if it is present or else install it _but do not enable it_. 59 | - If set to `updated`, equivalent to `present` if the app is absent or update to the version proposed by the server. 60 | type: str 61 | choices: 62 | - "present" 63 | - "absent" 64 | - "removed" 65 | - "disabled" 66 | - "updated" 67 | default: "present" 68 | required: false 69 | aliases: 70 | - "status" 71 | 72 | requirements: 73 | - "python >=3.6" 74 | """ 75 | 76 | EXAMPLES = r""" 77 | - name: Enable preinstalled contact application 78 | nextcloud.admin.app: 79 | id: contacts 80 | state: present 81 | nextcloud_path: /var/lib/www/nextcloud 82 | 83 | - name: Update calendar application 84 | nextcloud.admin.app: 85 | name: calendar 86 | state: updated 87 | nextcloud_path: /var/lib/www/nextcloud 88 | """ 89 | 90 | RETURN = r""" 91 | actions_taken: 92 | description: The informations collected for the application requested. 93 | returned: always 94 | type: list 95 | contains: 96 | state: 97 | description: 98 | - Action taken and reported by the nextcloud server. 99 | returned: always 100 | type: str 101 | version: 102 | description: App version present or updated on the server. 103 | returned: always 104 | type: str 105 | miscellaneous: 106 | description: Informative messages sent by the server during app operation. 107 | returned: when not empty 108 | type: list 109 | contains: 110 | misc: 111 | description: Something reported by the server. 112 | type: str 113 | """ 114 | 115 | from ansible.module_utils.basic import AnsibleModule 116 | from ansible_collections.nextcloud.admin.plugins.module_utils.app import app 117 | from ansible_collections.nextcloud.admin.plugins.module_utils.exceptions import ( 118 | AppExceptions, 119 | ) 120 | from ansible_collections.nextcloud.admin.plugins.module_utils.nc_tools import ( 121 | extend_nc_tools_args_spec, 122 | ) 123 | 124 | module_args_spec = dict( 125 | name=dict(type="str", aliases=["id"], required=True), 126 | state=dict( 127 | type="str", 128 | required=False, 129 | default="present", 130 | choices=["present", "absent", "removed", "disabled", "updated"], 131 | aliases=["status"], 132 | ), 133 | ) 134 | 135 | 136 | def main(): 137 | global module 138 | misc_msg = [] 139 | result = dict( 140 | actions_taken=[], 141 | version=None, 142 | ) 143 | module = AnsibleModule( 144 | argument_spec=extend_nc_tools_args_spec(module_args_spec), 145 | supports_check_mode=True, 146 | ) 147 | app_name = module.params.get("name") 148 | target_state = module.params.get("state", "present") 149 | nc_app = app(module, app_name) 150 | # case1: switch between enable/disable status 151 | if (target_state == "disabled" and nc_app.state == "present") or ( 152 | target_state == "present" and nc_app.state == "disabled" 153 | ): 154 | if module.check_mode: 155 | result["actions_taken"] = [target_state] 156 | else: 157 | try: 158 | actions_taken, misc_msg = nc_app.toggle() 159 | result["actions_taken"].extend(actions_taken) 160 | except AppExceptions as e: 161 | e.fail_json(module, **result) 162 | # case2: install and maybe enable the application 163 | elif ( 164 | target_state in ["present", "updated", "disabled"] and nc_app.state == "absent" 165 | ): 166 | enable = target_state != "disabled" 167 | if module.check_mode: 168 | result["version"] = "undefined in check mode" 169 | result["actions_taken"] = ["installed"] 170 | if enable: 171 | result["actions_taken"].append("enabled") 172 | else: 173 | try: 174 | actions_taken, misc_msg = nc_app.install(enable=enable) 175 | result["actions_taken"].extend(actions_taken) 176 | result["version"] = nc_app.version 177 | except AppExceptions as e: 178 | e.fail_json(module, **result) 179 | # case3: remove the application 180 | elif target_state in ["absent", "removed"] and nc_app.state in [ 181 | "disabled", 182 | "present", 183 | ]: 184 | result["version"] = nc_app.version 185 | if module.check_mode: 186 | result["actions_taken"] = ["removed"] 187 | if nc_app.state == "present": 188 | result["actions_taken"].insert(0, "disabled") 189 | else: 190 | try: 191 | actions_taken, misc_msg = nc_app.remove() 192 | result["actions_taken"].extend(actions_taken) 193 | except AppExceptions as e: 194 | e.fail_json(module, **result) 195 | # case3: update the application if posible 196 | elif target_state == "updated" and nc_app.state == "present": 197 | if nc_app.update_available: 198 | if module.check_mode: 199 | result["actions_taken"].append("updated") 200 | result["version"] = nc_app.update_version_available 201 | else: 202 | try: 203 | previous_version, result["version"] = nc_app.update() 204 | result["actions_taken"].append("updated") 205 | except AppExceptions as e: 206 | e.fail_json(module, **result) 207 | if misc_msg: 208 | result.update(miscellaneous=misc_msg) 209 | result.update(changed=bool(result["actions_taken"])) 210 | if not result["version"]: 211 | result["version"] = nc_app.version 212 | module.exit_json(**result) 213 | 214 | 215 | if __name__ == "__main__": 216 | main() 217 | -------------------------------------------------------------------------------- /roles/install_nextcloud/files/nextcloud_custom_mimetypemapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment" : "This file was generated by Ansible, Do NOT modify this file by hand!", 3 | 4 | "3gp": ["video/3gpp"], 5 | "7z": ["application/x-7z-compressed"], 6 | "accdb": ["application/msaccess"], 7 | "ai": ["application/illustrator"], 8 | "apk": ["application/vnd.android.package-archive"], 9 | "arw": ["image/x-dcraw"], 10 | "avi": ["video/x-msvideo"], 11 | "bash": ["text/x-shellscript"], 12 | "blend": ["application/x-blender"], 13 | "bin": ["application/x-bin"], 14 | "bmp": ["image/bmp"], 15 | "bpg": ["image/bpg"], 16 | "bz2": ["application/x-bzip2"], 17 | "cb7": ["application/x-cbr"], 18 | "cba": ["application/x-cbr"], 19 | "cbr": ["application/x-cbr"], 20 | "cbt": ["application/x-cbr"], 21 | "cbtc": ["application/x-cbr"], 22 | "cbz": ["application/x-cbr"], 23 | "cc": ["text/x-c"], 24 | "cdr": ["application/coreldraw"], 25 | "class": ["application/java"], 26 | "cnf": ["text/plain"], 27 | "conf": ["text/plain"], 28 | "cpp": ["text/x-c++src"], 29 | "cr2": ["image/x-dcraw"], 30 | "css": ["text/css"], 31 | "csv": ["text/csv"], 32 | "cvbdl": ["application/x-cbr"], 33 | "c": ["text/x-c"], 34 | "c++": ["text/x-c++src"], 35 | "dcr": ["image/x-dcraw"], 36 | "deb": ["application/x-deb"], 37 | "dng": ["image/x-dcraw"], 38 | "doc": ["application/msword"], 39 | "docm": ["application/vnd.ms-word.document.macroEnabled.12"], 40 | "docx": ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"], 41 | "dot": ["application/msword"], 42 | "dotx": ["application/vnd.openxmlformats-officedocument.wordprocessingml.template"], 43 | "dv": ["video/dv"], 44 | "eot": ["application/vnd.ms-fontobject"], 45 | "epub": ["application/epub+zip"], 46 | "eps": ["application/postscript"], 47 | "erf": ["image/x-dcraw"], 48 | "exe": ["application/x-ms-dos-executable"], 49 | "fb2": ["application/x-fictionbook+xml", "text/plain"], 50 | "flac": ["audio/flac"], 51 | "flv": ["video/x-flv"], 52 | "gif": ["image/gif"], 53 | "gz": ["application/x-gzip"], 54 | "gzip": ["application/x-gzip"], 55 | "h": ["text/x-h"], 56 | "hh": ["text/x-h"], 57 | "hpp": ["text/x-h"], 58 | "html": ["text/html", "text/plain"], 59 | "htm": ["text/html", "text/plain"], 60 | "ical": ["text/calendar"], 61 | "ics": ["text/calendar"], 62 | "iiq": ["image/x-dcraw"], 63 | "impress": ["text/impress"], 64 | "java": ["text/x-java-source"], 65 | "jpeg": ["image/jpeg"], 66 | "jpg": ["image/jpeg"], 67 | "jps": ["image/jpeg"], 68 | "js": ["application/javascript", "text/plain"], 69 | "json": ["application/json", "text/plain"], 70 | "k25": ["image/x-dcraw"], 71 | "kdc": ["image/x-dcraw"], 72 | "key": ["application/x-iwork-keynote-sffkey"], 73 | "keynote": ["application/x-iwork-keynote-sffkey"], 74 | "kra": ["application/x-krita"], 75 | "lwp": ["application/vnd.lotus-wordpro"], 76 | "m2t": ["video/mp2t"], 77 | "m4a": ["audio/mp4"], 78 | "m4b": ["audio/m4b"], 79 | "m4v": ["video/mp4"], 80 | "markdown": ["text/markdown"], 81 | "mdown": ["text/markdown"], 82 | "md": ["text/markdown"], 83 | "mdb": ["application/msaccess"], 84 | "mdwn": ["text/markdown"], 85 | "mkd": ["text/markdown"], 86 | "mef": ["image/x-dcraw"], 87 | "mkv": ["video/x-matroska"], 88 | "mobi": ["application/x-mobipocket-ebook"], 89 | "mov": ["video/quicktime"], 90 | "mp3": ["audio/mpeg"], 91 | "mp4": ["video/mp4"], 92 | "mpeg": ["video/mpeg"], 93 | "mpg": ["video/mpeg"], 94 | "mpo": ["image/jpeg"], 95 | "msi": ["application/x-msi"], 96 | "mts": ["video/MP2T"], 97 | "mt2s": ["video/MP2T"], 98 | "nef": ["image/x-dcraw"], 99 | "numbers": ["application/x-iwork-numbers-sffnumbers"], 100 | "odf": ["application/vnd.oasis.opendocument.formula"], 101 | "odg": ["application/vnd.oasis.opendocument.graphics"], 102 | "odp": ["application/vnd.oasis.opendocument.presentation"], 103 | "ods": ["application/vnd.oasis.opendocument.spreadsheet"], 104 | "odt": ["application/vnd.oasis.opendocument.text"], 105 | "oga": ["audio/ogg"], 106 | "ogg": ["audio/ogg"], 107 | "ogv": ["video/ogg"], 108 | "one": ["application/msonenote"], 109 | "opus": ["audio/ogg"], 110 | "orf": ["image/x-dcraw"], 111 | "otf": ["application/font-sfnt"], 112 | "pad": ["application/x-ownpad"], 113 | "calc": ["application/x-ownpad"], 114 | "pages": ["application/x-iwork-pages-sffpages"], 115 | "pdf": ["application/pdf"], 116 | "pfb": ["application/x-font"], 117 | "pef": ["image/x-dcraw"], 118 | "php": ["application/x-php"], 119 | "pl": ["application/x-perl"], 120 | "png": ["image/png"], 121 | "pot": ["application/vnd.ms-powerpoint"], 122 | "potm": ["application/vnd.ms-powerpoint.template.macroEnabled.12"], 123 | "potx": ["application/vnd.openxmlformats-officedocument.presentationml.template"], 124 | "ppa": ["application/vnd.ms-powerpoint"], 125 | "ppam": ["application/vnd.ms-powerpoint.addin.macroEnabled.12"], 126 | "pps": ["application/vnd.ms-powerpoint"], 127 | "ppsm": ["application/vnd.ms-powerpoint.slideshow.macroEnabled.12"], 128 | "ppsx": ["application/vnd.openxmlformats-officedocument.presentationml.slideshow"], 129 | "ppt": ["application/vnd.ms-powerpoint"], 130 | "pptm": ["application/vnd.ms-powerpoint.presentation.macroEnabled.12"], 131 | "pptx": ["application/vnd.openxmlformats-officedocument.presentationml.presentation"], 132 | "ps": ["application/postscript"], 133 | "psd": ["application/x-photoshop"], 134 | "py": ["text/x-python"], 135 | "raf": ["image/x-dcraw"], 136 | "rar": ["application/x-rar-compressed"], 137 | "reveal": ["text/reveal"], 138 | "rss": ["application/rss+xml"], 139 | "rtf": ["text/rtf"], 140 | "rw2": ["image/x-dcraw"], 141 | "sgf": ["application/sgf"], 142 | "sh-lib": ["text/x-shellscript"], 143 | "sh": ["text/x-shellscript"], 144 | "srf": ["image/x-dcraw"], 145 | "sr2": ["image/x-dcraw"], 146 | "svg": ["image/svg+xml", "text/plain"], 147 | "swf": ["application/x-shockwave-flash", "application/octet-stream"], 148 | "tar": ["application/x-tar"], 149 | "tar.bz2": ["application/x-bzip2"], 150 | "tar.gz": ["application/x-compressed"], 151 | "tbz2": ["application/x-bzip2"], 152 | "tex": ["application/x-tex"], 153 | "tgz": ["application/x-compressed"], 154 | "tiff": ["image/tiff"], 155 | "tif": ["image/tiff"], 156 | "ttf": ["application/font-sfnt"], 157 | "txt": ["text/plain"], 158 | "vcard": ["text/vcard"], 159 | "vcf": ["text/vcard"], 160 | "vob": ["video/dvd"], 161 | "vsd": ["application/vnd.visio"], 162 | "wav": ["audio/wav"], 163 | "webm": ["video/webm"], 164 | "woff": ["application/font-woff"], 165 | "wpd": ["application/vnd.wordperfect"], 166 | "wmv": ["video/x-ms-wmv"], 167 | "xcf": ["application/x-gimp"], 168 | "xla": ["application/vnd.ms-excel"], 169 | "xlam": ["application/vnd.ms-excel.addin.macroEnabled.12"], 170 | "xls": ["application/vnd.ms-excel"], 171 | "xlsb": ["application/vnd.ms-excel.sheet.binary.macroEnabled.12"], 172 | "xlsm": ["application/vnd.ms-excel.sheet.macroEnabled.12"], 173 | "xlsx": ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], 174 | "xlt": ["application/vnd.ms-excel"], 175 | "xltm": ["application/vnd.ms-excel.template.macroEnabled.12"], 176 | "xltx": ["application/vnd.openxmlformats-officedocument.spreadsheetml.template"], 177 | "xml": ["application/xml", "text/plain"], 178 | "xrf": ["image/x-dcraw"], 179 | "yaml": ["application/yaml", "text/plain"], 180 | "yml": ["application/yaml", "text/plain"], 181 | "zip": ["application/zip"] 182 | } 183 | -------------------------------------------------------------------------------- /plugins/module_utils/nc_tools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2022, Marc Crébassa 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | import os 27 | from multiprocessing import Process, Pipe 28 | import json 29 | from textwrap import dedent 30 | from ansible_collections.nextcloud.admin.plugins.module_utils.exceptions import ( 31 | OccExceptions, 32 | OccAuthenticationException, 33 | OccFileNotFoundException, 34 | OccNoCommandsDefined, 35 | OccNotEnoughArguments, 36 | OccOptionRequiresValue, 37 | OccOptionNotDefined, 38 | PhpInlineExceptions, 39 | PhpScriptException, 40 | PhpResultJsonException, 41 | ) 42 | from shlex import shlex 43 | import copy 44 | 45 | 46 | NC_TOOLS_ARGS_SPEC = dict( 47 | nextcloud_path=dict( 48 | type="str", required=True, aliases=["path", "nc_path", "nc_dir"] 49 | ), 50 | php_runtime=dict(type="str", required=False, default="php", aliases=["php"]), 51 | ) 52 | 53 | 54 | def extend_nc_tools_args_spec(some_module_spec): 55 | arg_spec = copy.deepcopy(NC_TOOLS_ARGS_SPEC) 56 | arg_spec.update(some_module_spec) 57 | return arg_spec 58 | 59 | 60 | def convert_string(command: str) -> list: 61 | command_lex = shlex(command, posix=False) 62 | command_lex.whitespace_split = True 63 | command_lex.commenters = "" 64 | command_lex.escape = "" 65 | command_lex.quotes = "\"'" 66 | return [token if " " in token else token.strip("\"'") for token in command_lex] 67 | 68 | 69 | def execute_occ_command(conn, module, php_exec, command, **kwargs): 70 | """ 71 | Execute a given occ command using the PHP interpreter and handle user permissions. 72 | 73 | This function attempts to runs occ through the PHP interpreter with 74 | the appropriate user permissions. It checks if the current user has the same 75 | UID as the owner of the occ file, switches to that user if necessary, 76 | and runs the command. The output of the command execution along with any 77 | errors are sent back via the provided connection object. 78 | 79 | Parameters: 80 | - conn (multiprocessing.connection.Connection): The connection object used for communication. 81 | - module (AnsibleModule): An object providing methods for running commands. 82 | - php_exec (str): The path to the PHP executable. 83 | - command (list): A list where the first element is 'occ' with its full path. 84 | 85 | Raises: 86 | - OccFileNotFoundException: If the command file does not exist. 87 | - OccAuthenticationException: If there are insufficient permissions to switch the user. 88 | 89 | Returns: 90 | None: This function does not return anything. It sends the results or exceptions through the conn object. 91 | """ 92 | try: 93 | cli_stats = os.stat(command[0]) 94 | if os.getuid() != cli_stats.st_uid: 95 | os.setgid(cli_stats.st_gid) 96 | os.setuid(cli_stats.st_uid) 97 | 98 | rc, stdout, stderr = module.run_command([php_exec] + command, **kwargs) 99 | conn.send({"rc": rc, "stdout": stdout, "stderr": stderr}) 100 | except FileNotFoundError: 101 | conn.send({"exception": "OccFileNotFoundException"}) 102 | except PermissionError: 103 | conn.send( 104 | { 105 | "exception": "OccAuthenticationException", 106 | "msg": f"Insufficient permissions to switch to user id {cli_stats.st_uid}.", 107 | } 108 | ) 109 | except Exception as e: 110 | conn.send({"exception": str(e)}) 111 | finally: 112 | conn.close() 113 | 114 | 115 | def run_occ(module, command, **kwargs): 116 | cli_full_path = module.params.get("nextcloud_path") + "/occ" 117 | php_exec = module.params.get("php_runtime") 118 | if isinstance(command, list): 119 | full_command = [cli_full_path, "--no-ansi", "--no-interaction"] + command 120 | elif isinstance(command, str): 121 | full_command = [ 122 | cli_full_path, 123 | "--no-ansi", 124 | "--no-interaction", 125 | ] + convert_string(command) 126 | 127 | # execute the occ command in a child process to keep current privileges 128 | module_conn, occ_conn = Pipe() 129 | p = Process( 130 | target=execute_occ_command, 131 | args=(occ_conn, module, php_exec, full_command), 132 | kwargs=kwargs, 133 | ) 134 | p.start() 135 | result = module_conn.recv() 136 | p.join() 137 | 138 | # check if the child process has sent an exception. 139 | if "exception" in result: 140 | exception_type = result["exception"] 141 | # raise the proper exception. 142 | if exception_type == "OccFileNotFoundException": 143 | raise OccFileNotFoundException(full_command) 144 | elif exception_type == "OccAuthenticationException": 145 | raise OccAuthenticationException(full_command, **result) 146 | else: 147 | raise OccExceptions(f"An unknown error occurred: {exception_type}") 148 | 149 | if "is in maintenance mode" in result["stderr"]: 150 | module.warn(" ".join(result["stderr"].splitlines()[0:1])) 151 | maintenanceMode = True 152 | else: 153 | maintenanceMode = False 154 | 155 | if "is not installed" in result["stderr"]: 156 | module.warn(result["stderr"].splitlines()[0]) 157 | 158 | output = result["stderr"] or result["stdout"] 159 | if result["rc"] != 0 and output: 160 | error_msg = convert_string(output.strip().splitlines()[0]) 161 | if all(x in error_msg for x in ["Command", "is", "not", "defined."]): 162 | raise OccNoCommandsDefined(full_command, **result) 163 | elif all(x in error_msg for x in ["Not", "enough", "arguments"]): 164 | raise OccNotEnoughArguments(full_command, **result) 165 | elif all(x in error_msg for x in ["option", "does", "not", "exist."]): 166 | raise OccOptionNotDefined(full_command, **result) 167 | elif all(x in error_msg for x in ["option", "requires", "value."]): 168 | raise OccOptionRequiresValue(full_command, **result) 169 | else: 170 | raise OccExceptions(full_command, **result) 171 | elif result["rc"] != 0: 172 | raise OccExceptions(full_command, **result) 173 | 174 | return result["rc"], result["stdout"], result["stderr"], maintenanceMode 175 | 176 | 177 | def run_php_inline(module, php_code: str) -> dict: 178 | """ 179 | Interface with Nextcloud server through ad-hoc php scripts. 180 | The script must define the var $result that will be exported into a python dict 181 | """ 182 | if isinstance(php_code, list): 183 | php_code = "\n".join(php_code) 184 | elif isinstance(php_code, str): 185 | php_code = dedent(php_code).strip() 186 | else: 187 | raise Exception("php_code must be a list or a string") 188 | 189 | full_code = f""" 190 | require_once 'lib/base.php'; 191 | {php_code} 192 | if (!isset($result)) {{ 193 | $result = null; 194 | }} 195 | echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); 196 | """ 197 | rc, stdout, stderr = module.run_command( 198 | [module.params.get("php_runtime"), "-r", full_code], 199 | cwd=module.params.get("nextcloud_path"), 200 | ) 201 | if rc != 0: 202 | raise PhpScriptException( 203 | msg="Failed to run the given php script.", 204 | stderr=stderr, 205 | stdout=stdout, 206 | rc=rc, 207 | php_script=full_code, 208 | ) 209 | 210 | stdout = stdout.strip() 211 | try: 212 | result = json.loads(stdout) if stdout and stdout != "null" else None 213 | return result 214 | except json.JSONDecodeError as e: 215 | raise PhpResultJsonException( 216 | msg="Failed to decode JSON from php script stdout", 217 | stderr=stderr, 218 | stdout=stdout, 219 | rc=rc, 220 | JSONDecodeError=str(e), 221 | ) 222 | except Exception: 223 | raise PhpInlineExceptions(stderr=stderr, stdout=stdout, rc=rc) 224 | -------------------------------------------------------------------------------- /plugins/modules/group.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2025, Marc Crébassa 5 | # Redistribution and use in source and binary forms, with or without 6 | # modification, are permitted provided that the following conditions are met: 7 | 8 | # * Redistributions of source code must retain the above copyright notice, this 9 | # list of conditions and the following disclaimer. 10 | 11 | # * Redistributions in binary form must reproduce the above copyright notice, 12 | # this list of conditions and the following disclaimer in the documentation 13 | # and/or other materials provided with the distribution. 14 | 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | 27 | DOCUMENTATION = r""" 28 | --- 29 | module: group 30 | short_description: Manage Nextcloud groups. 31 | author: 32 | - Marc Crébassa (@aalaesar) 33 | description: 34 | - This module allows for the addition or removal of groups in a Nextcloud instance. 35 | - It also supports adding or removing users from a group. 36 | - The module requires elevated privileges unless it is run as the user that owns the occ tool. 37 | options: 38 | state: 39 | description: 40 | - Desired state of the group. 41 | - Use 'present' to ensure the group exists, 'absent' to ensure it does not. 42 | - Use 'append_users' to add users to the group without affecting existing members. 43 | - Use 'remove_users' to remove users from the group without affecting other members. 44 | choices: ['present', 'absent', 'append_users', 'remove_users'] 45 | default: 'present' 46 | aliases: ['status', 'action'] 47 | type: str 48 | id: 49 | description: 50 | - The unique identifier or name of the group. 51 | required: true 52 | aliases: ['name', 'group_id'] 53 | type: str 54 | display_name: 55 | description: 56 | - The display name for the group. 57 | default: Null 58 | aliases: ['displayName'] 59 | type: str 60 | users: 61 | description: 62 | - A list of usernames to be added or removed from the group. 63 | - When state is 'present', the module will match the exact list provided. 64 | default: Null 65 | elements: str 66 | aliases: ['members'] 67 | type: list 68 | ignore_missing_users: 69 | description: 70 | - Whether to ignore errors when specified users are not found. 71 | default: False 72 | type: bool 73 | error_on_missing: 74 | description: 75 | - If set to True, the group is absent and any user management is expected, the module will fail. 76 | - The group will not be created. 77 | default: False 78 | type: bool 79 | extends_documentation_fragment: 80 | - nextcloud.admin.occ_common_options 81 | requirements: 82 | - python >= 3.12 83 | """ 84 | 85 | EXAMPLES = r""" 86 | - name: Ensure group is present with display name 87 | group: 88 | id: "engineering" 89 | display_name: "Engineering Team" 90 | state: "present" 91 | 92 | - name: Ensure group is absent 93 | group: 94 | id: "temporary_group" 95 | state: "absent" 96 | 97 | - name: Ensure group has specific users 98 | group: 99 | id: "project_team" 100 | users: 101 | - "alice" 102 | - "bob" 103 | state: "present" 104 | 105 | - name: Append users to a group 106 | group: 107 | id: "project_team" 108 | users: 109 | - "charlie" 110 | state: "append_users" 111 | 112 | - name: Remove users from a group 113 | group: 114 | id: "old_project_team" 115 | users: 116 | - "dave" 117 | state: "remove_users" 118 | """ 119 | 120 | RETURN = r""" 121 | changed: 122 | description: Indicates whether any changes were made to the group or its memberships. 123 | returned: always 124 | type: bool 125 | added_users: 126 | description: A list of users that were successfully added to the group. 127 | returned: when users are added 128 | type: list 129 | removed_users: 130 | description: A list of users that were successfully removed from the group. 131 | returned: when users are removed 132 | type: list 133 | """ 134 | 135 | 136 | from ansible.module_utils.basic import AnsibleModule 137 | from ansible_collections.nextcloud.admin.plugins.module_utils.nc_tools import ( 138 | extend_nc_tools_args_spec, 139 | ) 140 | from ansible_collections.nextcloud.admin.plugins.module_utils.identities import ( 141 | idState, 142 | Group, 143 | ) 144 | from ansible_collections.nextcloud.admin.plugins.module_utils.exceptions import ( 145 | IdentityNotPresent, 146 | ) 147 | 148 | module_args_spec = dict( 149 | state=dict( 150 | type="str", 151 | required=False, 152 | choices=["present", "absent", "append_users", "remove_users"], 153 | aliases=["status", "action"], 154 | default="present", 155 | ), 156 | id=dict( 157 | type="str", 158 | aliases=["name", "group_id"], 159 | required=True, 160 | ), 161 | display_name=dict( 162 | required=False, 163 | aliases=["displayName"], 164 | default=None, 165 | ), 166 | users=dict( 167 | type="list", 168 | required=False, 169 | default=None, 170 | aliases=["members"], 171 | elements="str", 172 | ), 173 | ignore_missing_users=dict( 174 | type="bool", 175 | required=False, 176 | default=False, 177 | ), 178 | error_on_missing=dict( 179 | type="bool", 180 | required=False, 181 | default=False, 182 | ), 183 | ) 184 | 185 | 186 | def main(): 187 | global module 188 | module = AnsibleModule( 189 | argument_spec=extend_nc_tools_args_spec(module_args_spec), 190 | supports_check_mode=True, 191 | ) 192 | result = dict( 193 | changed=False, 194 | added_users=[], 195 | removed_users=[], 196 | ) 197 | group_id = module.params.get("id") 198 | 199 | nc_group = Group(module, group_id) 200 | 201 | display_name = module.params.get("display_name") 202 | ignore_missing_users = module.params.get("ignore_missing_users") 203 | error_on_missing = module.params.get("error_on_missing") 204 | 205 | if module.params.get("state") == "absent": 206 | group_desired_state = idState.ABSENT 207 | else: 208 | group_desired_state = idState.PRESENT 209 | 210 | if module.params.get("state") in ["append_users", "remove_users"]: 211 | users_mgnt = module.params.get("state") 212 | else: 213 | users_mgnt = "exact_match" 214 | users_list = module.params.get("users") 215 | 216 | # fails if the module is called with error_on_missing true with a non empty user list 217 | # while the group is missing AND status requested anything but 'absent' 218 | if ( 219 | error_on_missing 220 | and nc_group.state is idState.ABSENT 221 | and users_list 222 | and group_desired_state is idState.PRESENT 223 | ): 224 | module.fail_json( 225 | msg=f"Group {group_id} is absent while trying to manage its users." 226 | ) 227 | 228 | # add/delete group here 229 | if nc_group.state is not group_desired_state: 230 | if not module.check_mode: 231 | if group_desired_state is idState.ABSENT: 232 | nc_group.delete() 233 | else: 234 | nc_group.add(display_name) 235 | result["changed"] = True 236 | 237 | # add/remove users here 238 | if group_desired_state is idState.PRESENT and users_list: 239 | users_to_add = set() 240 | users_to_remove = set() 241 | if users_mgnt == "exact_match": 242 | users_to_add = set(users_list) - set(nc_group.users) 243 | users_to_remove = set(nc_group.users) - set(users_list) 244 | elif users_mgnt == "append_users": 245 | users_to_add = set(users_list) 246 | elif users_mgnt == "remove_users": 247 | users_to_remove = set(users_list) 248 | 249 | if not module.check_mode: 250 | try: 251 | for a_user in users_to_add: 252 | nc_group.add_user(a_user) 253 | result["added_users"] += [a_user] 254 | result["changed"] = True 255 | for a_user in users_to_remove: 256 | nc_group.remove_user(a_user) 257 | result["removed_users"] += [a_user] 258 | result["changed"] = True 259 | except IdentityNotPresent as e: 260 | if ignore_missing_users: 261 | pass 262 | else: 263 | e.fail_json(module) 264 | else: 265 | result["added_users"] = list(users_to_add) 266 | result["removed_users"] = list(users_to_remove) 267 | if users_to_add or users_to_remove: 268 | result["changed"] = True 269 | 270 | # finish and show result 271 | 272 | module.exit_json(**result) 273 | 274 | 275 | if __name__ == "__main__": 276 | main() 277 | -------------------------------------------------------------------------------- /roles/install_nextcloud/templates/nginx_nc.j2: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # This file was generated by Ansible for {{ansible_fqdn}} 3 | # Do NOT modify this file by hand! 4 | ################################################################################ 5 | 6 | # Set the `immutable` cache control options only for assets with a cache busting `v` argument 7 | map $arg_v $asset_immutable { 8 | "" ""; 9 | default "immutable"; 10 | } 11 | 12 | {% if nextcloud_install_tls and nextcloud_tls_enforce %} 13 | server { 14 | listen 80; 15 | {% if nextcloud_ipv6 %} 16 | listen [::]:80; 17 | {% endif %} 18 | server_name {{ nextcloud_trusted_domain | ansible.utils.ipwrap | join(' ') }}; 19 | 20 | # Prevent nginx HTTP Server Detection 21 | server_tokens off; 22 | 23 | # Enforce HTTPS 24 | return 301 https://$server_name$request_uri; 25 | } 26 | {% endif %} 27 | 28 | server { 29 | {% if not nextcloud_install_tls or not nextcloud_tls_enforce %} 30 | listen 80; 31 | {% if nextcloud_ipv6 %} 32 | listen [::]:80; 33 | {% endif %} 34 | {% endif %} 35 | 36 | server_name {{ nextcloud_trusted_domain | ansible.utils.ipwrap | join(' ') }}; 37 | 38 | # Path to the root of your installation 39 | root {{ nextcloud_webroot }}; 40 | 41 | {% if nextcloud_install_tls %} 42 | listen 443 ssl http2; 43 | {% if nextcloud_ipv6 %} 44 | listen [::]:443 ssl http2; 45 | {% endif %} 46 | 47 | ssl_certificate {{ nextcloud_tls_cert_file }}; 48 | ssl_certificate_key {{ nextcloud_tls_cert_key_file }}; 49 | 50 | # Prevent nginx HTTP Server Detection 51 | server_tokens off; 52 | 53 | ssl_session_timeout 1d; 54 | ssl_session_cache shared:SSL:{{ nextcloud_tls_session_cache_size }}; 55 | # ssl_session_tickets off; 56 | 57 | # OCSP stapling 58 | ssl_stapling on; 59 | ssl_stapling_verify on; 60 | 61 | # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits 62 | ssl_dhparam {{ nextcloud_tls_dhparam }}; 63 | 64 | # Use Mozilla's guidelines for SSL/TLS settings 65 | # https://mozilla.github.io/server-side-tls/ssl-config-generator/ 66 | {% if nextcloud_mozilla_modern_ssl_profile %} 67 | # modern configuration. tweak to your needs. 68 | ssl_protocols TLSv1.3; 69 | {% else %} 70 | # intermediate configuration. tweak to your needs. 71 | ssl_protocols TLSv1.2 TLSv1.3; 72 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; 73 | {% endif %} 74 | ssl_prefer_server_ciphers off; 75 | 76 | # HSTS settings 77 | # WARNING: Only add the preload option once you read about 78 | # the consequences in https://hstspreload.org/. This option 79 | # will add the domain to a hardcoded list that is shipped 80 | # in all major browsers and getting removed from this list 81 | # could take several months. 82 | {% if nextcloud_hsts is string %} 83 | add_header Strict-Transport-Security "{{ nextcloud_hsts }}"; 84 | {% endif %} 85 | {% endif %} 86 | 87 | # set max upload size and increase upload timeout: 88 | client_max_body_size {{ nextcloud_max_upload_size }}; 89 | client_body_timeout 300s; 90 | fastcgi_buffers 64 4K; 91 | 92 | # Enable gzip but do not remove ETag headers 93 | gzip on; 94 | gzip_vary on; 95 | gzip_comp_level 4; 96 | gzip_min_length 256; 97 | gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; 98 | gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/wasm application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy; 99 | 100 | # Pagespeed is not supported by Nextcloud, so if your server is built 101 | # with the `ngx_pagespeed` module, uncomment this line to disable it. 102 | #pagespeed off; 103 | 104 | # The settings allows you to optimize the HTTP2 bandwitdth. 105 | # See https://blog.cloudflare.com/delivering-http-2-upload-speed-improvements/ 106 | # for tunning hints 107 | client_body_buffer_size 512k; 108 | 109 | # HTTP response headers borrowed from Nextcloud `.htaccess` 110 | add_header Referrer-Policy "no-referrer" always; 111 | add_header X-Content-Type-Options "nosniff" always; 112 | add_header X-Download-Options "noopen" always; 113 | add_header X-Frame-Options "SAMEORIGIN" always; 114 | add_header X-Permitted-Cross-Domain-Policies "none" always; 115 | add_header X-Robots-Tag "noindex, nofollow" always; 116 | add_header X-XSS-Protection "1; mode=block" always; 117 | 118 | # Remove X-Powered-By, which is an information leak 119 | fastcgi_hide_header X-Powered-By; 120 | 121 | # Add .mjs as a file extension for javascript 122 | # Either include it in the default mime.types list 123 | # or include you can include that list explicitly and add the file extension 124 | # only for Nextcloud like below: 125 | include mime.types; 126 | types { 127 | application/javascript js mjs; 128 | } 129 | 130 | # Specify how to handle directories -- specifying `/index.php$request_uri` 131 | # here as the fallback means that Nginx always exhibits the desired behaviour 132 | # when a client requests a path that corresponds to a directory that exists 133 | # on the server. In particular, if that directory contains an index.php file, 134 | # that file is correctly served; if it doesn't, then the request is passed to 135 | # the front-end controller. This consistent behaviour means that we don't need 136 | # to specify custom rules for certain paths (e.g. images and other assets, 137 | # `/updater`, `/ocm-provider`, `/ocs-provider`), and thus 138 | # `try_files $uri $uri/ /index.php$request_uri` 139 | # always provides the desired behaviour. 140 | index index.php index.html /index.php$request_uri; 141 | 142 | # Rule borrowed from `.htaccess` to handle Microsoft DAV clients 143 | location = / { 144 | if ( $http_user_agent ~ ^DavClnt ) { 145 | return 302 /remote.php/webdav/$is_args$args; 146 | } 147 | } 148 | 149 | location = /robots.txt { 150 | allow all; 151 | log_not_found off; 152 | access_log off; 153 | } 154 | 155 | # Make a regex exception for `/.well-known` so that clients can still 156 | # access it despite the existence of the regex rule 157 | # `location ~ /(\.|autotest|...)` which would otherwise handle requests 158 | # for `/.well-known`. 159 | location ^~ /.well-known { 160 | # The rules in this block are an adaptation of the rules 161 | # in `.htaccess` that concern `/.well-known`. 162 | 163 | location = /.well-known/carddav { return 301 /remote.php/dav/; } 164 | location = /.well-known/caldav { return 301 /remote.php/dav/; } 165 | 166 | location /.well-known/acme-challenge { try_files $uri $uri/ =404; } 167 | location /.well-known/pki-validation { try_files $uri $uri/ =404; } 168 | 169 | # Let Nextcloud's API for `/.well-known` URIs handle all other 170 | # requests by passing them to the front-end controller. 171 | return 301 /index.php$request_uri; 172 | } 173 | 174 | # Rules borrowed from `.htaccess` to hide certain paths from clients 175 | location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/) { return 404; } 176 | location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) { return 404; } 177 | 178 | # Ensure this block, which passes PHP files to the PHP process, is above the blocks 179 | # which handle static assets (as seen below). If this block is not declared first, 180 | # then Nginx will encounter an infinite rewriting loop when it prepends `/index.php` 181 | # to the URI, resulting in a HTTP 500 error response. 182 | location ~ \.php(?:$|/) { 183 | # Required for legacy support 184 | rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+|.+\/richdocumentscode\/proxy) /index.php$request_uri; 185 | 186 | fastcgi_split_path_info ^(.+?\.php)(/.*)$; 187 | set $path_info $fastcgi_path_info; 188 | 189 | try_files $fastcgi_script_name =404; 190 | 191 | include fastcgi_params; 192 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 193 | fastcgi_param PATH_INFO $path_info; 194 | fastcgi_param HTTPS $https if_not_empty; 195 | 196 | fastcgi_param modHeadersAvailable true; # Avoid sending the security headers twice 197 | fastcgi_param front_controller_active true; # Enable pretty urls 198 | fastcgi_pass php-handler; 199 | 200 | fastcgi_intercept_errors on; 201 | fastcgi_request_buffering off; 202 | 203 | fastcgi_max_temp_file_size 0; 204 | } 205 | 206 | location ~ \.(?:css|js|svg|gif|png|jpg|ico|wasm|tflite|map)$ { 207 | try_files $uri /index.php$request_uri; 208 | add_header Cache-Control "public, max-age=15778463, $asset_immutable"; 209 | access_log off; # Optional: Don't log access to assets 210 | 211 | location ~ \.wasm$ { 212 | default_type application/wasm; 213 | } 214 | } 215 | 216 | location ~ \.woff2?$ { 217 | try_files $uri /index.php$request_uri; 218 | expires 7d; # Cache-Control policy borrowed from `.htaccess` 219 | access_log off; # Optional: Don't log access to assets 220 | } 221 | 222 | # Rule borrowed from `.htaccess` 223 | location /remote { 224 | return 301 /remote.php$request_uri; 225 | } 226 | 227 | location / { 228 | try_files $uri $uri/ /index.php$request_uri; 229 | } 230 | } 231 | --------------------------------------------------------------------------------