--- # ============================================================================= # 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 }}) — skipping (live_migrate_fallback=skip)" ansible.builtin.debug: msg: "SKIPPING {{ guest.type | upper }} {{ guest.vmid }} ({{ guest.name }}) — 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: - name: "Migrate | {{ guest.vmid }} | Execute migration" ansible.builtin.shell: | python3 << 'PYEOF' import urllib.request, json, ssl, time ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE api_base = "https://{{ api_host }}:{{ api_port }}/api2/json" headers = {"Authorization": "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}"} node = "{{ current_node }}" target = "{{ migration_targets | first }}" vmid = {{ guest.vmid }} gtype = "{{ guest.type }}" name = "{{ guest.name }}" status = "{{ guest.status }}" needs_fallback = {{ guest.needs_fallback | lower }} fallback = "{{ live_migrate_fallback }}" shutdown_timeout = {{ vm_shutdown_timeout }} start_timeout = {{ vm_start_timeout }} def api_req(path, method="GET", body=None): url = f"{api_base}{path}" data = json.dumps(body).encode() if body else None hdrs = {**headers} if data: hdrs["Content-Type"] = "application/json" req = urllib.request.Request(url, data=data, headers=hdrs, method=method) with urllib.request.urlopen(req, context=ctx) as r: return json.loads(r.read())["data"] # ── Cold migration: shutdown first ──────────────────────────────────── if needs_fallback and fallback == "shutdown" and status == "running": print(f"Shutting down {gtype.upper()} {vmid} ({name})...") api_req(f"/nodes/{node}/{gtype}/{vmid}/status/shutdown", "POST", {"timeout": shutdown_timeout, "forceStop": 1}) # Wait for stop for _ in range(shutdown_timeout // 5): s = api_req(f"/nodes/{node}/{gtype}/{vmid}/status/current") if s["status"] == "stopped": print(f" {vmid} stopped") break time.sleep(5) else: print(f"ERROR: {vmid} did not stop within {shutdown_timeout}s") exit(1) # ── Trigger migration ───────────────────────────────────────────────── online = 0 if (needs_fallback and fallback == "shutdown") else 1 print(f"Migrating {gtype.upper()} {vmid} ({name}) → {target} (online={online})...") task_id = api_req(f"/nodes/{node}/{gtype}/{vmid}/migrate", "POST", {"target": target, "online": online}) # ── Wait for migration task ─────────────────────────────────────────── for _ in range(60): t = api_req(f"/nodes/{node}/tasks/{task_id}/status") if t["status"] == "stopped": if t.get("exitstatus") != "OK": print(f"ERROR: migration failed — {t.get('exitstatus')}") exit(1) print(f" Migration complete: {t.get('exitstatus')}") break time.sleep(10) else: print(f"ERROR: migration task timed out") exit(1) # ── Cold migration: restart on target ───────────────────────────────── if needs_fallback and fallback == "shutdown" and status == "running": print(f"Starting {vmid} on {target}...") api_req(f"/nodes/{target}/{gtype}/{vmid}/status/start", "POST") for _ in range(start_timeout // 5): s = api_req(f"/nodes/{target}/{gtype}/{vmid}/status/current") if s["status"] == "running": print(f" {vmid} running on {target}") break time.sleep(5) else: print(f"WARNING: {vmid} did not start within {start_timeout}s — check manually") print(f"Done: {gtype.upper()} {vmid} ({name}) → {target}") PYEOF register: migrate_result delegate_to: localhost changed_when: true - name: "Migrate | {{ guest.vmid }} ({{ guest.name }}) | Log result" ansible.builtin.debug: msg: "{{ migrate_result.stdout_lines }}" delegate_to: localhost