diff --git a/nextcloud/update_nextcloud_v3.yml b/nextcloud/update_nextcloud_v3.yml new file mode 100644 index 0000000..45f72da --- /dev/null +++ b/nextcloud/update_nextcloud_v3.yml @@ -0,0 +1,1499 @@ +# nextcloud/update_nextcloud_v3.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 }}" + nextcloud_version: >- + {{ NEXTCLOUD_VERSION | default('') | string | trim }} + +# 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.j2 + +# 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 + 1200)) + + while true + do + if {{ docker_cmd }} exec \ + {{ nextcloud_container | quote }} \ + sh -c ' + set -eu + + # Apache becomes PID 1 only after the Nextcloud entrypoint completes. + # Do not match the apache2-foreground argument here. + read -r pid1_name < /proc/1/comm + + if [ "${pid1_name}" != "apache2" ]; then + exit 1 + fi + + # Ensure the official entrypoint has released its initialization lock. + flock -w 10 /var/www/html/nextcloud-init-sync.lock true + + 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); + } + + $actual = implode(".", array_slice($OC_Version, 0, 3)); + $expected = "{{ nextcloud_version }}"; + if ($actual !== $expected) { + fwrite(STDERR, "Expected Nextcloud $expected, found $actual\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; + ' + + current_step="checking Nextcloud OCC autoloader" + echo "Checking Nextcloud OCC before database upgrade" + {{ docker_cmd }} exec -u www-data \ + {{ nextcloud_container | quote }} \ + php occ --version + + 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 database upgrade after application updates" + echo "Running database upgrade after application updates" + + {{ docker_cmd }} exec -u www-data \ + {{ nextcloud_container | quote }} \ + php occ upgrade --no-interaction + + 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 requested Nextcloud version + ansible.builtin.assert: + that: + - nextcloud_version | length > 0 + - 'nextcloud_version is match("^[0-9]+[.][0-9]+[.][0-9]+$")' + fail_msg: >- + NEXTCLOUD_VERSION is required and must have format X.Y.Z, + for example 34.0.3. + quiet: true + + - 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 | Render Compose file on Proxmox host + ansible.builtin.template: + 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) + - nc_status.json.versionstring | default('') == nextcloud_version + 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 + ('