diff --git a/nextcloud/update_nextcloud.yml b/nextcloud/update_nextcloud.yml new file mode 100644 index 0000000..232f0f6 --- /dev/null +++ b/nextcloud/update_nextcloud.yml @@ -0,0 +1,289 @@ +# nextcloud/update_nextcloud.yml + +- name: Update Nextcloud on VM via Proxmox + hosts: proxmox + gather_facts: false + become: true + become_user: root + become_method: sudo + + vars: + # --- Connection to VM (provided by Semaphore env vars) --- + vm_ip: "{{ lookup('env', 'VM_IP') }}" + vm_user: "{{ lookup('env', 'VM_USER') }}" + vm_pass: "{{ lookup('env', 'VM_PASS') }}" + use_sudo: false + + # --- Debug / retries --- + DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}" + RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}" + + # --- Nextcloud specifics --- + nextcloud_project: "nextcloud-collabora" + nextcloud_compose_file: "/data/compose/nextcloud/docker-compose-nextcloud.yml" + nextcloud_service: "nextcloud" + + # Backup directory on the VM (timestamped on controller) + 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 command prefix (consistent behavior and quiet hints) + docker_prefix: "unalias docker 2>/dev/null || true; DOCKER_CLI_HINTS=0; command docker" + + # --- Backup phase commands (run on VM) --- + nextcloud_backup_commands: + - > + mkdir -p "{{ backup_dir }}" + - > + docker exec -u www-data nextcloud php occ maintenance:mode --on + - > + tar czf "{{ backup_dir }}/config.tgz" -C /data/compose/nextcloud config + - > + tar czf "{{ backup_dir }}/custom_apps.tgz" -C /data/compose/nextcloud custom_apps + - > + docker exec nextcloud-db sh -c 'command -v mariadb-dump >/dev/null && mariadb-dump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE" || mysqldump -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' > "{{ backup_dir }}/db.sql" + + # --- Upgrade phase commands (run on VM) --- + nextcloud_upgrade_commands: + - > + {{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} pull {{ nextcloud_service }} + - > + {{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} up -d --no-deps --force-recreate {{ nextcloud_service }} + - > + sleep 10 + - > + docker exec -u www-data nextcloud php occ upgrade + - > + docker exec -u www-data nextcloud php occ app:update --all || true + - > + docker exec -u www-data nextcloud php occ maintenance:repair --include-expensive || true + - > + docker exec -u www-data nextcloud php occ status + + tasks: + - name: Ensure sshpass is installed (for password-based SSH) + ansible.builtin.apt: + name: sshpass + state: present + update_cache: yes + + - name: Nextcloud | Show current version before upgrade (DEBUG) + ansible.builtin.command: + argv: + - sshpass + - -e + - ssh + - -o + - StrictHostKeyChecking=no + - -o + - ConnectTimeout=15 + - "{{ vm_user }}@{{ vm_ip }}" + - bash + - -lc + - 'docker exec -u www-data nextcloud php occ -V || true' + environment: + SSHPASS: "{{ vm_pass }}" + register: nc_version_before + changed_when: false + failed_when: false + when: DEBUG == 1 + + # ------------------------- + # Backup phase + # ------------------------- + + - name: Nextcloud | Run backup commands on VM (via SSH) + ansible.builtin.command: + argv: + - sshpass + - -e + - ssh + - -o + - StrictHostKeyChecking=no + - -o + - ConnectTimeout=15 + - "{{ vm_user }}@{{ vm_ip }}" + - bash + - -lc + - "{{ ('sudo ' if use_sudo else '') + item }}" + environment: + SSHPASS: "{{ vm_pass }}" + loop: "{{ nextcloud_backup_commands }}" + loop_control: + index_var: idx + label: "backup-cmd-{{ idx }}" + register: nc_backup_cmds + changed_when: false + no_log: "{{ DEBUG == 0 }}" + + - name: Nextcloud | Show outputs of backup commands (DEBUG) + ansible.builtin.debug: + msg: | + CMD: {{ item.item }} + RC: {{ item.rc }} + STDOUT: + {{ (item.stdout | default('')).strip() }} + STDERR: + {{ (item.stderr | default('')).strip() }} + loop: "{{ nc_backup_cmds.results }}" + when: DEBUG == 1 + + - name: Nextcloud | Fail play if any backup command failed + ansible.builtin.assert: + that: "item.rc == 0" + fail_msg: "Nextcloud backup step failed on VM: {{ item.item }} (rc={{ item.rc }})" + success_msg: "All Nextcloud backup commands succeeded." + loop: "{{ nc_backup_cmds.results }}" + loop_control: + index_var: idx + label: "backup-cmd-{{ idx }}" + + # ------------------------- + # Upgrade phase + # ------------------------- + + - name: Nextcloud | Run upgrade commands on VM (via SSH) + ansible.builtin.command: + argv: + - sshpass + - -e + - ssh + - -o + - StrictHostKeyChecking=no + - -o + - ConnectTimeout=15 + - "{{ vm_user }}@{{ vm_ip }}" + - bash + - -lc + - "{{ ('sudo ' if use_sudo else '') + item }}" + environment: + SSHPASS: "{{ vm_pass }}" + loop: "{{ nextcloud_upgrade_commands }}" + loop_control: + index_var: idx + label: "upgrade-cmd-{{ idx }}" + register: nc_upgrade_cmds + changed_when: false + no_log: "{{ DEBUG == 0 }}" + + - name: Nextcloud | Show outputs of upgrade commands (DEBUG) + ansible.builtin.debug: + msg: | + CMD: {{ item.item }} + RC: {{ item.rc }} + STDOUT: + {{ (item.stdout | default('')).strip() }} + STDERR: + {{ (item.stderr | default('')).strip() }} + loop: "{{ nc_upgrade_cmds.results }}" + when: DEBUG == 1 + + - name: Nextcloud | Fail play if any upgrade command failed + ansible.builtin.assert: + that: "item.rc == 0" + fail_msg: "Nextcloud upgrade step failed on VM: {{ item.item }} (rc={{ item.rc }})" + success_msg: "All Nextcloud upgrade commands succeeded." + loop: "{{ nc_upgrade_cmds.results }}" + loop_control: + index_var: idx + label: "upgrade-cmd-{{ idx }}" + + - name: Nextcloud | Disable maintenance mode (only after successful upgrade) + ansible.builtin.command: + argv: + - sshpass + - -e + - ssh + - -o + - StrictHostKeyChecking=no + - -o + - ConnectTimeout=15 + - "{{ vm_user }}@{{ vm_ip }}" + - bash + - -lc + - 'docker exec -u www-data nextcloud php occ maintenance:mode --off' + environment: + SSHPASS: "{{ vm_pass }}" + register: nc_maint_off + changed_when: false + no_log: "{{ DEBUG == 0 }}" + + # ------------------------- + # Readiness check (status.php) + # ------------------------- + + - name: Nextcloud | Wait for status.php (controller first) + 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: "{{ RETRIES }}" + delay: 4 + until: nc_status_controller.status == 200 + failed_when: false + changed_when: false + + - name: Nextcloud | VM-side fetch status.php (JSON via Python) + ansible.builtin.command: + argv: + - sshpass + - -e + - ssh + - -o + - StrictHostKeyChecking=no + - -o + - ConnectTimeout=15 + - "{{ vm_user }}@{{ vm_ip }}" + - bash + - -lc + - | + python3 - <<'PY' + import json, urllib.request, sys + try: + with urllib.request.urlopen("{{ nextcloud_status_url }}", timeout=15) as r: + sys.stdout.write(r.read().decode()) + except Exception: + pass + PY + 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 + no_log: "{{ DEBUG == 0 }}" + + - name: Nextcloud | Choose status JSON (controller wins, else VM) + ansible.builtin.set_fact: + nextcloud_status_json: >- + {{ + (nc_status_controller.json + if (nc_status_controller.status | default(0)) == 200 and (nc_status_controller.json is defined) + else ( + (nc_status_vm.stdout | default('') | trim | length > 0) + | ternary((nc_status_vm.stdout | trim | from_json), omit) + ) + ) + }} + failed_when: false + + - name: Nextcloud | Print concise status summary (DEBUG) + 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('?') }}) + when: nextcloud_status_json is defined and DEBUG == 1 + + - name: Nextcloud | Status JSON not available (DEBUG) + ansible.builtin.debug: + msg: "status.php is not reachable or did not return JSON." + when: nextcloud_status_json is not defined and DEBUG == 1