# nextcloud/update_nextcloud.yml - name: Update Nextcloud on VM via Proxmox hosts: proxmox_nextcloud gather_facts: false become: true become_user: root become_method: sudo vars: # Internal normalized 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" backup_dir: >- /data/compose/nextcloud/backup-{{ lookup('pipe', 'date +%F-%H%M%S') }} nextcloud_base_url: "https://cloud.martinfencl.eu" nextcloud_status_url: "{{ nextcloud_base_url }}/status.php" docker_prefix: >- unalias docker 2>/dev/null || true; export DOCKER_CLI_HINTS=0; command docker # Backup commands executed inside the Nextcloud VM nextcloud_backup_commands: - >- mkdir -p {{ backup_dir | quote }} - >- {{ docker_prefix }} exec -u www-data {{ nextcloud_container }} php occ maintenance:mode --on - >- {{ docker_prefix }} exec {{ nextcloud_container }} sh -c 'tar czf /tmp/nextcloud_conf.tgz -C /var/www/html config custom_apps' - >- {{ docker_prefix }} cp {{ nextcloud_container }}:/tmp/nextcloud_conf.tgz {{ (backup_dir + '/nextcloud_conf.tgz') | quote }} - >- {{ docker_prefix }} exec {{ nextcloud_container }} rm -f /tmp/nextcloud_conf.tgz - >- set -o pipefail; {{ docker_prefix }} exec {{ nextcloud_db_container }} 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 }} # Upgrade commands executed while maintenance mode remains enabled nextcloud_upgrade_commands: - >- {{ docker_prefix }} compose -p {{ nextcloud_project | quote }} -f {{ nextcloud_compose_file | quote }} pull {{ nextcloud_service | quote }} - >- {{ docker_prefix }} compose -p {{ nextcloud_project | quote }} -f {{ nextcloud_compose_file | quote }} up -d --no-deps --force-recreate {{ nextcloud_service | quote }} - >- timeout 180s bash -c 'until docker inspect --format "{{ '{{' }}.State.Running{{ '}}' }}" {{ nextcloud_container | quote }} 2>/dev/null | grep -qx true; do sleep 3; done' - >- {{ docker_prefix }} exec -u www-data {{ nextcloud_container }} php occ upgrade - >- {{ docker_prefix }} exec -u www-data {{ nextcloud_container }} php occ app:update --all - >- {{ docker_prefix }} exec -u www-data {{ nextcloud_container }} php occ maintenance:repair --include-expensive - >- {{ docker_prefix }} exec -u www-data {{ nextcloud_container }} 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_pass is defined - vm_pass | string | length > 0 fail_msg: >- Missing vm_ip, vm_user or vm_pass. Configure these variables in 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 | Show current version before upgrade ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ( 'sudo -n bash -lc ' + ( 'docker exec -u www-data nextcloud php occ -V' | quote ) ) if vm_use_sudo else ( 'bash -lc ' + ( 'docker exec -u www-data nextcloud php occ -V' | quote ) ) }} environment: SSHPASS: "{{ vm_pass }}" register: nc_version_before changed_when: false failed_when: false when: debug_level == 1 - name: Nextcloud | Print current version ansible.builtin.debug: msg: "{{ nc_version_before.stdout | default('Version unavailable') }}" when: debug_level == 1 # ------------------------------------------------------------------------- # Backup # ------------------------------------------------------------------------- - name: Nextcloud | Run backup commands on VM ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ('sudo -n bash -lc ' + (item | quote)) if vm_use_sudo else ('bash -lc ' + (item | quote)) }} environment: SSHPASS: "{{ vm_pass }}" loop: "{{ nextcloud_backup_commands }}" loop_control: index_var: backup_index label: "backup-cmd-{{ backup_index }}" register: nc_backup_cmds changed_when: false failed_when: false - name: Nextcloud | Show backup command outputs ansible.builtin.debug: msg: | CMD: {{ item.item }} RC: {{ item.rc }} STDOUT: {{ item.stdout | default('') | trim }} STDERR: {{ item.stderr | default('') | trim }} loop: "{{ nc_backup_cmds.results }}" loop_control: index_var: backup_debug_index label: "backup-cmd-{{ backup_debug_index }}" when: debug_level == 1 - name: Nextcloud | Fail if any backup command failed ansible.builtin.assert: that: - item.rc == 0 fail_msg: >- Nextcloud backup command failed: {{ item.item }} (rc={{ item.rc }}) STDERR: {{ item.stderr | default('') | trim }} quiet: true loop: "{{ nc_backup_cmds.results }}" loop_control: index_var: backup_assert_index label: "backup-cmd-{{ backup_assert_index }}" # ------------------------------------------------------------------------- # Upgrade # ------------------------------------------------------------------------- - name: Nextcloud | Run upgrade commands on VM ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ('sudo -n bash -lc ' + (item | quote)) if vm_use_sudo else ('bash -lc ' + (item | quote)) }} environment: SSHPASS: "{{ vm_pass }}" loop: "{{ nextcloud_upgrade_commands }}" loop_control: index_var: upgrade_index label: "upgrade-cmd-{{ upgrade_index }}" register: nc_upgrade_cmds changed_when: false failed_when: false - name: Nextcloud | Show upgrade command outputs ansible.builtin.debug: msg: | CMD: {{ item.item }} RC: {{ item.rc }} STDOUT: {{ item.stdout | default('') | trim }} STDERR: {{ item.stderr | default('') | trim }} loop: "{{ nc_upgrade_cmds.results }}" loop_control: index_var: upgrade_debug_index label: "upgrade-cmd-{{ upgrade_debug_index }}" when: debug_level == 1 - name: Nextcloud | Fail if any upgrade command failed ansible.builtin.assert: that: - item.rc == 0 fail_msg: >- Nextcloud upgrade command failed: {{ item.item }} (rc={{ item.rc }}) STDERR: {{ item.stderr | default('') | trim }} quiet: true loop: "{{ nc_upgrade_cmds.results }}" loop_control: index_var: upgrade_assert_index label: "upgrade-cmd-{{ upgrade_assert_index }}" # ------------------------------------------------------------------------- # Disable maintenance mode # ------------------------------------------------------------------------- - name: Nextcloud | Disable maintenance mode ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ( 'sudo -n bash -lc ' + ( 'docker exec -u www-data nextcloud ' + 'php occ maintenance:mode --off' | quote ) ) if vm_use_sudo else ( 'bash -lc ' + ( 'docker exec -u www-data nextcloud ' + 'php occ maintenance:mode --off' | quote ) ) }} environment: SSHPASS: "{{ vm_pass }}" register: nc_maint_off changed_when: false failed_when: false - name: Nextcloud | Fail if maintenance mode could not be 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 # ------------------------------------------------------------------------- # Readiness check # ------------------------------------------------------------------------- - name: Nextcloud | Wait for status.php from controller ansible.builtin.uri: url: "{{ nextcloud_status_url }}" method: GET return_content: true validate_certs: true status_code: 200 register: nc_status_controller delegate_to: localhost run_once: true retries: "{{ retry_count }}" delay: 4 until: - nc_status_controller.status | default(0) == 200 - nc_status_controller.json is defined failed_when: false changed_when: false - name: Nextcloud | Fetch status.php from VM ansible.builtin.command: argv: - sshpass - -e - ssh - -o - StrictHostKeyChecking=no - -o - UserKnownHostsFile=/dev/null - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - >- {{ ( 'sudo -n bash -lc ' + ( "python3 -c " + ( "import urllib.request; " + "print(urllib.request.urlopen(" + nextcloud_status_url | quote + ", timeout=15).read().decode())" ) | quote ) ) if vm_use_sudo else ( 'bash -lc ' + ( "python3 -c " + ( "import urllib.request; " + "print(urllib.request.urlopen(" + nextcloud_status_url | quote + ", timeout=15).read().decode())" ) | quote ) ) }} environment: SSHPASS: "{{ vm_pass }}" register: nc_status_vm changed_when: false failed_when: false when: - nc_status_controller.status | default(0) != 200 or nc_status_controller.json is not defined - name: Nextcloud | Use controller status JSON ansible.builtin.set_fact: nextcloud_status_json: "{{ nc_status_controller.json }}" when: - nc_status_controller.status | default(0) == 200 - nc_status_controller.json is defined - name: Nextcloud | Use VM status JSON ansible.builtin.set_fact: nextcloud_status_json: "{{ nc_status_vm.stdout | trim | from_json }}" when: - nextcloud_status_json is not defined - nc_status_vm is defined - nc_status_vm.stdout | default('') | trim | length > 0 - name: Nextcloud | Validate final status ansible.builtin.assert: that: - nextcloud_status_json is defined - nextcloud_status_json.installed | default(false) | bool - not (nextcloud_status_json.maintenance | default(true) | bool) - not (nextcloud_status_json.needsDbUpgrade | default(true) | bool) fail_msg: >- Nextcloud is not ready after upgrade. Status: {{ nextcloud_status_json | default('unavailable') }} quiet: true - name: Nextcloud | Print final status ansible.builtin.debug: msg: >- Nextcloud {{ nextcloud_status_json.version | default('?') }} (installed={{ nextcloud_status_json.installed | default('?') }}, maintenance={{ nextcloud_status_json.maintenance | default('?') }}, needsDbUpgrade={{ nextcloud_status_json.needsDbUpgrade | default('?') }})