--- - name: Install GuestOps backup and monitoring operations hosts: guestops become: true gather_facts: true vars: guestops_root: /srv/guestops guestops_current: "{{ guestops_root }}/current" guestops_config_root: /etc/guestops guestops_backup_root: /var/backups/guestops guestops_gnupg_root: /var/lib/guestops-backup/gnupg guestops_zabbix_service: zabbix-agent2 guestops_zabbix_include_directory: /etc/zabbix/zabbix_agent2.d guestops_enable_backup_schedule: false guestops_durable_logs_configured: false pre_tasks: - name: Validate required operational variables ansible.builtin.assert: that: - guestops_release_commit is match('^[0-9a-f]{40}$') - guestops_backup_recipient is match('^[A-Fa-f0-9]{40}$') - guestops_recovery_public_key | length > 0 - guestops_evidence_directory | length > 0 - guestops_durable_logs_configured | bool fail_msg: Release commit, recovery public key/fingerprint, evidence directory and reviewed durable logging are required. - name: Confirm selected release link ansible.builtin.stat: path: "{{ guestops_current }}" follow: false register: guestops_selected_release - name: Require deployed release before operations installation ansible.builtin.assert: that: - guestops_selected_release.stat.exists - guestops_selected_release.stat.islnk - guestops_selected_release.stat.lnk_source == guestops_root + '/releases/' + guestops_release_commit fail_msg: /srv/guestops/current must select the exact approved release commit. - name: Confirm selected release target exists ansible.builtin.stat: path: "{{ guestops_root }}/releases/{{ guestops_release_commit }}" follow: true register: guestops_release_target - name: Require selected release directory ansible.builtin.assert: that: - guestops_release_target.stat.exists - guestops_release_target.stat.isdir fail_msg: The selected release target must be an installed directory. - name: Inspect pre-provisioned private operational files ansible.builtin.stat: path: "{{ item.path }}" follow: false loop: - { path: /etc/guestops/backup.env, mode: "0600" } - { path: /etc/guestops/backup-transfer.env, mode: "0600" } - { path: /etc/guestops/backup-transfer.key, mode: "0600" } - { path: /etc/guestops/backup-known-hosts, mode: "0644" } register: guestops_operational_files - name: Require private backup and transfer configuration ansible.builtin.assert: that: - item.stat.exists - item.stat.isreg - not item.stat.islnk - item.stat.mode == item.item.mode fail_msg: "{{ item.item.path }} must be a regular non-symlink file with mode {{ item.item.mode }}." loop: "{{ guestops_operational_files.results }}" no_log: true tasks: - name: Create private backup directories ansible.builtin.file: path: "{{ item }}" state: directory owner: root group: root mode: "0700" loop: - "{{ guestops_backup_root }}" - "{{ guestops_gnupg_root }}" - name: Stage recovery public key ansible.builtin.copy: src: "{{ guestops_recovery_public_key }}" dest: /etc/guestops/recovery-public.asc owner: root group: root mode: "0600" - name: Import recovery public key only ansible.builtin.command: argv: - gpg - --batch - --homedir - "{{ guestops_gnupg_root }}" - --import - /etc/guestops/recovery-public.asc no_log: true changed_when: true - name: Verify configured recovery fingerprint ansible.builtin.command: argv: - gpg - --batch - --homedir - "{{ guestops_gnupg_root }}" - --list-keys - "{{ guestops_backup_recipient }}" changed_when: false no_log: true - name: Remove staged recovery public key ansible.builtin.file: path: /etc/guestops/recovery-public.asc state: absent - name: Install GuestOps systemd units ansible.builtin.copy: src: "{{ playbook_dir }}/../systemd/{{ item }}" dest: "/etc/systemd/system/{{ item }}" owner: root group: root mode: "0644" loop: - guestops-backup.service - guestops-backup.timer - guestops-backup-transfer.service - guestops-backup-transfer.timer - guestops-monitor-status.service - guestops-monitor-status.timer register: guestops_systemd_units - name: Verify installed systemd units ansible.builtin.command: argv: - systemd-analyze - verify - /etc/systemd/system/guestops-backup.service - /etc/systemd/system/guestops-backup.timer - /etc/systemd/system/guestops-backup-transfer.service - /etc/systemd/system/guestops-backup-transfer.timer - /etc/systemd/system/guestops-monitor-status.service - /etc/systemd/system/guestops-monitor-status.timer changed_when: false - name: Reload systemd unit definitions ansible.builtin.systemd_service: daemon_reload: true when: guestops_systemd_units.changed - name: Enable transfer retry and monitoring timers ansible.builtin.systemd_service: name: "{{ item }}" enabled: true state: started loop: - guestops-backup-transfer.timer - guestops-monitor-status.timer - name: Select backup schedule state ansible.builtin.systemd_service: name: guestops-backup.timer enabled: "{{ guestops_enable_backup_schedule | bool }}" state: "{{ 'started' if guestops_enable_backup_schedule | bool else 'stopped' }}" - name: Require Zabbix include directory ansible.builtin.stat: path: "{{ guestops_zabbix_include_directory }}" register: guestops_zabbix_directory - name: Confirm Zabbix agent configuration path ansible.builtin.assert: that: - guestops_zabbix_directory.stat.exists - guestops_zabbix_directory.stat.isdir fail_msg: Override guestops_zabbix_include_directory for the installed Zabbix agent. - name: Install restricted Zabbix status item ansible.builtin.copy: src: "{{ playbook_dir }}/../systemd/zabbix-agent-guestops.conf.example" dest: "{{ guestops_zabbix_include_directory }}/guestops.conf" owner: root group: root mode: "0644" register: guestops_zabbix_configuration - name: Restart Zabbix agent after configuration change ansible.builtin.service: name: "{{ guestops_zabbix_service }}" enabled: true state: restarted when: guestops_zabbix_configuration.changed - name: Generate initial non-sensitive monitoring status ansible.builtin.command: argv: [systemctl, start, guestops-monitor-status.service] changed_when: true - name: Read installed timer schedule ansible.builtin.command: argv: [systemctl, list-timers, --all, --no-pager, guestops-backup.timer, guestops-backup-transfer.timer, guestops-monitor-status.timer] register: guestops_timer_status changed_when: false - name: Retain monitoring status on the Ansible controller ansible.builtin.fetch: src: /run/guestops-monitor/status.json dest: "{{ guestops_evidence_directory }}/monitor-status.json" flat: true - name: Retain timer status on the Ansible controller ansible.builtin.copy: content: "{{ guestops_timer_status.stdout }}\n" dest: "{{ guestops_evidence_directory }}/systemd-timers.txt" mode: "0600" delegate_to: localhost become: false