# 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_service: "nextcloud" # Compose file stored in Git. nextcloud_compose_local_file: >- {{ playbook_dir }}/../docker-compose/docker-compose-nextcloud.yml # Compose file location on the Nextcloud VM. nextcloud_compose_file: >- /data/compose/nextcloud/docker-compose-nextcloud.yml # Temporary Compose file on the Proxmox host. nextcloud_compose_controller_staging_file: >- /tmp/docker-compose-nextcloud-{{ backup_timestamp }}.yml # Temporary Compose file on the Nextcloud VM. nextcloud_compose_vm_staging_file: >- /tmp/docker-compose-nextcloud-{{ backup_timestamp }}.yml # 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 # ------------------------------------------------------------------------- # Compose installation script # ------------------------------------------------------------------------- compose_install_script: | set -Eeuo pipefail current_step="Compose installation initialization" trap ' rc=$? echo >&2 echo "Compose installation failed" >&2 echo "Step: ${current_step}" >&2 echo "Line: ${LINENO}" >&2 echo "Command: ${BASH_COMMAND}" >&2 echo "Return code: ${rc}" >&2 exit "${rc}" ' ERR source_file={{ nextcloud_compose_vm_staging_file | quote }} destination_file={{ nextcloud_compose_file | quote }} destination_dir="$(dirname "${destination_file}")" cleanup() { rm -f "${source_file}" } trap cleanup EXIT current_step="checking staged Compose file" echo "Checking staged Docker Compose file" test -s "${source_file}" current_step="validating staged Compose file" echo "Validating staged Docker Compose file" {{ docker_cmd }} compose \ -p {{ nextcloud_project | quote }} \ -f "${source_file}" \ config --quiet current_step="creating Compose destination directory" echo "Ensuring Compose destination directory exists" mkdir -p "${destination_dir}" current_step="comparing Compose files" if [ -f "${destination_file}" ] && \ cmp -s "${source_file}" "${destination_file}" then echo "Compose file is already up to date" exit 0 fi current_step="installing Compose file" echo "Installing Compose file" install \ -m 0644 \ "${source_file}" \ "${destination_file}" current_step="validating installed Compose file" echo "Validating installed Docker Compose file" {{ docker_cmd }} compose \ -p {{ nextcloud_project | quote }} \ -f "${destination_file}" \ config --quiet echo "Compose file updated" # ------------------------------------------------------------------------- # 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 runtime" echo "Checking Nextcloud runtime" {{ docker_cmd }} exec \ {{ nextcloud_container | quote }} \ sh -c ' command -v php >/dev/null test -s /var/www/html/occ test -s /var/www/html/version.php test -s /var/www/html/config/config.php ' current_step="reading Nextcloud status" echo "Reading Nextcloud status" # The status command is informational during preflight. A previous failed # upgrade may legitimately leave maintenance mode enabled or a database # upgrade pending. Such a state must not prevent a recovery run. {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ status || true 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" maintenance_enabled_by_script="false" cleanup() { rc=$? if [ "${rc}" -ne 0 ] && \ [ "${maintenance_enabled_by_script}" = "true" ] then echo "Backup failed; attempting to disable maintenance mode" >&2 {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ maintenance:mode --off >/dev/null 2>&1 || true fi } trap cleanup EXIT 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 maintenance_enabled_by_script="true" 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 \ --single-transaction \ --quick \ --routines \ --triggers \ -u"$MYSQL_USER" \ -p"$MYSQL_PASSWORD" \ "$MYSQL_DATABASE" else exec mysqldump \ --single-transaction \ --quick \ --routines \ --triggers \ -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 maintenance_enabled_by_script="false" 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 {{ docker_cmd }} logs \ --tail 100 \ {{ nextcloud_container | quote }} >&2 || true exit 124 fi sleep 3 done current_step="waiting for Nextcloud initialization" echo "Waiting for Nextcloud container initialization" initialization_deadline=$((SECONDS + 300)) while true do if {{ docker_cmd }} exec \ {{ nextcloud_container | quote }} \ sh -c ' set -eu # The official image entrypoint eventually replaces PID 1 with # apache2-foreground. Until that happens, application files may # still be copied from /usr/src/nextcloud to /var/www/html. pid1_command="$( tr "\000" " " < /proc/1/cmdline )" case "${pid1_command}" in *apache2-foreground*) ;; *) exit 1 ;; esac command -v php >/dev/null test -s /var/www/html/occ test -s /var/www/html/version.php test -s /var/www/html/config/config.php test -d /var/www/html/apps test -s /var/www/html/apps/activity/appinfo/info.xml ' >/dev/null 2>&1 then break fi if [ "${SECONDS}" -ge "${initialization_deadline}" ]; then echo "Timeout waiting for Nextcloud initialization" >&2 {{ docker_cmd }} logs \ --tail 200 \ {{ nextcloud_container | quote }} >&2 || true exit 124 fi sleep 3 done current_step="validating Nextcloud PHP runtime" echo "Validating Nextcloud PHP runtime" {{ docker_cmd }} exec \ {{ nextcloud_container | quote }} \ php -r ' require "/var/www/html/version.php"; if (empty($OC_Version)) { fwrite(STDERR, "Nextcloud version is unavailable\n"); exit(1); } echo "Nextcloud code version: "; echo implode(".", $OC_Version), PHP_EOL; ' current_step="validating bundled application files" echo "Validating bundled Nextcloud application files" {{ docker_cmd }} exec \ {{ nextcloud_container | quote }} \ php -r ' $infoFile = "/var/www/html/apps/activity/appinfo/info.xml"; if (!is_file($infoFile) || filesize($infoFile) === 0) { fwrite(STDERR, "Activity application metadata is missing\n"); exit(1); } $xml = @simplexml_load_file($infoFile); if ($xml === false) { fwrite(STDERR, "Activity application metadata is invalid\n"); exit(1); } if ((string) $xml->id !== "activity") { fwrite(STDERR, "Unexpected activity application ID\n"); exit(1); } echo "Activity application version: "; echo (string) $xml->version, PHP_EOL; ' echo "Nextcloud container initialization completed" current_step="running Nextcloud database upgrade" echo "Running the Nextcloud database upgrade" # Do not disable maintenance mode before this command. A previous failed # upgrade may have intentionally left maintenance mode enabled. OCC # upgrade manages the required maintenance state itself. {{ 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 current_step="verifying final Nextcloud state" echo "Verifying final Nextcloud 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 needs_db_upgrade="$( {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ status --output=json | {{ docker_cmd }} exec -i -u www-data \ {{ nextcloud_container | quote }} \ php -r ' $status = json_decode(stream_get_contents(STDIN), true); if (!is_array($status)) { exit(2); } echo !empty($status["needsDbUpgrade"]) ? "true" : "false"; ' )" if [ "${needs_db_upgrade}" != "false" ]; then echo "Nextcloud still requires a database upgrade" >&2 exit 1 fi 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 current_step="checking database upgrade state" needs_db_upgrade="$( {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ status --output=json | {{ docker_cmd }} exec -i -u www-data \ {{ nextcloud_container | quote }} \ php -r ' $status = json_decode(stream_get_contents(STDIN), true); if (!is_array($status)) { exit(2); } echo !empty($status["needsDbUpgrade"]) ? "true" : "false"; ' )" if [ "${needs_db_upgrade}" != "false" ]; then echo "Nextcloud still requires a database upgrade" >&2 exit 1 fi current_step="checking activity application integrity" echo "Checking activity application integrity" {{ docker_cmd }} exec -u www-data \ {{ nextcloud_container | quote }} \ php occ integrity:check-app activity 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 # ------------------------------------------------------------------------- # Compose synchronization # ------------------------------------------------------------------------- - name: Nextcloud | Check Compose file in repository ansible.builtin.stat: path: "{{ nextcloud_compose_local_file }}" register: nextcloud_compose_local_stat delegate_to: localhost run_once: true changed_when: false - name: Nextcloud | Fail when repository Compose file is missing ansible.builtin.assert: that: - nextcloud_compose_local_stat.stat.exists - nextcloud_compose_local_stat.stat.isreg - nextcloud_compose_local_stat.stat.size | int > 0 fail_msg: >- Nextcloud Compose file is missing or empty: {{ nextcloud_compose_local_file }} quiet: true run_once: true - name: Nextcloud | Synchronize Compose file to VM block: - name: Nextcloud | Stage Compose file on Proxmox host ansible.builtin.copy: src: "{{ nextcloud_compose_local_file }}" dest: "{{ nextcloud_compose_controller_staging_file }}" mode: "0600" - name: Nextcloud | Upload Compose file to VM staging path ansible.builtin.command: argv: - sshpass - -e - scp - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - LogLevel=ERROR - -o - ConnectTimeout=15 - "{{ nextcloud_compose_controller_staging_file }}" - "{{ vm_user }}@{{ vm_ip }}:{{ nextcloud_compose_vm_staging_file }}" environment: SSHPASS: "{{ vm_password }}" register: nextcloud_compose_upload changed_when: false no_log: true - name: Nextcloud | Validate and install Compose file 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 -c ' ~ (compose_install_script | quote)) if vm_use_sudo else ('bash -c ' ~ (compose_install_script | quote)) }} environment: SSHPASS: "{{ vm_password }}" register: nextcloud_compose_install changed_when: >- 'Compose file updated' in (nextcloud_compose_install.stdout | default('')) failed_when: false no_log: true - name: Nextcloud | Show Compose installation result ansible.builtin.debug: msg: | Compose installation return code: {{ nextcloud_compose_install.rc }} STDOUT: {{ nextcloud_compose_install.stdout | default('') | trim }} STDERR: {{ nextcloud_compose_install.stderr | default('') | trim }} when: - debug_level == 1 or nextcloud_compose_install.rc != 0 - name: Nextcloud | Fail when Compose installation failed ansible.builtin.assert: that: - nextcloud_compose_install.rc == 0 fail_msg: | Failed to install Nextcloud Compose file. Source file: {{ nextcloud_compose_local_file }} Destination file: {{ nextcloud_compose_file }} Return code: {{ nextcloud_compose_install.rc }} STDOUT: {{ nextcloud_compose_install.stdout | default('') | trim }} STDERR: {{ nextcloud_compose_install.stderr | default('') | trim }} quiet: true always: - name: Nextcloud | Remove staged Compose file from Proxmox host ansible.builtin.file: path: "{{ nextcloud_compose_controller_staging_file }}" state: absent changed_when: false - name: Nextcloud | Remove staged Compose file from VM ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - LogLevel=ERROR - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - >- rm -f {{ nextcloud_compose_vm_staging_file | quote }} environment: SSHPASS: "{{ vm_password }}" changed_when: false failed_when: false no_log: 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 -c ' ~ (preflight_script | quote)) if vm_use_sudo else ('bash -c ' ~ (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 -c ' ~ (nextcloud_version_command | quote)) if vm_use_sudo else ('bash -c ' ~ (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 -c ' ~ (backup_script | quote)) if vm_use_sudo else ('bash -c ' ~ (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 -c ' ~ (upgrade_script | quote)) if vm_use_sudo else ('bash -c ' ~ (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. Maintenance mode has intentionally not been disabled because the upgrade command failed. Resolve the reported error and rerun the playbook or complete the OCC upgrade manually. Return code: {{ nc_upgrade.rc }} STDOUT: {{ nc_upgrade.stdout | default('') | trim }} STDERR: {{ nc_upgrade.stderr | default('') | trim }} quiet: true always: # Disable maintenance mode when: # - the backup failed before the upgrade task was started, or # - the upgrade completed successfully. # # Do not disable maintenance mode after a failed OCC upgrade. - 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 -c ' ~ (nextcloud_maintenance_off_command | quote)) if vm_use_sudo else ('bash -c ' ~ (nextcloud_maintenance_off_command | quote)) }} environment: SSHPASS: "{{ vm_password }}" register: nc_maint_off changed_when: false failed_when: false no_log: true when: - nc_upgrade is not defined or nc_upgrade.rc == 0 - 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: - nc_upgrade is not defined or nc_upgrade.rc == 0 - 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 when: - nc_upgrade is not defined or nc_upgrade.rc == 0 # ------------------------------------------------------------------------- # 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 -c ' ~ (postcheck_script | quote)) if vm_use_sudo else ('bash -c ' ~ (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 ('