From ca0f11b1c918aa7b348e7c4a219ccc78c8bc7ac6 Mon Sep 17 00:00:00 2001 From: Semaphore Date: Thu, 12 Mar 2026 21:44:20 -0700 Subject: [PATCH] =?UTF-8?q?Fix=20snapshot=20role=20=E2=80=94=20XO=20REST?= =?UTF-8?q?=20API,=20fix=20playbook=20host=20targeting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - roles/snapshot/tasks/main.yml: replace xe CLI with XO REST API - POST /rest/v0/vms//actions/snapshot?sync=true - stores returned snapshot UUID as snapshot_id - baremetal: skips gracefully with warning - playbooks/snapshot_pre.yml: target linux_hosts only (was all) - playbooks/linux_patch.yml: remove snapshot + report roles (snapshot is separate step) - playbooks/site_maintenance.yml: remove bootstrap play (handled per-playbook), remove windows_patch import (WinRM not implemented) --- playbooks/linux_patch.yml | 9 ++- playbooks/site_maintenance.yml | 17 +++-- playbooks/snapshot_pre.yml | 13 +++- roles/snapshot/tasks/main.yml | 109 ++++++++++++++++++++------------- 4 files changed, 97 insertions(+), 51 deletions(-) diff --git a/playbooks/linux_patch.yml b/playbooks/linux_patch.yml index ce13c64..c2b5b96 100644 --- a/playbooks/linux_patch.yml +++ b/playbooks/linux_patch.yml @@ -1,4 +1,10 @@ --- +# ============================================================================= +# playbooks/linux_patch.yml +# Linux-only patch run. Snapshots should be taken separately via snapshot_pre.yml +# before running this playbook, or use site_maintenance.yml for the full sequence. +# ============================================================================= + - name: Bootstrap — ensure Python is available hosts: linux_hosts gather_facts: false @@ -9,7 +15,6 @@ hosts: linux_hosts gather_facts: true roles: - - snapshot - preflight - linux_patch - - report + #- report diff --git a/playbooks/site_maintenance.yml b/playbooks/site_maintenance.yml index 0d81d20..d6b047b 100644 --- a/playbooks/site_maintenance.yml +++ b/playbooks/site_maintenance.yml @@ -1,11 +1,16 @@ --- -- name: Bootstrap — ensure Python is available - hosts: all - gather_facts: false - tasks: - - ansible.builtin.import_tasks: ../roles/preflight/tasks/bootstrap.yml +# ============================================================================= +# playbooks/site_maintenance.yml +# Full maintenance sequence: +# 1. Snapshot guest VMs (linux_hosts via XO/Proxmox API) +# 2. Preflight safety checks (linux_hosts + xcpng_hosts) +# 3. Linux patching (linux_hosts) +# +# Windows hosts are excluded — WinRM patching not yet implemented. +# XCP-NG pool updates are a separate template (xcpng_pool_update.yml) +# and should be run before this playbook during a maintenance window. +# ============================================================================= - import_playbook: snapshot_pre.yml - import_playbook: site_preflight.yml - import_playbook: linux_patch.yml -- import_playbook: windows_patch.yml diff --git a/playbooks/snapshot_pre.yml b/playbooks/snapshot_pre.yml index 1e2e08a..21796a7 100644 --- a/playbooks/snapshot_pre.yml +++ b/playbooks/snapshot_pre.yml @@ -1,12 +1,21 @@ --- +# ============================================================================= +# playbooks/snapshot_pre.yml +# Pre-patch snapshots for Linux guest VMs via hypervisor API. +# Targets linux_hosts only — snapshots taken per-VM before patching. +# XCP-NG: uses XO REST API (xcpng_vm_uuid required per host) +# Proxmox: uses community.general.proxmox_snap (proxmox_vmid required per host) +# Baremetal: snapshot tasks skipped automatically (no hypervisor_type match) +# ============================================================================= + - name: Bootstrap — ensure Python is available - hosts: all + hosts: linux_hosts gather_facts: false tasks: - ansible.builtin.import_tasks: ../roles/preflight/tasks/bootstrap.yml - name: Pre-patch snapshot - hosts: all + hosts: linux_hosts gather_facts: true roles: - snapshot diff --git a/roles/snapshot/tasks/main.yml b/roles/snapshot/tasks/main.yml index 23e8ee9..0455545 100644 --- a/roles/snapshot/tasks/main.yml +++ b/roles/snapshot/tasks/main.yml @@ -1,13 +1,32 @@ --- +# ============================================================================= +# roles/snapshot/tasks/main.yml +# +# Creates pre-patch snapshots for guest VMs. +# Runs against linux_hosts — each host needs xcpng_vm_uuid or proxmox_vmid set. +# +# Hypervisor dispatch: +# proxmox → community.general.proxmox_snap (delegate_to: localhost) +# xcpng → XO REST API snapshot action (delegate_to: localhost) +# baremetal → skipped entirely +# +# Required vars: +# XCP-NG: xcpng_vm_uuid, XO_URL, XO_TOKEN (from variable group) +# Proxmox: proxmox_vmid, PROXMOX_HOST, PROXMOX_TOKEN_ID, PROXMOX_TOKEN_SECRET +# ============================================================================= + +# ─── Proxmox snapshots ──────────────────────────────────────────────────────── + - name: Create pre-patch snapshot (Proxmox) community.general.proxmox_snap: - api_host: "{{ PROXMOX_HOST }}" - api_user: "{{ PROXMOX_TOKEN_ID | split('!') | first }}" - api_token_id: "{{ PROXMOX_TOKEN_ID | split('!') | last }}" + api_host: "{{ PROXMOX_HOST }}" + api_user: "{{ PROXMOX_TOKEN_ID | split('!') | first }}" + api_token_id: "{{ PROXMOX_TOKEN_ID | split('!') | last }}" api_token_secret: "{{ PROXMOX_TOKEN_SECRET }}" - vmid: "{{ proxmox_vmid }}" - state: present - snapname: "{{ snapshot_name_prefix }}-{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}" + vmid: "{{ proxmox_vmid }}" + state: present + snapname: >- + {{ snapshot_name_prefix }}-{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }} description: "Ansible pre-patch snapshot {{ ansible_date_time.iso8601 }}" register: proxmox_snapshot_result when: hypervisor_type == "proxmox" @@ -15,52 +34,60 @@ - name: Store Proxmox snapshot name ansible.builtin.set_fact: - snapshot_id: "{{ snapshot_name_prefix }}-{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}" + snapshot_id: >- + {{ snapshot_name_prefix }}-{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }} when: hypervisor_type == "proxmox" -- name: Create pre-patch snapshot (XCP-NG) - ansible.builtin.shell: | - xe vm-snapshot vm={{ xcpng_vm_uuid }} new-name-label="{{ snapshot_name_prefix }}-{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}" +# ─── XCP-NG snapshots via XO REST API ──────────────────────────────────────── + +- name: Create pre-patch snapshot (XCP-NG via XO REST API) + ansible.builtin.uri: + url: "{{ XO_URL }}/rest/v0/vms/{{ xcpng_vm_uuid }}/actions/snapshot?sync=true" + method: POST + headers: + Cookie: "authenticationToken={{ XO_TOKEN }}" + Content-Type: "application/json" + Accept: "application/json" + body_format: json + body: + name_label: >- + {{ snapshot_name_prefix }}-{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }} + validate_certs: "{{ xo_validate_certs | default(true) }}" + status_code: [200, 201] + timeout: 120 register: xcpng_snapshot_result - changed_when: xcpng_snapshot_result.rc == 0 - when: hypervisor_type == "xcpng" and xcpng_host | length > 0 - delegate_to: "{{ xcpng_host if xcpng_host | length > 0 else 'localhost' }}" + when: hypervisor_type == "xcpng" or hypervisor_type == "mixed" + delegate_to: localhost - name: Store XCP-NG snapshot UUID ansible.builtin.set_fact: - snapshot_id: "{{ xcpng_snapshot_result.stdout | trim }}" - when: hypervisor_type == "xcpng" + snapshot_id: "{{ xcpng_snapshot_result.json | regex_replace('\"', '') }}" + when: + - hypervisor_type == "xcpng" or hypervisor_type == "mixed" + - xcpng_snapshot_result is not skipped + - xcpng_snapshot_result.json is defined -- name: Verify snapshot was created (Proxmox) - community.general.proxmox_snap: - api_host: "{{ PROXMOX_HOST }}" - api_user: "{{ PROXMOX_TOKEN_ID | split('!') | first }}" - api_token_id: "{{ PROXMOX_TOKEN_ID | split('!') | last }}" - api_token_secret: "{{ PROXMOX_TOKEN_SECRET }}" - vmid: "{{ proxmox_vmid }}" - snapname: "{{ snapshot_id }}" - state: present - register: proxmox_snap_verify - when: hypervisor_type == "proxmox" - delegate_to: localhost +# ─── Verify and assert ─────────────────────────────────────────────────────── -- name: Verify snapshot was created (XCP-NG) - ansible.builtin.shell: | - xe snapshot-list uuid={{ snapshot_id }} | grep uuid - register: xcpng_snap_verify - changed_when: false - failed_when: xcpng_snap_verify.stdout == "" - when: hypervisor_type == "xcpng" and xcpng_host | length > 0 - delegate_to: "{{ xcpng_host if xcpng_host | length > 0 else 'localhost' }}" - -- name: Assert snapshot exists before proceeding +- name: Assert snapshot was created ansible.builtin.assert: that: - snapshot_id is defined - - snapshot_id != "" - fail_msg: "SNAPSHOT: Failed to create or verify snapshot for {{ inventory_hostname }}. Aborting patch run." - success_msg: "Snapshot verified: {{ snapshot_id }}" + - snapshot_id | length > 0 + fail_msg: >- + SNAPSHOT FAILED: Could not create or verify snapshot for {{ inventory_hostname }}. + Aborting to protect against unsnapshotted patch run. + success_msg: "Snapshot OK: {{ snapshot_id }} for {{ inventory_hostname }}" + when: hypervisor_type != "baremetal" -- name: Log snapshot ID +- name: Log snapshot result ansible.builtin.debug: msg: "Pre-patch snapshot created: {{ snapshot_id }} for {{ inventory_hostname }}" + when: hypervisor_type != "baremetal" + +- name: Log snapshot skipped (baremetal) + ansible.builtin.debug: + msg: >- + Snapshot skipped for {{ inventory_hostname }} — hypervisor_type is baremetal. + Ensure change approval is in place before patching. + when: hypervisor_type == "baremetal"