From 6df0813628999321254eba6cb09959129f80865d Mon Sep 17 00:00:00 2001 From: Samuel James Date: Sat, 29 Aug 2026 11:34:54 +0000 Subject: [PATCH] backup-homarr: rootless rclone install + delete older R2 versions --- playbooks/backup-homarr.yml | 58 +++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/playbooks/backup-homarr.yml b/playbooks/backup-homarr.yml index 471b488..95035b7 100644 --- a/playbooks/backup-homarr.yml +++ b/playbooks/backup-homarr.yml @@ -137,17 +137,39 @@ group: "{{ deploy_user }}" mode: "0750" - - name: Ensure rclone is installed (rootless via user bin if needed) + - name: Ensure rclone is installed (ROOTLESS — no sudo; installs to ~/.local/bin) block: - - name: rclone present? - ansible.builtin.command: rclone version + - name: rclone present on PATH or in ~/.local/bin? + ansible.builtin.shell: 'command -v rclone || command -v "$HOME/.local/bin/rclone"' register: rc_ver changed_when: false failed_when: false - - name: Install rclone via official script (needs sudo) - ansible.builtin.shell: curl -fsSL https://rclone.org/install.sh | sudo bash + + - name: Install rclone rootless (download static binary to ~/.local/bin) when: rc_ver.rc != 0 - changed_when: true + block: + - name: Ensure ~/.local/bin exists + ansible.builtin.file: + path: "/home/{{ deploy_user }}/.local/bin" + state: directory + mode: "0755" + + - name: Download + unpack latest rclone linux-amd64 into ~/.local/bin (rootless, python unzip) + ansible.builtin.shell: | + set -e + D="$(mktemp -d)"; cd "$D" + curl -fsSL https://downloads.rclone.org/rclone-current-linux-amd64.zip -o rclone.zip + python3 -c "import zipfile,glob,shutil,os,stat; z=zipfile.ZipFile('rclone.zip'); z.extractall('.'); b=glob.glob('rclone-*-linux-amd64/rclone')[0]; dst=os.path.expanduser('~/.local/bin/rclone'); shutil.copy(b,dst); os.chmod(dst, 0o755)" + changed_when: true + + - name: Resolve rclone binary path (PATH or ~/.local/bin) + ansible.builtin.shell: 'command -v rclone || echo "/home/{{ deploy_user }}/.local/bin/rclone"' + register: rclone_bin_res + changed_when: false + + - name: Set rclone binary fact + ansible.builtin.set_fact: + rclone_bin: "{{ rclone_bin_res.stdout_lines[0] }}" - name: "Stop Homarr (consistent sqlite snapshot)" community.docker.docker_compose_v2: @@ -192,7 +214,7 @@ - name: Upload tar to R2 (r2:/homarr/) ansible.builtin.command: - cmd: rclone copyto "{{ backup_dir }}/{{ backup_name }}" "r2:{{ r2_bucket }}/homarr/{{ backup_name }}" --s3-no-check-bucket + cmd: "{{ rclone_bin }} copyto \"{{ backup_dir }}/{{ backup_name }}\" \"r2:{{ r2_bucket }}/homarr/{{ backup_name }}\" --s3-no-check-bucket" environment: "{{ rclone_env }}" register: upload changed_when: true @@ -200,12 +222,29 @@ - name: Verify object exists in R2 ansible.builtin.command: - cmd: rclone lsf "r2:{{ r2_bucket }}/homarr/{{ backup_name }}" + cmd: "{{ rclone_bin }} lsf \"r2:{{ r2_bucket }}/homarr/{{ backup_name }}\"" environment: "{{ rclone_env }}" register: verify changed_when: false failed_when: backup_name not in verify.stdout + - name: List existing Homarr backups in R2 for this host + ansible.builtin.command: + cmd: "{{ rclone_bin }} lsf \"r2:{{ r2_bucket }}/homarr/\"" + environment: "{{ rclone_env }}" + register: r2_listing + changed_when: false + + - name: Delete OLDER R2 backups for this host (keep only the one just uploaded) + ansible.builtin.command: + cmd: "{{ rclone_bin }} deletefile \"r2:{{ r2_bucket }}/homarr/{{ item }}\"" + environment: "{{ rclone_env }}" + loop: "{{ r2_listing.stdout_lines }}" + when: + - item is match('homarr-' ~ inventory_hostname ~ '-.*\.tar$') + - item != backup_name + changed_when: true + - name: Prune old LOCAL tars (keep {{ keep_local }} newest) ansible.builtin.shell: >- ls -1t "{{ backup_dir }}"/homarr-*.tar 2>/dev/null | tail -n +{{ keep_local + 1 }} | xargs -r rm -f @@ -216,7 +255,8 @@ ansible.builtin.debug: msg: >- Backed up {{ backup_name }} ({{ (bkstat.stat.size / 1048576) | round(1) }} MB) - -> r2:{{ r2_bucket }}/homarr/ ; Homarr healthy (HTTP {{ health.status }}). + -> r2:{{ r2_bucket }}/homarr/ (older versions for {{ inventory_hostname }} deleted) ; + Homarr healthy (HTTP {{ health.status }}). - name: "Cleanup runner temp key" hosts: localhost