--- - name: Guard Gyro VMID before API updates hosts: mini-pc gather_facts: false tasks: - name: Read existing VMID 150 configuration ansible.builtin.command: pct config 150 register: gyro_existing_vmid check_mode: false changed_when: false failed_when: false - name: Refuse to modify a foreign VMID 150 ansible.builtin.assert: that: - gyro_existing_vmid.rc != 0 or gyro_existing_hostname == 'gyro' fail_msg: VMID 150 already exists and is not the Gyro container. vars: gyro_existing_hostname: >- {{ gyro_existing_vmid.stdout_lines | select('match', '^hostname: ') | map('regex_replace', '^hostname: ', '') | first | default('') }} - name: Create Gyro LXC on mini-pc hosts: localhost connection: local become: false gather_facts: false vars: ansible_become: false ansible_python_interpreter: "{{ ansible_playbook_python }}" pve_lxc_vmid: 150 pve_lxc_node: mini-pc pve_lxc_hostname: gyro pve_lxc_ip: 192.168.1.35/24 pve_lxc_gateway: 192.168.1.1 pve_lxc_disk: local-lvm:2 pve_lxc_cores: 1 pve_lxc_memory: 512 pve_lxc_swap: 256 pve_lxc_startup: order=80 pve_lxc_unprivileged: true pve_lxc_update: false pve_lxc_features: [] pve_lxc_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst roles: - role: pve_lxc - name: Configure Gyro LXC isolation hosts: mini-pc gather_facts: false vars: gyro_vmid: 150 tasks: - name: Read Gyro LXC configuration ansible.builtin.command: "pct config {{ gyro_vmid }}" register: gyro_lxc_config changed_when: false - name: Require expected Gyro LXC properties ansible.builtin.assert: that: - "'hostname: gyro' in gyro_lxc_config.stdout" - "'unprivileged: 1' in gyro_lxc_config.stdout" - "'rootfs: local-lvm:' in gyro_lxc_config.stdout" - "'onboot: 1' in gyro_lxc_config.stdout" - name: Check for an existing Gyro firewall file ansible.builtin.stat: path: "/etc/pve/firewall/{{ gyro_vmid }}.fw" register: gyro_firewall_file - name: Preserve the existing Gyro firewall file ansible.builtin.slurp: src: "/etc/pve/firewall/{{ gyro_vmid }}.fw" register: gyro_previous_firewall when: gyro_firewall_file.stat.exists - name: Apply and verify Gyro firewall block: - name: Render Proxmox firewall for Gyro ansible.builtin.copy: dest: "/tmp/gyro-{{ gyro_vmid }}.fw" owner: root group: root mode: "0640" content: | [OPTIONS] enable: 1 policy_in: DROP policy_out: ACCEPT [RULES] IN ACCEPT -source {{ homelab_lan_cidr }} -p tcp -dport 22 -log nolog IN ACCEPT -source {{ openvpn_network_cidr }} -p tcp -dport 22 -log nolog OUT ACCEPT -dest 192.168.1.27 -p tcp -dport 7890 -log nolog OUT DROP -dest {{ homelab_lan_cidr }} -log nolog register: gyro_rendered_firewall changed_when: false - name: Compare rendered and active Gyro firewall ansible.builtin.command: >- cmp -s /tmp/gyro-{{ gyro_vmid }}.fw /etc/pve/firewall/{{ gyro_vmid }}.fw register: gyro_firewall_comparison changed_when: false failed_when: gyro_firewall_comparison.rc not in [0, 1] when: gyro_firewall_file.stat.exists - name: Install Proxmox firewall for Gyro ansible.builtin.command: >- cp /tmp/gyro-{{ gyro_vmid }}.fw /etc/pve/firewall/{{ gyro_vmid }}.fw when: not gyro_firewall_file.stat.exists or gyro_firewall_comparison.rc != 0 changed_when: true - name: Validate Proxmox firewall configuration ansible.builtin.command: pve-firewall compile changed_when: false - name: Read cluster firewall options ansible.builtin.command: pvesh get /cluster/firewall/options --output-format json register: gyro_cluster_firewall_options changed_when: false - name: Report staged Proxmox firewall state ansible.builtin.debug: msg: >- {{ 'Gyro Proxmox firewall is active.' if (gyro_cluster_firewall_options.stdout | from_json).enable | default(0) | int == 1 else 'Gyro Proxmox firewall is staged but inactive because the cluster firewall is disabled; UFW remains the enforced isolation layer.' }} - name: Verify SSH remains reachable through the firewall ansible.builtin.wait_for_connection: timeout: 180 delegate_to: gyro vars: ansible_become: false rescue: - name: Restore the previous Gyro firewall file ansible.builtin.copy: dest: "/tmp/gyro-{{ gyro_vmid }}-previous.fw" content: "{{ gyro_previous_firewall.content | b64decode }}" owner: root group: root mode: "0640" when: gyro_firewall_file.stat.exists - name: Reinstall the previous Gyro firewall file ansible.builtin.command: >- cp /tmp/gyro-{{ gyro_vmid }}-previous.fw /etc/pve/firewall/{{ gyro_vmid }}.fw when: gyro_firewall_file.stat.exists changed_when: true - name: Remove the failed new Gyro firewall file ansible.builtin.file: path: "/etc/pve/firewall/{{ gyro_vmid }}.fw" state: absent when: not gyro_firewall_file.stat.exists - name: Recompile restored Proxmox firewall configuration ansible.builtin.command: pve-firewall compile changed_when: false - name: Remove temporary Gyro firewall files after rollback ansible.builtin.file: path: "{{ item }}" state: absent loop: - "/tmp/gyro-{{ gyro_vmid }}.fw" - "/tmp/gyro-{{ gyro_vmid }}-previous.fw" - name: Stop after rolling back the Gyro firewall ansible.builtin.fail: msg: Gyro firewall validation or SSH reachability failed; the previous firewall state was restored. - name: Remove temporary Gyro firewall file ansible.builtin.file: path: "/tmp/gyro-{{ gyro_vmid }}.fw" state: absent changed_when: false - name: Wait for Gyro SSH hosts: gyro gather_facts: false tasks: - name: Wait for Gyro to accept SSH connections ansible.builtin.wait_for_connection: timeout: 180