Post-Tyranny-Tech-Infrastru.../ansible/playbooks/260123-upgrade-nextcloud-v2.yml

370 lines
13 KiB
YAML

---
# 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 <server> \
# --private-key "../keys/ssh/<server>"
#
# 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: |
count=0
max_attempts=24
while [ $count -lt $max_attempts ]; do
if docker exec nextcloud curl -f http://localhost:80/status.php 2>/dev/null; then
echo "Ready after $count attempts"
exit 0
fi
sleep 5
count=$((count + 1))
done
echo "Timeout after $max_attempts attempts"
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
============================================================