refactor: proxmox_upgrade use inline Python for all API calls
This commit is contained in:
@@ -6,111 +6,103 @@
|
||||
# guest = { vmid, name, type, status, needs_fallback, fallback_reason }
|
||||
# =============================================================================
|
||||
|
||||
- name: "Migrate | {{ guest.type | upper }} {{ guest.vmid }} ({{ guest.name }}) — skip check"
|
||||
- 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 }}) — live_migrate_fallback=skip, will go down during reboot"
|
||||
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:
|
||||
# ── 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 }} | Execute migration"
|
||||
ansible.builtin.shell: |
|
||||
python3 << 'PYEOF'
|
||||
import urllib.request, json, ssl, time
|
||||
|
||||
- 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
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
# ── 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
|
||||
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 }}
|
||||
|
||||
# ── 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
|
||||
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"]
|
||||
|
||||
- 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: 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)
|
||||
|
||||
# ── 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
|
||||
# ── 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})
|
||||
|
||||
- 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
|
||||
# ── 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)
|
||||
|
||||
- name: "Migrate | {{ guest.vmid }} ({{ guest.name }}) | Migration complete"
|
||||
# ── 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: >-
|
||||
{{ 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 %}
|
||||
msg: "{{ migrate_result.stdout_lines }}"
|
||||
delegate_to: localhost
|
||||
|
||||
Reference in New Issue
Block a user