feat: proxmox_upgrade role and playbook
This commit is contained in:
186
roles/proxmox_upgrade/tasks/drain.yml
Normal file
186
roles/proxmox_upgrade/tasks/drain.yml
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# proxmox_upgrade — drain.yml
|
||||
# Migrate all VMs/LXCs off a node before upgrading it
|
||||
# Uses Proxmox API — runs delegate_to: localhost
|
||||
# =============================================================================
|
||||
|
||||
# ── Get all guests on this node ───────────────────────────────────────────────
|
||||
- name: Drain | Get all VMs on node {{ current_node }}
|
||||
ansible.builtin.uri:
|
||||
url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ current_node }}/qemu"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}"
|
||||
validate_certs: false
|
||||
register: node_vms
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Drain | Get all LXCs on node {{ current_node }}
|
||||
ansible.builtin.uri:
|
||||
url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ current_node }}/lxc"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}"
|
||||
validate_certs: false
|
||||
register: node_lxcs
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Drain | Get available target nodes
|
||||
ansible.builtin.uri:
|
||||
url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}"
|
||||
validate_certs: false
|
||||
register: all_nodes
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Drain | Build target node list (exclude current node)
|
||||
ansible.builtin.set_fact:
|
||||
migration_targets: >-
|
||||
{{ all_nodes.json.data
|
||||
| selectattr('status', 'equalto', 'online')
|
||||
| rejectattr('node', 'equalto', current_node)
|
||||
| map(attribute='node')
|
||||
| list }}
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Drain | Fail if no migration targets available
|
||||
ansible.builtin.fail:
|
||||
msg: "No online nodes available to migrate guests to. Cannot drain {{ current_node }}."
|
||||
when: migration_targets | length == 0
|
||||
delegate_to: localhost
|
||||
|
||||
# ── Classify VMs — live migratable vs needs fallback ─────────────────────────
|
||||
- name: Drain | Get VM configs to check migratability
|
||||
ansible.builtin.uri:
|
||||
url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ current_node }}/qemu/{{ item.vmid }}/config"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "PVEAPIToken={{ api_token_id }}={{ api_token_secret }}"
|
||||
validate_certs: false
|
||||
register: vm_configs
|
||||
loop: "{{ node_vms.json.data }}"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Drain | Build guest migration plan
|
||||
ansible.builtin.set_fact:
|
||||
migration_plan: >-
|
||||
{%- set plan = [] -%}
|
||||
{%- for vm in node_vms.json.data -%}
|
||||
{%- set cfg = vm_configs.results[loop.index0].json.data -%}
|
||||
{%- set tags = (vm.tags | default('')) .split(',') | map('trim') | list -%}
|
||||
{%- set excluded = tags | select('in', migrate_exclude_tags) | list | length > 0 -%}
|
||||
{%- set has_passthrough = 'hostpci0' in cfg or 'usb0' in cfg -%}
|
||||
{%- set has_local_disk = shared_storage == false -%}
|
||||
{%- set has_local_cdrom = cfg.values() | select('string') | select('match', '.*local.*\\.iso.*') | list | length > 0 -%}
|
||||
{%- set needs_fallback = has_passthrough or has_local_disk or has_local_cdrom -%}
|
||||
{%- if not excluded -%}
|
||||
{%- set _ = plan.append({
|
||||
'vmid': vm.vmid,
|
||||
'name': vm.name,
|
||||
'type': 'qemu',
|
||||
'status': vm.status,
|
||||
'needs_fallback': needs_fallback,
|
||||
'fallback_reason': ('passthrough' if has_passthrough else ('local_disk' if has_local_disk else ('local_cdrom' if has_local_cdrom else '')))
|
||||
}) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- for lxc in node_lxcs.json.data -%}
|
||||
{%- set tags = (lxc.tags | default('')) .split(',') | map('trim') | list -%}
|
||||
{%- set excluded = tags | select('in', migrate_exclude_tags) | list | length > 0 -%}
|
||||
{%- if not excluded -%}
|
||||
{%- set _ = plan.append({
|
||||
'vmid': lxc.vmid,
|
||||
'name': lxc.name,
|
||||
'type': 'lxc',
|
||||
'status': lxc.status,
|
||||
'needs_fallback': false,
|
||||
'fallback_reason': ''
|
||||
}) -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ plan }}
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Drain | Log migration plan
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Migration plan for {{ current_node }}:
|
||||
{% for g in migration_plan %}
|
||||
- {{ g.type | upper }} {{ g.vmid }} ({{ g.name }}) [{{ g.status }}]
|
||||
{% if g.needs_fallback %} ⚠ needs fallback ({{ g.fallback_reason }}) — action: {{ live_migrate_fallback }}{% endif %}
|
||||
{% endfor %}
|
||||
delegate_to: localhost
|
||||
|
||||
# ── Abort if any guests need fallback and live_migrate_fallback is 'migrate' ──
|
||||
- name: Drain | Warn about non-migratable guests
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
WARNING — {{ item.type | upper }} {{ item.vmid }} ({{ item.name }})
|
||||
cannot be live migrated ({{ item.fallback_reason }}).
|
||||
live_migrate_fallback={{ live_migrate_fallback }} —
|
||||
{% if live_migrate_fallback == 'skip' %}
|
||||
THIS VM WILL GO DOWN DURING NODE REBOOT.
|
||||
{% elif live_migrate_fallback == 'shutdown' %}
|
||||
Will be shut down, cold migrated, and restarted.
|
||||
{% else %}
|
||||
Will attempt live migrate anyway (may fail).
|
||||
{% endif %}
|
||||
loop: "{{ migration_plan | selectattr('needs_fallback') | list }}"
|
||||
delegate_to: localhost
|
||||
|
||||
# ── Perform migrations ────────────────────────────────────────────────────────
|
||||
- name: Drain | Migrate guests (sequential)
|
||||
when: not migration_bulk | bool
|
||||
include_tasks: migrate_guest.yml
|
||||
loop: "{{ migration_plan | rejectattr('needs_fallback') | list + migration_plan | selectattr('needs_fallback') | rejectattr('needs_fallback' if live_migrate_fallback == 'skip' else 'nonexistent') | list }}"
|
||||
loop_var: guest
|
||||
|
||||
- name: Drain | Migrate guests (bulk — fire all at once)
|
||||
when: migration_bulk | bool
|
||||
block:
|
||||
- name: Drain | Bulk | Trigger all live migrations simultaneously
|
||||
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: "{{ 1 if not guest.needs_fallback else 0 }}"
|
||||
validate_certs: false
|
||||
register: bulk_migration_tasks
|
||||
loop: "{{ migration_plan | rejectattr('needs_fallback') | list }}"
|
||||
loop_var: guest
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Drain | Bulk | Wait for all migrations to complete
|
||||
ansible.builtin.uri:
|
||||
url: "https://{{ api_host }}:{{ api_port }}/api2/json/nodes/{{ current_node }}/tasks/{{ item.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
|
||||
loop: "{{ bulk_migration_tasks.results }}"
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Drain | Bulk | Check all migrations succeeded
|
||||
ansible.builtin.fail:
|
||||
msg: "Migration task failed for VMID — exitstatus: {{ item.json.data.exitstatus }}"
|
||||
loop: "{{ task_status.results }}"
|
||||
when: item.json.data.exitstatus != 'OK'
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Drain | Bulk | Handle fallback guests sequentially
|
||||
include_tasks: migrate_guest.yml
|
||||
loop: "{{ migration_plan | selectattr('needs_fallback') | list }}"
|
||||
loop_var: guest
|
||||
when: live_migrate_fallback != 'skip'
|
||||
|
||||
Reference in New Issue
Block a user