Files
ansible-msp-automations/roles/proxmox_upgrade/tasks/migrate_guest.yml

109 lines
5.3 KiB
YAML

---
# =============================================================================
# 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