From 4239ab4ed0b2252a2f65b31b2f38a0de3bf0c090 Mon Sep 17 00:00:00 2001 From: "martin.fencl" Date: Thu, 6 Aug 2026 18:00:47 +0200 Subject: [PATCH] test2 --- nextcloud/check_stack_nextcloud.yml | 106 ++++++- nextcloud/update_nextcloud_v2.yml | 472 ++++++++++++++++++++-------- 2 files changed, 431 insertions(+), 147 deletions(-) diff --git a/nextcloud/check_stack_nextcloud.yml b/nextcloud/check_stack_nextcloud.yml index e8fd969..ddd3221 100644 --- a/nextcloud/check_stack_nextcloud.yml +++ b/nextcloud/check_stack_nextcloud.yml @@ -1,43 +1,115 @@ # nextcloud/check_stack_nextcloud.yml -- name: Run Nextcloud maintenance - hosts: nextcloud +- name: Run Nextcloud maintenance 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 }}" + vm_commands: - - "docker exec -u www-data nextcloud php -f /var/www/html/cron.php" - - "docker exec -u www-data nextcloud php occ app:update --all" - - "docker exec -u www-data nextcloud php occ maintenance:repair --include-expensive" - - "docker exec -u www-data nextcloud php occ status" - - "set -o pipefail; timeout 180s bash -x /data/compose/nextcloud/stack-health.sh - + docker exec -u www-data nextcloud + php -f /var/www/html/cron.php + + - >- + docker exec -u www-data nextcloud + php occ app:update --all + + - >- + docker exec -u www-data nextcloud + php occ maintenance:repair --include-expensive + + - >- + docker exec -u www-data nextcloud + php occ status + + - >- + set -o pipefail; + timeout 180s bash -x + /data/compose/nextcloud/stack-health.sh + 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: Run Nextcloud commands - ansible.builtin.shell: - cmd: "{{ item }}" - executable: /bin/bash + - name: Ensure sshpass is installed + ansible.builtin.apt: + name: sshpass + state: present + update_cache: true + + - name: Run Nextcloud 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: "{{ vm_commands }}" + loop_control: + label: "{{ item }}" register: vm_cmds changed_when: false failed_when: false - - name: Show command outputs + - name: Show outputs for each command ansible.builtin.debug: msg: | CMD: {{ item.item }} - RC: {{ item.rc }} + RC: {{ item.rc }} STDOUT: - {{ item.stdout | default('') }} + {{ (item.stdout | default('') | trim) }} STDERR: - {{ item.stderr | default('') }} + {{ (item.stderr | default('') | trim) }} loop: "{{ vm_cmds.results }}" + loop_control: + label: "{{ item.item }}" - - name: Fail when a command failed + - name: Fail play if any command failed ansible.builtin.assert: that: - item.rc == 0 - fail_msg: "Command failed: {{ item.item }} (rc={{ item.rc }})" + fail_msg: >- + Command failed on VM: {{ item.item }} + (rc={{ item.rc }}) + + STDERR: + {{ item.stderr | default('') | trim }} quiet: true loop: "{{ vm_cmds.results }}" + loop_control: + label: "{{ item.item }}" diff --git a/nextcloud/update_nextcloud_v2.yml b/nextcloud/update_nextcloud_v2.yml index e39143f..16b3265 100644 --- a/nextcloud/update_nextcloud_v2.yml +++ b/nextcloud/update_nextcloud_v2.yml @@ -1,77 +1,162 @@ # nextcloud/update_nextcloud.yml - name: Update Nextcloud on VM via Proxmox - hosts: proxmox_nextcloud # linux_servers + hosts: proxmox_nextcloud 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 + # Internal normalized variables + vm_use_sudo: "{{ use_sudo | default(false) | bool }}" + debug_level: "{{ DEBUG | default(0) | int }}" + retry_count: "{{ RETRIES | default(25) | int }}" - # --- Debug / retries --- - DEBUG: "{{ lookup('env', 'DEBUG') | default(0) | int }}" - RETRIES: "{{ lookup('env', 'RETRIES') | default(25) | int }}" - - # --- Nextcloud specifics --- + # Nextcloud configuration 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_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 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) --- + 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 }}" - - >- - {{ docker_prefix }} exec -u www-data nextcloud php occ maintenance:mode --on - - >- - {{ docker_prefix }} exec nextcloud sh -c 'tar czf /tmp/nextcloud_conf.tgz -C /var/www/html config custom_apps' - - >- - {{ docker_prefix }} cp nextcloud:/tmp/nextcloud_conf.tgz "{{ backup_dir }}/nextcloud_conf.tgz" - - >- - {{ docker_prefix }} exec nextcloud rm /tmp/nextcloud_conf.tgz || true - - >- - {{ docker_prefix }} 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" + mkdir -p {{ backup_dir | quote }} - # --- Upgrade phase commands (run on VM) --- + - >- + {{ 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 }} -f {{ nextcloud_compose_file }} pull {{ nextcloud_service }} + {{ docker_prefix }} + compose + -p {{ nextcloud_project | quote }} + -f {{ nextcloud_compose_file | quote }} + pull {{ nextcloud_service | quote }} + - >- - {{ docker_prefix }} compose -p {{ nextcloud_project }} -f {{ nextcloud_compose_file }} up -d --no-deps --force-recreate {{ nextcloud_service }} + {{ docker_prefix }} + compose + -p {{ nextcloud_project | quote }} + -f {{ nextcloud_compose_file | quote }} + up -d + --no-deps + --force-recreate + {{ nextcloud_service | quote }} + - >- - {{ docker_prefix }} exec -u www-data nextcloud php occ maintenance:mode --off + 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 php occ upgrade + {{ docker_prefix }} + exec -u www-data {{ nextcloud_container }} + php occ upgrade + - >- - {{ docker_prefix }} exec -u www-data nextcloud php occ app:update --all || true + {{ docker_prefix }} + exec -u www-data {{ nextcloud_container }} + php occ app:update --all + - >- - {{ docker_prefix }} exec -u www-data nextcloud php occ maintenance:repair --include-expensive || true + {{ docker_prefix }} + exec -u www-data {{ nextcloud_container }} + php occ maintenance:repair --include-expensive + - >- - {{ docker_prefix }} exec -u www-data nextcloud php occ maintenance:mode --on + {{ 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 (for password-based SSH) + - name: Ensure sshpass is installed ansible.builtin.apt: name: sshpass state: present - update_cache: yes + update_cache: true - - name: Nextcloud | Show current version before upgrade (DEBUG) + - name: Nextcloud | Show current version before upgrade ansible.builtin.command: argv: - sshpass @@ -80,22 +165,46 @@ - -o - StrictHostKeyChecking=no - -o + - UserKnownHostsFile=/dev/null + - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - - bash - - -lc - - 'docker exec -u www-data nextcloud php occ -V || true' + - >- + {{ + ( + '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 == 1 + when: debug_level == 1 - # ------------------------- - # Backup phase - # ------------------------- - - name: Nextcloud | Run backup commands on VM (via SSH) # run plain commands via SSH + - 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 @@ -104,46 +213,64 @@ - -o - StrictHostKeyChecking=no - -o + - UserKnownHostsFile=/dev/null + - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - - "{{ ('sudo ' if use_sudo else '') + item }}" + - >- + {{ + ('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: idx - label: "backup-cmd-{{ idx }}" + index_var: backup_index + label: "backup-cmd-{{ backup_index }}" register: nc_backup_cmds changed_when: false - no_log: "{{ DEBUG == 0 }}" + failed_when: false - - name: Nextcloud | Show outputs of backup commands (DEBUG) + - name: Nextcloud | Show backup command outputs ansible.builtin.debug: msg: | CMD: {{ item.item }} RC: {{ item.rc }} STDOUT: - {{ (item.stdout | default('')).strip() }} + {{ item.stdout | default('') | trim }} 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." + {{ item.stderr | default('') | trim }} loop: "{{ nc_backup_cmds.results }}" loop_control: - index_var: idx - label: "backup-cmd-{{ idx }}" + index_var: backup_debug_index + label: "backup-cmd-{{ backup_debug_index }}" + when: debug_level == 1 - # ------------------------- - # Upgrade phase - # ------------------------- + - 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 }}) - - name: Nextcloud | Run upgrade commands on VM (via SSH) + 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 @@ -152,45 +279,64 @@ - -o - StrictHostKeyChecking=no - -o + - UserKnownHostsFile=/dev/null + - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - - bash - - -lc - - "{{ ('sudo ' if use_sudo else '') + item }}" + - >- + {{ + ('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: idx - label: "upgrade-cmd-{{ idx }}" + index_var: upgrade_index + label: "upgrade-cmd-{{ upgrade_index }}" register: nc_upgrade_cmds changed_when: false failed_when: false - no_log: "{{ DEBUG == 0 }}" - - name: Nextcloud | Show outputs of upgrade commands (DEBUG) + - name: Nextcloud | Show upgrade command outputs ansible.builtin.debug: msg: | CMD: {{ item.item }} RC: {{ item.rc }} STDOUT: - {{ (item.stdout | default('')).strip() }} + {{ item.stdout | default('') | trim }} 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." + {{ item.stderr | default('') | trim }} loop: "{{ nc_upgrade_cmds.results }}" loop_control: - index_var: idx - label: "upgrade-cmd-{{ idx }}" + index_var: upgrade_debug_index + label: "upgrade-cmd-{{ upgrade_debug_index }}" + when: debug_level == 1 - - name: Nextcloud | Disable maintenance mode (only after successful upgrade) + - 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 @@ -199,20 +345,54 @@ - -o - StrictHostKeyChecking=no - -o + - UserKnownHostsFile=/dev/null + - -o - ConnectTimeout=15 - "{{ vm_user }}@{{ vm_ip }}" - - "{{ ('sudo ' if use_sudo else '') }}docker exec -u www-data nextcloud php occ maintenance:mode --off" + - >- + {{ + ( + '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 - no_log: "{{ DEBUG == 0 }}" + failed_when: false - # ------------------------- - # Readiness check (status.php) - # ------------------------- + - 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 }}). - - name: Nextcloud | Wait for status.php (controller first) + 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 @@ -222,13 +402,15 @@ register: nc_status_controller delegate_to: localhost run_once: true - retries: "{{ RETRIES }}" + retries: "{{ retry_count }}" delay: 4 - until: nc_status_controller.status == 200 + until: + - nc_status_controller.status | default(0) == 200 + - nc_status_controller.json is defined failed_when: false changed_when: false - - name: Nextcloud | VM-side fetch status.php (JSON via Python) + - name: Nextcloud | Fetch status.php from VM ansible.builtin.command: argv: - sshpass @@ -237,51 +419,81 @@ - -o - StrictHostKeyChecking=no - -o + - UserKnownHostsFile=/dev/null + - -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 + - >- + {{ + ( + '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 - no_log: "{{ DEBUG == 0 }}" + when: + - nc_status_controller.status | default(0) != 200 + or nc_status_controller.json is not defined - - name: Nextcloud | Choose status JSON (controller wins, else VM) + - name: Nextcloud | Use controller status JSON 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 + nextcloud_status_json: "{{ nc_status_controller.json }}" + when: + - nc_status_controller.status | default(0) == 200 + - nc_status_controller.json is defined - - name: Nextcloud | Print concise status summary (DEBUG) + - 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('?') }}) - 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 + needsDbUpgrade={{ + nextcloud_status_json.needsDbUpgrade | default('?') + }})