From 885edc980c2ae9670c8cabaa691934b247fa9f62 Mon Sep 17 00:00:00 2001 From: "martin.fencl" Date: Wed, 26 Nov 2025 17:07:02 +0100 Subject: [PATCH] Add Ansible playbook for updating Nextcloud DB and Redis on VM via Proxmox --- nextcloud/update_nextcloud_db_redis.yml | 293 ++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 nextcloud/update_nextcloud_db_redis.yml diff --git a/nextcloud/update_nextcloud_db_redis.yml b/nextcloud/update_nextcloud_db_redis.yml new file mode 100644 index 0000000..4e280f9 --- /dev/null +++ b/nextcloud/update_nextcloud_db_redis.yml @@ -0,0 +1,293 @@ +# nextcloud/update_nextcloud_db_redis.yml + +- name: Update Nextcloud DB and Redis 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" + + # Service names from docker-compose file + nextcloud_web_service: "nextcloud" + nextcloud_db_service: "nextclouddb" + redis_service: "redis" + + # Backup directory on the VM (timestamped on controller) + backup_dir: "/data/compose/nextcloud/backup-db-redis-{{ 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) --- + # Same idea as in update_nextcloud.yml: maintenance on + config/custom_apps + DB dump + nextcloud_backup_commands: + - > + mkdir -p "{{ backup_dir }}" + - > + docker exec -u www-data nextcloud php occ maintenance:mode --on + - > + docker exec nextcloud sh -c 'tar czf /tmp/nextcloud_conf.tgz -C /var/www/html config custom_apps' + - > + docker cp nextcloud:/tmp/nextcloud_conf.tgz "{{ backup_dir }}/nextcloud_conf.tgz" + - > + docker exec nextcloud rm /tmp/nextcloud_conf.tgz || true + - > + 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" + + # --- DB + Redis upgrade commands (run on VM) --- + db_redis_upgrade_commands: + # Update MariaDB service + - > + {{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} pull {{ nextcloud_db_service }} + - > + {{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} up -d --no-deps --force-recreate {{ nextcloud_db_service }} + # Simple DB health check (non-fatal) + - > + docker exec nextcloud-db sh -c 'mysqladmin ping -u"$MYSQL_USER" -p"$MYSQL_PASSWORD" "$MYSQL_DATABASE"' || true + # Update Redis service + - > + {{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} pull {{ redis_service }} + - > + {{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} up -d --no-deps --force-recreate {{ redis_service }} + # Simple Redis health check (non-fatal) + - > + docker exec redis redis-cli PING || true + + 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 DB/Redis 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 }}" + - "{{ ('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 DB/Redis backup step failed on VM: {{ item.item }} (rc={{ item.rc }})" + success_msg: "All Nextcloud DB/Redis backup commands succeeded." + loop: "{{ nc_backup_cmds.results }}" + loop_control: + index_var: idx + label: "backup-cmd-{{ idx }}" + + # ------------------------- + # DB + Redis upgrade phase + # ------------------------- + + - name: Nextcloud | Run DB/Redis 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: "{{ db_redis_upgrade_commands }}" + loop_control: + index_var: idx + label: "db-redis-cmd-{{ idx }}" + register: nc_db_redis_cmds + changed_when: false + failed_when: false + no_log: "{{ DEBUG == 0 }}" + + - name: Nextcloud | Show outputs of DB/Redis 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_db_redis_cmds.results }}" + when: DEBUG == 1 + + - name: Nextcloud | Fail play if any DB/Redis upgrade command failed + ansible.builtin.assert: + that: "item.rc == 0" + fail_msg: "Nextcloud DB/Redis upgrade step failed on VM: {{ item.item }} (rc={{ item.rc }})" + success_msg: "All Nextcloud DB/Redis upgrade commands succeeded." + loop: "{{ nc_db_redis_cmds.results }}" + loop_control: + index_var: idx + label: "db-redis-cmd-{{ idx }}" + + # ------------------------- + # Disable maintenance + readiness check + # ------------------------- + + - name: Nextcloud | Disable maintenance mode after DB/Redis upgrade + ansible.builtin.command: + argv: + - sshpass + - -e + - ssh + - -o + - StrictHostKeyChecking=no + - -o + - ConnectTimeout=15 + - "{{ vm_user }}@{{ vm_ip }}" + - "{{ ('sudo ' if use_sudo else '') }}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 }}" + + - 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 + 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