# 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 stored in Semaphore Extra variables. vm_use_sudo: "{{ use_sudo | default(false) | bool }}" debug_level: "{{ DEBUG | default(0) | int }}" retry_count: "{{ RETRIES | default(25) | int }}" # Docker Compose configuration. nextcloud_project: "nextcloud-collabora" nextcloud_compose_file: "/data/compose/nextcloud/docker-compose-nextcloud.yml" nextcloud_service: "nextcloud" # Container names. nextcloud_container: "nextcloud" nextcloud_db_container: "nextcloud-db" redis_container: "redis" # Public endpoints. 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 configuration. backup_root: "/data/compose/nextcloud" # Docker command without CLI hints. 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 # ------------------------------------------------------------------------- preflight_script: | set -Eeuo pipefail current_step="preflight initialization" trap ' rc=$? echo >&2 echo "Preflight check failed" >&2 echo "Step: ${current_step}" >&2 echo "Line: ${LINENO}" >&2 echo "Command: ${BASH_COMMAND}" >&2 echo "Return code: ${rc}" >&2 exit "${rc}" ' ERR current_step="checking Docker executable" echo "Checking Docker executable" command -v docker >/dev/null current_step="checking Docker daemon" echo "Checking Docker daemon" docker version >/dev/null current_step="checking Docker Compose" echo "Checking Docker Compose" docker compose version >/dev/null current_step="checking Docker Compose file" echo "Checking Docker Compose file" test -f {{ nextcloud_compose_file | quote }} current_step="validating Docker Compose configuration" echo "Validating Docker Compose configuration" {{ docker_cmd }} compose \ -p {{ nextcloud_project | quote }} \ -f {{ nextcloud_compose_file | quote }} \ config --quiet current_step="checking required containers" echo "Checking required containers" 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 current_step="checking Nextcloud status" echo "Checking Nextcloud status" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ status current_step="checking MariaDB" echo "Checking MariaDB" {{ docker_cmd }} exec \ {{ nextcloud_db_container | quote }} \ sh -c ' if command -v mariadb-admin >/dev/null 2>&1; then exec mariadb-admin ping \ -h 127.0.0.1 \ --silent else exec mysqladmin ping \ -h 127.0.0.1 \ --silent fi ' current_step="checking Redis" echo "Checking Redis" {{ docker_cmd }} exec \ {{ redis_container | quote }} \ redis-cli -h 127.0.0.1 ping | grep -qx PONG echo "Preflight checks completed successfully" # ------------------------------------------------------------------------- # Backup script # ------------------------------------------------------------------------- backup_script: | set -Eeuo pipefail current_step="backup initialization" trap ' rc=$? echo >&2 echo "Nextcloud backup failed" >&2 echo "Step: ${current_step}" >&2 echo "Line: ${LINENO}" >&2 echo "Command: ${BASH_COMMAND}" >&2 echo "Return code: ${rc}" >&2 exit "${rc}" ' ERR current_step="creating backup directory" echo "Creating backup directory: {{ backup_dir }}" mkdir -p {{ backup_dir | quote }} current_step="enabling maintenance mode" echo "Enabling Nextcloud maintenance mode" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ maintenance:mode --on current_step="creating configuration archive" 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 ' current_step="copying configuration archive" echo "Copying configuration archive" {{ docker_cmd }} cp \ {{ nextcloud_container | quote }}:/tmp/nextcloud_conf.tgz \ {{ (backup_dir ~ '/nextcloud_conf.tgz') | quote }} current_step="removing temporary configuration archive" echo "Removing temporary configuration archive" {{ docker_cmd }} exec \ {{ nextcloud_container | quote }} \ rm -f /tmp/nextcloud_conf.tgz current_step="creating database dump" 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 }} current_step="validating configuration archive" echo "Validating configuration archive" test -s {{ (backup_dir ~ '/nextcloud_conf.tgz') | quote }} current_step="validating database dump" echo "Validating database dump" test -s {{ (backup_dir ~ '/db.sql') | quote }} current_step="disabling maintenance mode after backup" echo "Disabling Nextcloud maintenance mode after backup" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ maintenance:mode --off current_step="verifying maintenance mode after backup" echo "Verifying maintenance mode after backup" maintenance_state="$( {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ config:system:get maintenance \ 2>/dev/null || true )" if [ "${maintenance_state}" = "true" ] || \ [ "${maintenance_state}" = "1" ]; then echo "Maintenance mode is still enabled after backup" >&2 exit 1 fi echo "Backup completed successfully" echo "Backup directory: {{ backup_dir }}" # ------------------------------------------------------------------------- # Upgrade script # ------------------------------------------------------------------------- upgrade_script: | set -Eeuo pipefail current_step="upgrade initialization" trap ' rc=$? echo >&2 echo "Nextcloud upgrade failed" >&2 echo "Step: ${current_step}" >&2 echo "Line: ${LINENO}" >&2 echo "Command: ${BASH_COMMAND}" >&2 echo "Return code: ${rc}" >&2 exit "${rc}" ' ERR current_step="validating Docker Compose configuration" echo "Validating Docker Compose configuration" {{ docker_cmd }} compose \ -p {{ nextcloud_project | quote }} \ -f {{ nextcloud_compose_file | quote }} \ config --quiet current_step="pulling Nextcloud image" echo "Pulling the current Nextcloud image" {{ docker_cmd }} compose \ -p {{ nextcloud_project | quote }} \ -f {{ nextcloud_compose_file | quote }} \ pull {{ nextcloud_service | quote }} current_step="recreating Nextcloud container" 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 }} current_step="waiting for Nextcloud container" echo "Waiting for the Nextcloud container" container_deadline=$((SECONDS + 180)) while true do container_state="$(docker inspect --format '{% raw %}{{.State.Running}}{% endraw %}' {{ nextcloud_container | quote }} 2>/dev/null || true)" if [ "${container_state}" = "true" ]; then break fi if [ "${SECONDS}" -ge "${container_deadline}" ]; then echo "Timeout waiting for container {{ nextcloud_container }}" >&2 exit 124 fi sleep 3 done current_step="waiting for Nextcloud OCC" echo "Waiting for Nextcloud OCC" occ_deadline=$((SECONDS + 180)) while true do if {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ status >/dev/null 2>&1 then break fi if [ "${SECONDS}" -ge "${occ_deadline}" ]; then echo "Timeout waiting for Nextcloud OCC" >&2 exit 124 fi sleep 3 done current_step="disabling stale maintenance mode" echo "Ensuring maintenance mode is disabled before OCC upgrade" maintenance_state="$( {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ config:system:get maintenance \ 2>/dev/null || true )" if [ "${maintenance_state}" = "true" ] || \ [ "${maintenance_state}" = "1" ]; then echo "Maintenance mode is enabled; disabling it" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ maintenance:mode --off fi current_step="verifying maintenance mode before OCC upgrade" maintenance_state="$( {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ config:system:get maintenance \ 2>/dev/null || true )" if [ "${maintenance_state}" = "true" ] || \ [ "${maintenance_state}" = "1" ]; then echo "Maintenance mode could not be disabled" >&2 exit 1 fi current_step="running Nextcloud database upgrade" echo "Running the Nextcloud database upgrade" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ upgrade --no-interaction current_step="updating Nextcloud applications" echo "Updating Nextcloud applications" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ app:update --all current_step="running Nextcloud maintenance repair" echo "Running Nextcloud maintenance repair" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ maintenance:repair --include-expensive current_step="reading final Nextcloud status" echo "Reading Nextcloud status" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ status echo "Nextcloud upgrade commands completed successfully" # ------------------------------------------------------------------------- # Post-upgrade script # ------------------------------------------------------------------------- postcheck_script: | set -Eeuo pipefail current_step="post-upgrade initialization" trap ' rc=$? echo >&2 echo "Post-upgrade check failed" >&2 echo "Step: ${current_step}" >&2 echo "Line: ${LINENO}" >&2 echo "Command: ${BASH_COMMAND}" >&2 echo "Return code: ${rc}" >&2 exit "${rc}" ' ERR current_step="checking required containers" echo "Checking required containers after upgrade" 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 current_step="checking MariaDB" echo "Checking MariaDB after upgrade" {{ docker_cmd }} exec \ {{ nextcloud_db_container | quote }} \ sh -c ' if command -v mariadb-admin >/dev/null 2>&1; then exec mariadb-admin ping \ -h 127.0.0.1 \ --silent else exec mysqladmin ping \ -h 127.0.0.1 \ --silent fi ' current_step="checking Redis" echo "Checking Redis after upgrade" {{ docker_cmd }} exec \ {{ redis_container | quote }} \ redis-cli -h 127.0.0.1 ping | grep -qx PONG current_step="checking Nextcloud status" echo "Checking Nextcloud status after upgrade" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ status current_step="checking Nextcloud maintenance state" maintenance_state="$( {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ config:system:get maintenance \ 2>/dev/null || true )" if [ "${maintenance_state}" = "true" ] || \ [ "${maintenance_state}" = "1" ]; then echo "Nextcloud is still in maintenance mode" >&2 exit 1 fi echo "Post-upgrade checks completed successfully" 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 - name: Generate backup timestamp ansible.builtin.set_fact: backup_timestamp: "{{ now(utc=true, fmt='%Y-%m-%d-%H%M%S') }}" changed_when: false - name: Set backup directory ansible.builtin.set_fact: backup_dir: >- {{ backup_root }}/backup-{{ backup_timestamp }} changed_when: false tasks: - name: Ensure sshpass is installed ansible.builtin.apt: name: sshpass state: present update_cache: true # ------------------------------------------------------------------------- # Preflight # ------------------------------------------------------------------------- - 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 failed_when: false no_log: true - name: Nextcloud | Show preflight result ansible.builtin.debug: msg: | Preflight return code: {{ nc_preflight.rc }} STDOUT: {{ nc_preflight.stdout | default('') | trim }} STDERR: {{ nc_preflight.stderr | default('') | trim }} when: - debug_level == 1 or nc_preflight.rc != 0 - name: Nextcloud | Fail when preflight checks failed ansible.builtin.assert: that: - nc_preflight.rc == 0 fail_msg: | Nextcloud preflight checks failed. Return code: {{ nc_preflight.rc }} STDOUT: {{ nc_preflight.stdout | default('') | trim }} STDERR: {{ nc_preflight.stderr | default('') | trim }} quiet: true # ------------------------------------------------------------------------- # Current version # ------------------------------------------------------------------------- - 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 - -o - ServerAliveInterval=10 - -o - ServerAliveCountMax=3 - "{{ 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 }} # ------------------------------------------------------------------------- # Backup and upgrade # ------------------------------------------------------------------------- - 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 changed_when: nc_backup.rc == 0 failed_when: false no_log: true - name: Nextcloud | Show backup output ansible.builtin.debug: msg: | Backup directory: {{ backup_dir }} Backup return code: {{ nc_backup.rc }} STDOUT: {{ nc_backup.stdout | default('') | trim }} STDERR: {{ nc_backup.stderr | default('') | trim }} when: - debug_level == 1 or nc_backup.rc != 0 - name: Nextcloud | Fail when backup failed ansible.builtin.assert: that: - nc_backup.rc == 0 fail_msg: | Nextcloud backup failed. Backup directory: {{ backup_dir }} Return code: {{ nc_backup.rc }} STDOUT: {{ nc_backup.stdout | default('') | trim }} STDERR: {{ nc_backup.stderr | default('') | trim }} quiet: true - name: Nextcloud | Pull image and run 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 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 - -o - ServerAliveInterval=10 - -o - ServerAliveCountMax=3 - "{{ 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: | Maintenance return code: {{ nc_maint_off.rc }} STDOUT: {{ nc_maint_off.stdout | default('') | trim }} STDERR: {{ nc_maint_off.stderr | default('') | trim }} when: - debug_level == 1 or nc_maint_off.rc != 0 - name: Nextcloud | Verify maintenance mode was disabled ansible.builtin.assert: that: - nc_maint_off.rc == 0 fail_msg: | Failed to disable Nextcloud maintenance mode. Return code: {{ nc_maint_off.rc }} STDOUT: {{ nc_maint_off.stdout | default('') | trim }} STDERR: {{ nc_maint_off.stderr | default('') | trim }} quiet: true # ------------------------------------------------------------------------- # Public Nextcloud readiness # ------------------------------------------------------------------------- - 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 # ------------------------------------------------------------------------- # Post-upgrade checks # ------------------------------------------------------------------------- - name: Nextcloud | Run post-upgrade 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 ' ~ (postcheck_script | quote)) if vm_use_sudo else ('bash -lc ' ~ (postcheck_script | quote)) }} environment: SSHPASS: "{{ vm_password }}" register: nc_postcheck changed_when: false failed_when: false no_log: true - name: Nextcloud | Show post-upgrade checks ansible.builtin.debug: msg: | Post-upgrade return code: {{ nc_postcheck.rc }} STDOUT: {{ nc_postcheck.stdout | default('') | trim }} STDERR: {{ nc_postcheck.stderr | default('') | trim }} when: - debug_level == 1 or nc_postcheck.rc != 0 - name: Nextcloud | Fail when post-upgrade checks failed ansible.builtin.assert: that: - nc_postcheck.rc == 0 fail_msg: | Nextcloud post-upgrade checks failed. Return code: {{ nc_postcheck.rc }} STDOUT: {{ nc_postcheck.stdout | default('') | trim }} STDERR: {{ nc_postcheck.stderr | default('') | trim }} quiet: true # ------------------------------------------------------------------------- # External Collabora checks # ------------------------------------------------------------------------- - 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 ('