# nextcloud/update_nextcloud_v2.yml - name: Update Nextcloud on VM via Proxmox hosts: proxmox_nextcloud gather_facts: false become: true become_user: root become_method: sudo vars: # vm_pass is stored in Semaphore Secrets as an environment variable. vm_password: "{{ lookup('ansible.builtin.env', 'vm_pass') }}" # Values from Semaphore Extra variables. vm_use_sudo: "{{ use_sudo | default(false) | bool }}" debug_level: "{{ DEBUG | default(0) | int }}" retry_count: "{{ RETRIES | default(25) | int }}" # Nextcloud configuration. nextcloud_project: "nextcloud-collabora" nextcloud_compose_file: "/data/compose/nextcloud/docker-compose-nextcloud.yml" nextcloud_service: "nextcloud" nextcloud_container: "nextcloud" nextcloud_db_container: "nextcloud-db" redis_container: "redis" nextcloud_base_url: "https://cloud.martinfencl.eu" nextcloud_status_url: "{{ nextcloud_base_url }}/status.php" collabora_url: "https://collabora.martinfencl.eu/" collabora_discovery_url: "https://collabora.martinfencl.eu/hosting/discovery" backup_timestamp: "{{ lookup('pipe', 'date +%F-%H%M%S') }}" backup_dir: "/data/compose/nextcloud/backup-{{ backup_timestamp }}" docker_cmd: "env DOCKER_CLI_HINTS=0 docker" nextcloud_version_command: >- {{ docker_cmd }} exec -u www-data {{ nextcloud_container | quote }} php occ -V nextcloud_maintenance_off_command: >- {{ docker_cmd }} exec -u www-data {{ nextcloud_container | quote }} php occ maintenance:mode --off preflight_script: | set -euo pipefail command -v docker >/dev/null docker version >/dev/null docker compose version >/dev/null test -f {{ nextcloud_compose_file | quote }} for container in \ {{ nextcloud_container | quote }} \ {{ nextcloud_db_container | quote }} \ {{ redis_container | quote }} do state="$(docker inspect --format '{% raw %}{{.State.Running}}{% endraw %}' "${container}" 2>/dev/null || true)" if [ "${state}" != "true" ]; then echo "Container ${container} is not running" >&2 exit 1 fi done {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ status backup_script: | set -euo pipefail mkdir -p {{ backup_dir | quote }} echo "Enabling Nextcloud maintenance mode" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ maintenance:mode --on echo "Backing up Nextcloud configuration and custom applications" {{ docker_cmd }} exec \ {{ nextcloud_container | quote }} \ sh -c 'tar czf /tmp/nextcloud_conf.tgz -C /var/www/html config custom_apps' {{ docker_cmd }} cp \ {{ nextcloud_container | quote }}:/tmp/nextcloud_conf.tgz \ {{ (backup_dir ~ '/nextcloud_conf.tgz') | quote }} {{ docker_cmd }} exec \ {{ nextcloud_container | quote }} \ rm -f /tmp/nextcloud_conf.tgz echo "Backing up Nextcloud database" {{ docker_cmd }} exec \ {{ nextcloud_db_container | quote }} \ sh -c ' if command -v mariadb-dump >/dev/null 2>&1; then exec mariadb-dump \ -u"$MYSQL_USER" \ -p"$MYSQL_PASSWORD" \ "$MYSQL_DATABASE" else exec mysqldump \ -u"$MYSQL_USER" \ -p"$MYSQL_PASSWORD" \ "$MYSQL_DATABASE" fi ' > {{ (backup_dir ~ '/db.sql') | quote }} test -s {{ (backup_dir ~ '/nextcloud_conf.tgz') | quote }} test -s {{ (backup_dir ~ '/db.sql') | quote }} echo "Backup completed successfully: {{ backup_dir }}" upgrade_script: | set -euo pipefail echo "Pulling the current Nextcloud image" {{ docker_cmd }} compose \ -p {{ nextcloud_project | quote }} \ -f {{ nextcloud_compose_file | quote }} \ pull {{ nextcloud_service | quote }} echo "Recreating the Nextcloud container" {{ docker_cmd }} compose \ -p {{ nextcloud_project | quote }} \ -f {{ nextcloud_compose_file | quote }} \ up -d \ --no-deps \ --force-recreate \ {{ nextcloud_service | quote }} echo "Waiting for the Nextcloud container" timeout 180s bash -c ' until [ "$(docker inspect --format "{% raw %}{{.State.Running}}{% endraw %}" {{ nextcloud_container | quote }} 2>/dev/null)" = "true" ]; do sleep 3 done ' echo "Waiting for Nextcloud OCC" timeout 180s bash -c ' until docker exec -u www-data {{ nextcloud_container | quote }} php occ status >/dev/null 2>&1; do sleep 3 done ' echo "Running the Nextcloud database upgrade" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ upgrade echo "Updating Nextcloud applications" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ app:update --all echo "Running Nextcloud maintenance repair" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ maintenance:repair --include-expensive echo "Reading Nextcloud status" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ status pre_tasks: - name: Validate VM connection variables ansible.builtin.assert: that: - vm_ip is defined - vm_ip | string | trim | length > 0 - vm_user is defined - vm_user | string | trim | length > 0 - vm_password | string | length > 0 - retry_count | int > 0 fail_msg: >- Missing or invalid vm_ip, vm_user, vm_pass or RETRIES. Check the attached Semaphore Variable Group. quiet: true no_log: true tasks: - name: Ensure sshpass is installed ansible.builtin.apt: name: sshpass state: present update_cache: true - name: Nextcloud | Run preflight checks on VM ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - LogLevel=ERROR - -o - ConnectTimeout=15 - -o - ServerAliveInterval=10 - -o - ServerAliveCountMax=3 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ('sudo -n bash -lc ' ~ (preflight_script | quote)) if vm_use_sudo else ('bash -lc ' ~ (preflight_script | quote)) }} environment: SSHPASS: "{{ vm_password }}" register: nc_preflight changed_when: false no_log: true - name: Nextcloud | Show preflight result ansible.builtin.debug: msg: | RC: {{ nc_preflight.rc }} STDOUT: {{ nc_preflight.stdout | default('') | trim }} STDERR: {{ nc_preflight.stderr | default('') | trim }} when: debug_level == 1 - name: Nextcloud | Read current version before upgrade ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - LogLevel=ERROR - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ('sudo -n bash -lc ' ~ (nextcloud_version_command | quote)) if vm_use_sudo else ('bash -lc ' ~ (nextcloud_version_command | quote)) }} environment: SSHPASS: "{{ vm_password }}" register: nc_version_before changed_when: false failed_when: false no_log: true - name: Nextcloud | Print current version ansible.builtin.debug: msg: >- Current version: {{ nc_version_before.stdout | default('Nextcloud version is unavailable') | trim }} - name: Nextcloud | Run backup and upgrade block: - name: Nextcloud | Create configuration and database backup ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - LogLevel=ERROR - -o - ConnectTimeout=15 - -o - ServerAliveInterval=10 - -o - ServerAliveCountMax=3 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ('sudo -n bash -lc ' ~ (backup_script | quote)) if vm_use_sudo else ('bash -lc ' ~ (backup_script | quote)) }} environment: SSHPASS: "{{ vm_password }}" register: nc_backup no_log: true - name: Nextcloud | Show backup output ansible.builtin.debug: msg: | Backup directory: {{ backup_dir }} RC: {{ nc_backup.rc }} STDOUT: {{ nc_backup.stdout | default('') | trim }} STDERR: {{ nc_backup.stderr | default('') | trim }} when: debug_level == 1 - name: Nextcloud | Pull image and upgrade ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - LogLevel=ERROR - -o - ConnectTimeout=15 - -o - ServerAliveInterval=10 - -o - ServerAliveCountMax=3 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ('sudo -n bash -lc ' ~ (upgrade_script | quote)) if vm_use_sudo else ('bash -lc ' ~ (upgrade_script | quote)) }} environment: SSHPASS: "{{ vm_password }}" register: nc_upgrade changed_when: nc_upgrade.rc == 0 failed_when: false no_log: true - name: Nextcloud | Show upgrade output ansible.builtin.debug: msg: | Upgrade return code: {{ nc_upgrade.rc }} STDOUT: {{ nc_upgrade.stdout | default('') | trim }} STDERR: {{ nc_upgrade.stderr | default('') | trim }} - name: Nextcloud | Fail when upgrade command failed ansible.builtin.assert: that: - nc_upgrade.rc == 0 fail_msg: | Nextcloud upgrade failed. Return code: {{ nc_upgrade.rc }} STDOUT: {{ nc_upgrade.stdout | default('') | trim }} STDERR: {{ nc_upgrade.stderr | default('') | trim }} quiet: true - name: Nextcloud | Show upgrade output ansible.builtin.debug: msg: | RC: {{ nc_upgrade.rc }} STDOUT: {{ nc_upgrade.stdout | default('') | trim }} STDERR: {{ nc_upgrade.stderr | default('') | trim }} when: debug_level == 1 always: - name: Nextcloud | Disable maintenance mode ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - LogLevel=ERROR - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ('sudo -n bash -lc ' ~ (nextcloud_maintenance_off_command | quote)) if vm_use_sudo else ('bash -lc ' ~ (nextcloud_maintenance_off_command | quote)) }} environment: SSHPASS: "{{ vm_password }}" register: nc_maint_off changed_when: false failed_when: false no_log: true - name: Nextcloud | Show maintenance mode result ansible.builtin.debug: msg: | RC: {{ nc_maint_off.rc }} STDOUT: {{ nc_maint_off.stdout | default('') | trim }} STDERR: {{ nc_maint_off.stderr | default('') | trim }} when: debug_level == 1 - name: Nextcloud | Verify maintenance mode was disabled ansible.builtin.assert: that: - nc_maint_off.rc == 0 fail_msg: | Failed to disable Nextcloud maintenance mode. RC: {{ nc_maint_off.rc }} STDERR: {{ nc_maint_off.stderr | default('') | trim }} quiet: true - name: Nextcloud | Wait for public status.php ansible.builtin.uri: url: "{{ nextcloud_status_url }}" method: GET return_content: true validate_certs: true status_code: 200 timeout: 20 register: nc_status delegate_to: localhost run_once: true retries: "{{ retry_count }}" delay: 4 until: - nc_status.status | default(0) | int == 200 - nc_status.json is defined - nc_status.json.installed | default(false) | bool - not (nc_status.json.maintenance | default(true) | bool) - not (nc_status.json.needsDbUpgrade | default(true) | bool) changed_when: false - name: Nextcloud | Check required containers after upgrade ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - LogLevel=ERROR - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ( 'sudo -n bash -lc ' ~ ( 'for container in ' ~ (nextcloud_container | quote) ~ ' ' ~ (nextcloud_db_container | quote) ~ ' ' ~ (redis_container | quote) ~ '; do state="$(docker inspect --format ' ~ ("'{{.State.Running}}'") ~ ' "${container}" 2>/dev/null)"; ' ~ '[ "${state}" = "true" ] || exit 1; done' | quote ) ) if vm_use_sudo else ( 'bash -lc ' ~ ( 'for container in ' ~ (nextcloud_container | quote) ~ ' ' ~ (nextcloud_db_container | quote) ~ ' ' ~ (redis_container | quote) ~ '; do state="$(docker inspect --format ' ~ ("'{{.State.Running}}'") ~ ' "${container}" 2>/dev/null)"; ' ~ '[ "${state}" = "true" ] || exit 1; done' | quote ) ) }} environment: SSHPASS: "{{ vm_password }}" register: nc_containers_after changed_when: false no_log: true - name: Nextcloud | Check external Collabora root endpoint ansible.builtin.uri: url: "{{ collabora_url }}" method: GET return_content: true validate_certs: true status_code: 200 timeout: 20 register: collabora_root delegate_to: localhost run_once: true changed_when: false failed_when: >- (collabora_root.status | default(0) | int != 200) or ('OK' not in (collabora_root.content | default(''))) - name: Nextcloud | Check external Collabora discovery endpoint ansible.builtin.uri: url: "{{ collabora_discovery_url }}" method: GET return_content: true validate_certs: true status_code: 200 timeout: 20 register: collabora_discovery delegate_to: localhost run_once: true changed_when: false failed_when: >- (collabora_discovery.status | default(0) | int != 200) or ('