--- # ============================================================================= # hypervisor_backup_config — main tasks # ============================================================================= # Strategy: # - Create a tarball on the remote node (handles symlinks, permissions cleanly) # - Fetch the tarball to Semaphore host # - For git: extract into repo path, add/commit/push, clean up # - For local: leave tarball on the node, rotate old ones # - For sftp: transfer tarball to remote, clean up temp - name: Gather date/time facts ansible.builtin.setup: gather_subset: - date_time when: ansible_date_time is not defined # ── Set backup paths based on hypervisor type ───────────────────────────────── - name: Set backup paths for proxmox ansible.builtin.set_fact: pve_config_backup_paths: "{{ pve_config_backup_paths_proxmox }}" when: hypervisor_type == 'proxmox' - name: Set backup paths for xcpng ansible.builtin.set_fact: pve_config_backup_paths: "{{ pve_config_backup_paths_xcpng }}" when: hypervisor_type == 'xcpng' - name: Fail if hypervisor_type not supported ansible.builtin.fail: msg: "hypervisor_type '{{ hypervisor_type }}' is not supported. Use proxmox or xcpng." when: hypervisor_type not in ['proxmox', 'xcpng'] # ── Create tarball on remote node ───────────────────────────────────────────── # Done once — reused by all destinations - name: Create config tarball on node ansible.builtin.shell: | tar czf /tmp/{{ pve_config_backup_filename }}.tar.gz \ --ignore-failed-read \ --dereference \ {{ pve_config_backup_paths | join(' ') }} 2>/dev/null || true echo "done" register: node_tarball changed_when: true # ── Git backup ──────────────────────────────────────────────────────────────── - name: Git backup when: "'git' in (pve_config_backup_destinations | map(attribute='type') | list)" block: - name: Git | Ensure extract path exists on Semaphore host ansible.builtin.file: path: "{{ pve_config_git_repo_dir }}/{{ pve_config_git_base_path }}" state: directory mode: '0755' delegate_to: localhost - name: Git | Fetch tarball from node to Semaphore host ansible.builtin.fetch: src: "/tmp/{{ pve_config_backup_filename }}.tar.gz" dest: "/tmp/{{ pve_config_backup_filename }}.tar.gz" flat: true - name: Git | Extract tarball into repo path ansible.builtin.shell: | tar xzf /tmp/{{ pve_config_backup_filename }}.tar.gz \ -C {{ pve_config_git_repo_dir }}/{{ pve_config_git_base_path }} \ --strip-components=0 2>/dev/null || true echo "extracted" delegate_to: localhost changed_when: true - name: Git | Clean up local temp tarball ansible.builtin.file: path: "/tmp/{{ pve_config_backup_filename }}.tar.gz" state: absent delegate_to: localhost - name: Git | Check if there are changes to commit ansible.builtin.shell: | cd {{ pve_config_git_repo_dir }} git status --porcelain {{ pve_config_git_base_path }}/ register: git_status delegate_to: localhost changed_when: false - name: Git | Pull latest before committing ansible.builtin.shell: | cd {{ pve_config_git_repo_dir }} git pull origin {{ pve_config_git_branch }} --rebase delegate_to: localhost when: git_status.stdout != "" changed_when: false - name: Git | Stage backup files ansible.builtin.shell: | cd {{ pve_config_git_repo_dir }} git add {{ pve_config_git_base_path }}/ delegate_to: localhost when: git_status.stdout != "" - name: Git | Commit backup ansible.builtin.shell: | cd {{ pve_config_git_repo_dir }} git -c user.name="ansible-msp" -c user.email="ansible@msp.local" \ commit -m "{{ pve_config_git_commit_message }}" delegate_to: localhost when: git_status.stdout != "" register: git_commit - name: Git | Push to remote ansible.builtin.shell: | cd {{ pve_config_git_repo_dir }} git push origin {{ pve_config_git_branch }} delegate_to: localhost when: git_status.stdout != "" - name: Git | Log result ansible.builtin.debug: msg: "{{ 'Config backed up to git: ' + pve_config_git_base_path if git_status.stdout != '' else 'No config changes since last backup — git commit skipped' }}" # ── Local backup ────────────────────────────────────────────────────────────── - name: Local backup when: "'local' in (pve_config_backup_destinations | map(attribute='type') | list)" block: - name: Local | Ensure backup dir exists on node ansible.builtin.file: path: "{{ pve_config_local_backup_dir }}" state: directory mode: '0700' - name: Local | Move tarball to backup dir ansible.builtin.shell: | cp /tmp/{{ pve_config_backup_filename }}.tar.gz \ {{ pve_config_local_backup_dir }}/{{ pve_config_backup_filename }}.tar.gz changed_when: true - name: Local | Remove old backups beyond keep limit ansible.builtin.shell: | ls -1t {{ pve_config_local_backup_dir }}/{{ hypervisor_type }}_{{ client_id | lower | replace('-','_') | replace(' ','_') }}_{{ inventory_hostname }}_config_*.tar.gz 2>/dev/null \ | tail -n +{{ (pve_config_backup_keep | int) + 1 }} \ | xargs -r rm -f changed_when: false - name: Local | Log result ansible.builtin.debug: msg: "Config backed up locally: {{ pve_config_local_backup_dir }}/{{ pve_config_backup_filename }}.tar.gz" # ── SFTP backup ─────────────────────────────────────────────────────────────── - name: SFTP backup when: "'sftp' in (pve_config_backup_destinations | map(attribute='type') | list)" block: - name: SFTP | Validate required vars are set ansible.builtin.fail: msg: "sftp destination requires pve_config_sftp_host and pve_config_sftp_user to be set" when: pve_config_sftp_host == "" or pve_config_sftp_user == "" - name: SFTP | Fetch tarball to Semaphore host first ansible.builtin.fetch: src: "/tmp/{{ pve_config_backup_filename }}.tar.gz" dest: "/tmp/{{ pve_config_backup_filename }}.tar.gz" flat: true - name: SFTP | Transfer tarball to remote host ansible.builtin.shell: | sftp_opts="-o StrictHostKeyChecking=no -o BatchMode=yes" {% if pve_config_sftp_key != "" %} sftp_opts="$sftp_opts -i {{ pve_config_sftp_key }}" {% endif %} sftp $sftp_opts {{ pve_config_sftp_user }}@{{ pve_config_sftp_host }} << EOF cd {{ pve_config_sftp_remote_dir }} put /tmp/{{ pve_config_backup_filename }}.tar.gz EOF delegate_to: localhost changed_when: true - name: SFTP | Clean up local temp tarball ansible.builtin.file: path: "/tmp/{{ pve_config_backup_filename }}.tar.gz" state: absent delegate_to: localhost - name: SFTP | Log result ansible.builtin.debug: msg: "Config backed up via sftp: {{ pve_config_sftp_host }}:{{ pve_config_sftp_remote_dir }}/{{ pve_config_backup_filename }}.tar.gz" # ── Clean up temp tarball on node ───────────────────────────────────────────── - name: Clean up temp tarball on node ansible.builtin.file: path: "/tmp/{{ pve_config_backup_filename }}.tar.gz" state: absent