diff --git a/ansible/playbooks/260123-upgrade-nextcloud-stage-v2.yml b/ansible/playbooks/260123-upgrade-nextcloud-stage-v2.yml new file mode 100644 index 0000000..8f066ae --- /dev/null +++ b/ansible/playbooks/260123-upgrade-nextcloud-stage-v2.yml @@ -0,0 +1,120 @@ +--- +# Nextcloud Upgrade Stage Task File (Fixed Version) +# This file is included by 260123-upgrade-nextcloud-v2.yml for each upgrade stage +# Do not run directly +# +# Improvements: +# - Better version detection (actual running version) +# - Proper error handling +# - Clearer status messages +# - Maintenance mode handling + +- name: "Stage {{ stage.stage }}: Starting v{{ stage.from }} → v{{ stage.to }}" + debug: + msg: | + ============================================================ + Stage {{ stage.stage }}: Upgrading v{{ stage.from }} → v{{ stage.to }} + ============================================================ + +- name: "Stage {{ stage.stage }}: Get current running version" + shell: docker exec -u www-data nextcloud php occ status --output=json + register: stage_version_check + changed_when: false + +- name: "Stage {{ stage.stage }}: Parse current version" + set_fact: + stage_current: "{{ (stage_version_check.stdout | from_json).versionstring }}" + +- name: "Stage {{ stage.stage }}: Display current version" + debug: + msg: "Currently running: v{{ stage_current }}" + +- name: "Stage {{ stage.stage }}: Check if already on target version" + debug: + msg: "✓ Already on v{{ stage_current }} - skipping this stage" + when: stage_current is version(stage.to, '>=') + +- name: "Stage {{ stage.stage }}: Skip if already upgraded" + meta: end_play + when: stage_current is version(stage.to, '>=') + +- name: "Stage {{ stage.stage }}: Verify version is compatible" + fail: + msg: "Cannot upgrade from v{{ stage_current }} (expected v{{ stage.from }}.x)" + when: stage_current is version(stage.from, '<') or (stage_current is version(stage.to, '>=')) + +- name: "Stage {{ stage.stage }}: Update docker-compose.yml to v{{ stage.to }}" + replace: + path: "{{ nextcloud_base_dir }}/docker-compose.yml" + regexp: 'image:\s*nextcloud:{{ stage.from }}' + replace: 'image: nextcloud:{{ stage.to }}' + +- name: "Stage {{ stage.stage }}: Verify docker-compose.yml was updated" + shell: grep "image: nextcloud:{{ stage.to }}" {{ nextcloud_base_dir }}/docker-compose.yml + register: compose_verify + changed_when: false + failed_when: compose_verify.rc != 0 + +- name: "Stage {{ stage.stage }}: Pull Nextcloud v{{ stage.to }} image" + shell: docker pull nextcloud:{{ stage.to }} + register: image_pull + changed_when: "'Downloaded' in image_pull.stdout or 'Pulling' in image_pull.stdout or 'Downloaded newer' in image_pull.stderr" + +- name: "Stage {{ stage.stage }}: Stop containers before upgrade" + community.docker.docker_compose_v2: + project_src: "{{ nextcloud_base_dir }}" + state: stopped + +- name: "Stage {{ stage.stage }}: Start containers with new version" + community.docker.docker_compose_v2: + project_src: "{{ nextcloud_base_dir }}" + state: present + +- name: "Stage {{ stage.stage }}: Wait for Nextcloud container to be ready" + shell: | + for i in {1..60}; do + if docker exec nextcloud curl -f http://localhost:80/status.php 2>/dev/null; then + echo "Container ready" + exit 0 + fi + sleep 5 + done + echo "Timeout waiting for container" + exit 1 + register: container_ready + changed_when: false + +- name: "Stage {{ stage.stage }}: Run occ upgrade" + shell: docker exec -u www-data nextcloud php occ upgrade --no-interaction + register: occ_upgrade + changed_when: "'Update successful' in occ_upgrade.stdout or 'upgraded' in occ_upgrade.stdout" + failed_when: + - occ_upgrade.rc != 0 + - "'already latest version' not in occ_upgrade.stdout" + - "'No upgrade required' not in occ_upgrade.stdout" + +- name: "Stage {{ stage.stage }}: Display upgrade output" + debug: + msg: "{{ occ_upgrade.stdout_lines }}" + +- name: "Stage {{ stage.stage }}: Verify upgrade succeeded" + shell: docker exec -u www-data nextcloud php occ status --output=json + register: stage_verify + changed_when: false + +- name: "Stage {{ stage.stage }}: Parse upgraded version" + set_fact: + stage_upgraded: "{{ (stage_verify.stdout | from_json).versionstring }}" + +- name: "Stage {{ stage.stage }}: Check upgrade was successful" + fail: + msg: "Upgrade to v{{ stage.to }} failed - still on v{{ stage_upgraded }}" + when: stage_upgraded is version(stage.to, '<') + +- name: "Stage {{ stage.stage }}: Success" + debug: + msg: | + ============================================================ + ✓ Stage {{ stage.stage }} completed successfully + Upgraded from v{{ stage_current }} to v{{ stage_upgraded }} + ============================================================ diff --git a/ansible/playbooks/260123-upgrade-nextcloud-v2.yml b/ansible/playbooks/260123-upgrade-nextcloud-v2.yml new file mode 100644 index 0000000..211ea61 --- /dev/null +++ b/ansible/playbooks/260123-upgrade-nextcloud-v2.yml @@ -0,0 +1,365 @@ +--- +# Nextcloud Major Version Upgrade Playbook (Fixed Version) +# Created: 2026-01-23 +# Purpose: Safely upgrade Nextcloud from v30 to v32 via v31 (staged upgrade) +# +# Usage: +# cd ansible/ +# HCLOUD_TOKEN="..." ansible-playbook -i hcloud.yml \ +# playbooks/260123-upgrade-nextcloud-v2.yml --limit \ +# --private-key "../keys/ssh/" +# +# Requirements: +# - HCLOUD_TOKEN environment variable set +# - SSH access to target server +# - Sufficient disk space for backups +# +# Improvements over v1: +# - Idempotent: can be re-run safely after failures +# - Better version state tracking (reads actual running version) +# - Proper maintenance mode handling +# - Stage skipping if already on target version +# - Better error messages and rollback instructions + +- name: Upgrade Nextcloud from v30 to v32 (staged) + hosts: all + become: true + gather_facts: true + + vars: + nextcloud_base_dir: "/opt/nextcloud" + backup_dir: "/root/nextcloud-backup-{{ ansible_date_time.iso8601_basic_short }}" + target_version: "32" + + tasks: + # ============================================================ + # PRE-UPGRADE CHECKS + # ============================================================ + + - name: Display upgrade plan + debug: + msg: | + ============================================================ + Nextcloud Upgrade Plan - {{ inventory_hostname }} + ============================================================ + + Target: Nextcloud v{{ target_version }} + Backup: {{ backup_dir }} + + This playbook will: + 1. Detect current version + 2. Create backup if needed + 3. Upgrade through required stages (v30→v31→v32) + 4. Skip stages already completed + 5. Re-enable apps and disable maintenance mode + + Estimated time: 10-20 minutes + ============================================================ + + - name: Check if Nextcloud is installed + shell: docker ps --filter "name=^nextcloud$" --format "{{ '{{' }}.Names{{ '}}' }}" + register: nextcloud_running + changed_when: false + failed_when: false + + - name: Fail if Nextcloud is not running + fail: + msg: "Nextcloud container is not running on {{ inventory_hostname }}" + when: "'nextcloud' not in nextcloud_running.stdout" + + - name: Get current Nextcloud version + shell: docker exec -u www-data nextcloud php occ status --output=json + register: nextcloud_status + changed_when: false + failed_when: false + + - name: Parse Nextcloud status + set_fact: + nc_status: "{{ nextcloud_status.stdout | from_json }}" + when: nextcloud_status.rc == 0 + + - name: Handle Nextcloud in maintenance mode + block: + - name: Display maintenance mode warning + debug: + msg: "⚠ Nextcloud is in maintenance mode. Attempting to disable it..." + + - name: Disable maintenance mode if enabled + shell: docker exec -u www-data nextcloud php occ maintenance:mode --off + register: maint_off + changed_when: "'disabled' in maint_off.stdout" + + - name: Wait a moment for mode change + pause: + seconds: 2 + + - name: Re-check status after disabling maintenance mode + shell: docker exec -u www-data nextcloud php occ status --output=json + register: nextcloud_status_retry + changed_when: false + + - name: Update status + set_fact: + nc_status: "{{ nextcloud_status_retry.stdout | from_json }}" + when: nextcloud_status.rc != 0 or (nc_status is defined and nc_status.maintenance | bool) + + - name: Display current version + debug: + msg: | + Current: v{{ nc_status.versionstring }} + Target: v{{ target_version }} + Maintenance mode: {{ nc_status.maintenance }} + + - name: Check if already on target version + debug: + msg: "✓ Nextcloud is already on v{{ nc_status.versionstring }} - nothing to do" + when: nc_status.versionstring is version(target_version, '>=') + + - name: End play if already upgraded + meta: end_host + when: nc_status.versionstring is version(target_version, '>=') + + - name: Check disk space + shell: df -BG {{ nextcloud_base_dir }} | tail -1 | awk '{print $4}' | sed 's/G//' + register: disk_space_gb + changed_when: false + + - name: Verify sufficient disk space + fail: + msg: "Insufficient disk space: {{ disk_space_gb.stdout }}GB available, need at least 5GB" + when: disk_space_gb.stdout | int < 5 + + - name: Display available disk space + debug: + msg: "Available disk space: {{ disk_space_gb.stdout }}GB" + + # ============================================================ + # BACKUP PHASE (only if not already backed up) + # ============================================================ + + - name: Check if backup already exists + stat: + path: "{{ backup_dir }}" + register: backup_exists + + - name: Skip backup if already exists + debug: + msg: "✓ Backup already exists at {{ backup_dir }} - skipping backup phase" + when: backup_exists.stat.exists + + - name: Create backup + block: + - name: Create backup directory + file: + path: "{{ backup_dir }}" + state: directory + mode: '0700' + + - name: Enable maintenance mode for backup + shell: docker exec -u www-data nextcloud php occ maintenance:mode --on + register: maintenance_on + changed_when: "'enabled' in maintenance_on.stdout" + + - name: Backup Nextcloud database + shell: | + docker exec nextcloud-db pg_dump -U nextcloud nextcloud | gzip > {{ backup_dir }}/database.sql.gz + args: + creates: "{{ backup_dir }}/database.sql.gz" + + - name: Get database backup size + stat: + path: "{{ backup_dir }}/database.sql.gz" + register: db_backup + + - name: Display database backup info + debug: + msg: "Database backup: {{ (db_backup.stat.size / 1024 / 1024) | round(2) }} MB" + + - name: Stop Nextcloud containers for volume backup + community.docker.docker_compose_v2: + project_src: "{{ nextcloud_base_dir }}" + state: stopped + + - name: Backup Nextcloud app volume + shell: | + tar -czf {{ backup_dir }}/nextcloud-app-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-app/_data . + args: + creates: "{{ backup_dir }}/nextcloud-app-volume.tar.gz" + + - name: Backup Nextcloud database volume + shell: | + tar -czf {{ backup_dir }}/nextcloud-db-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-db-data/_data . + args: + creates: "{{ backup_dir }}/nextcloud-db-volume.tar.gz" + + - name: Copy current docker-compose.yml to backup + copy: + src: "{{ nextcloud_base_dir }}/docker-compose.yml" + dest: "{{ backup_dir }}/docker-compose.yml.backup" + remote_src: true + + - name: Display backup summary + debug: + msg: | + ============================================================ + ✓ Backup completed: {{ backup_dir }} + ============================================================ + + To restore from backup if needed: + 1. cd {{ nextcloud_base_dir }} && docker compose down + 2. tar -xzf {{ backup_dir }}/nextcloud-app-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-app/_data + 3. tar -xzf {{ backup_dir }}/nextcloud-db-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-db-data/_data + 4. cp {{ backup_dir }}/docker-compose.yml.backup {{ nextcloud_base_dir }}/docker-compose.yml + 5. cd {{ nextcloud_base_dir }} && docker compose up -d + ============================================================ + + - name: Restart containers after backup + community.docker.docker_compose_v2: + project_src: "{{ nextcloud_base_dir }}" + state: present + + - name: Wait for Nextcloud to be ready + shell: | + for i in {1..24}; do + if docker exec nextcloud curl -f http://localhost:80/status.php 2>/dev/null; then + exit 0 + fi + sleep 5 + done + exit 1 + register: nextcloud_ready + changed_when: false + + - name: Disable maintenance mode after backup + shell: docker exec -u www-data nextcloud php occ maintenance:mode --off + register: maint_off_backup + changed_when: "'disabled' in maint_off_backup.stdout" + + when: not backup_exists.stat.exists + + # ============================================================ + # DETERMINE UPGRADE PATH + # ============================================================ + + - name: Get current version for upgrade planning + shell: docker exec -u www-data nextcloud php occ status --output=json + register: current_version_check + changed_when: false + + - name: Parse current version + set_fact: + current_version: "{{ (current_version_check.stdout | from_json).versionstring }}" + + - name: Determine required upgrade stages + set_fact: + required_stages: "{{ [] }}" + + - name: Add v30→v31 stage if needed + set_fact: + required_stages: "{{ required_stages + [{'from': '30', 'to': '31', 'stage': 1}] }}" + when: current_version is version('30', '>=') and current_version is version('31', '<') + + - name: Add v31→v32 stage if needed + set_fact: + required_stages: "{{ required_stages + [{'from': '31', 'to': '32', 'stage': 2}] }}" + when: current_version is version('31', '>=') and current_version is version('32', '<') + + - name: Display upgrade stages + debug: + msg: | + Current version: v{{ current_version }} + Required stages: {{ required_stages | length }} + {% if required_stages | length > 0 %} + Will upgrade: {% for stage in required_stages %}v{{ stage.from }}→v{{ stage.to }}{{ ' ' if not loop.last else '' }}{% endfor %} + {% else %} + No upgrade stages needed + {% endif %} + + - name: Skip upgrade if no stages needed + meta: end_host + when: required_stages | length == 0 + + # ============================================================ + # STAGED UPGRADE LOOP + # ============================================================ + + - name: Perform staged upgrades + include_tasks: "{{ playbook_dir }}/260123-upgrade-nextcloud-stage-v2.yml" + loop: "{{ required_stages }}" + loop_control: + loop_var: stage + + # ============================================================ + # POST-UPGRADE + # ============================================================ + + - name: Get final version + shell: docker exec -u www-data nextcloud php occ status --output=json + register: final_status + changed_when: false + + - name: Parse final version + set_fact: + final_version: "{{ (final_status.stdout | from_json).versionstring }}" + + - name: Verify upgrade to target version + fail: + msg: "Upgrade incomplete - on v{{ final_version }}, expected v{{ target_version }}.x" + when: final_version is version(target_version, '<') + + - name: Run database optimizations + shell: docker exec -u www-data nextcloud php occ db:add-missing-indices + register: db_indices + changed_when: false + failed_when: false + + - name: Run bigint conversion + shell: docker exec -u www-data nextcloud php occ db:convert-filecache-bigint --no-interaction + register: db_bigint + changed_when: false + failed_when: false + timeout: 600 + + - name: Re-enable critical apps + shell: | + docker exec -u www-data nextcloud php occ app:enable user_oidc || true + docker exec -u www-data nextcloud php occ app:enable richdocuments || true + register: apps_enabled + changed_when: false + + - name: Ensure maintenance mode is disabled + shell: docker exec -u www-data nextcloud php occ maintenance:mode --off + register: final_maint_off + changed_when: "'disabled' in final_maint_off.stdout" + failed_when: false + + - name: Update docker-compose.yml to use latest tag + replace: + path: "{{ nextcloud_base_dir }}/docker-compose.yml" + regexp: 'image:\s*nextcloud:\d+' + replace: 'image: nextcloud:latest' + + - name: Display success message + debug: + msg: | + ============================================================ + ✓ UPGRADE SUCCESSFUL! + ============================================================ + + Server: {{ inventory_hostname }} + From: v30.x + To: v{{ final_version }} + + Backup: {{ backup_dir }} + + Next steps: + 1. Test login: https://nextcloud.{{ client_domain }} + 2. Test OIDC: Click "Login with Authentik" + 3. Test file operations + 4. Test Collabora Office + + If all tests pass, remove backup: + rm -rf {{ backup_dir }} + + docker-compose.yml now uses 'nextcloud:latest' tag + ============================================================