--- # ============================================================================= # proxmox_upgrade — migrate_guest.yml # Handles migration of a single VM or LXC # Called with loop_var: guest # guest = { vmid, name, type, status, needs_fallback, fallback_reason } # ============================================================================= - name: "Migrate | {{ guest.type | upper }} {{ guest.vmid }} ({{ guest.name }}) — skip check" ansible.builtin.debug: msg: "SKIPPING {{ guest.type | upper }} {{ guest.vmid }} ({{ guest.name }}) — live_migrate_fallback=skip, will go down during reboot" when: guest.needs_fallback and live_migrate_fallback == 'skip' delegate_to: localhost - name: "Migrate | {{ guest.type | upper }} {{ guest.vmid }} ({{ guest.name }})" when: not (guest.needs_fallback and live_migrate_fallback == 'skip') block: # ── Cold migration: shutdown first ─────────────────────────────────────── - name: "Migrate | {{ guest.vmid }} | Shutdown for cold migration" ansible.builtin.uri: url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ current_node }}/{{ 'qemu' if guest.type == 'qemu' else 'lxc' }}/{{ guest.vmid }}/status/shutdown" method: POST headers: Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}" body_format: json body: timeout: "{{ vm_shutdown_timeout }}" forceStop: 1 validate_certs: false when: guest.needs_fallback and live_migrate_fallback == 'shutdown' and guest.status == 'running' delegate_to: localhost - name: "Migrate | {{ guest.vmid }} | Wait for shutdown" ansible.builtin.uri: url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ current_node }}/{{ 'qemu' if guest.type == 'qemu' else 'lxc' }}/{{ guest.vmid }}/status/current" method: GET headers: Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}" validate_certs: false register: vm_status until: vm_status.json.data.status == 'stopped' retries: "{{ (vm_shutdown_timeout | int / 5) | int }}" delay: 5 when: guest.needs_fallback and live_migrate_fallback == 'shutdown' and guest.status == 'running' delegate_to: localhost # ── Trigger migration ───────────────────────────────────────────────────── - name: "Migrate | {{ guest.vmid }} | Trigger migration to {{ migration_targets | first }}" ansible.builtin.uri: url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ current_node }}/{{ 'qemu' if guest.type == 'qemu' else 'lxc' }}/{{ guest.vmid }}/migrate" method: POST headers: Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}" body_format: json body: target: "{{ migration_targets | first }}" online: "{{ 0 if (guest.needs_fallback and live_migrate_fallback == 'shutdown') else 1 }}" validate_certs: false register: migration_task delegate_to: localhost # ── Wait for migration to complete ──────────────────────────────────────── - name: "Migrate | {{ guest.vmid }} | Wait for migration task to complete" ansible.builtin.uri: url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ current_node }}/tasks/{{ migration_task.json.data }}/status" method: GET headers: Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}" validate_certs: false register: task_status until: task_status.json.data.status == 'stopped' retries: 60 delay: 10 delegate_to: localhost - name: "Migrate | {{ guest.vmid }} | Verify migration succeeded" ansible.builtin.fail: msg: "Migration of {{ guest.type | upper }} {{ guest.vmid }} ({{ guest.name }}) failed — {{ task_status.json.data.exitstatus }}" when: task_status.json.data.exitstatus != 'OK' delegate_to: localhost # ── Cold migration: restart on target ──────────────────────────────────── - name: "Migrate | {{ guest.vmid }} | Start on target node after cold migration" ansible.builtin.uri: url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ migration_targets | first }}/{{ 'qemu' if guest.type == 'qemu' else 'lxc' }}/{{ guest.vmid }}/status/start" method: POST headers: Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}" validate_certs: false when: guest.needs_fallback and live_migrate_fallback == 'shutdown' and guest.status == 'running' delegate_to: localhost - name: "Migrate | {{ guest.vmid }} | Wait for VM to start on target" ansible.builtin.uri: url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ migration_targets | first }}/{{ 'qemu' if guest.type == 'qemu' else 'lxc' }}/{{ guest.vmid }}/status/current" method: GET headers: Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}" validate_certs: false register: vm_start_status until: vm_start_status.json.data.status == 'running' retries: "{{ (vm_start_timeout | int / 5) | int }}" delay: 5 when: guest.needs_fallback and live_migrate_fallback == 'shutdown' and guest.status == 'running' delegate_to: localhost - name: "Migrate | {{ guest.vmid }} ({{ guest.name }}) | Migration complete" ansible.builtin.debug: msg: >- {{ guest.type | upper }} {{ guest.vmid }} ({{ guest.name }}) {% if guest.needs_fallback and live_migrate_fallback == 'shutdown' %} cold migrated to {{ migration_targets | first }} and restarted {% else %} live migrated to {{ migration_targets | first }} {% endif %} delegate_to: localhost