testing new proxmox logic
This commit is contained in:
371
playbooks/proxmox_migrate_vms.yml
Normal file
371
playbooks/proxmox_migrate_vms.yml
Normal file
@@ -0,0 +1,371 @@
|
||||
---
|
||||
# =============================================================================
|
||||
# proxmox_migrate_vms.yml
|
||||
# Flexible VM migration playbook supporting three modes:
|
||||
#
|
||||
# drain — move all VMs off a specific node (pre-maintenance)
|
||||
# rebalance — redistribute VMs evenly across all online nodes by resources
|
||||
# restore — return VMs to their origin nodes using a drain state file
|
||||
# targeted — migrate specific VMIDs or tagged VMs to a specified target
|
||||
#
|
||||
# Usage examples:
|
||||
# # Drain a node before maintenance
|
||||
# ansible-playbook proxmox_migrate_vms.yml -e "migrate_mode=drain migrate_source_node=pm-node-01"
|
||||
#
|
||||
# # Rebalance the cluster
|
||||
# ansible-playbook proxmox_migrate_vms.yml -e "migrate_mode=rebalance"
|
||||
#
|
||||
# # Restore VMs to origin after maintenance
|
||||
# ansible-playbook proxmox_migrate_vms.yml -e "migrate_mode=restore migrate_source_node=pm-node-01"
|
||||
#
|
||||
# # Migrate specific VMIDs to a target node
|
||||
# ansible-playbook proxmox_migrate_vms.yml -e "migrate_mode=targeted migrate_vmids=[100,101] migrate_target_node=pm-node-02"
|
||||
#
|
||||
# # Migrate VMs by tag
|
||||
# ansible-playbook proxmox_migrate_vms.yml -e "migrate_mode=targeted migrate_tags=[win11] migrate_target_node=pm-node-02"
|
||||
# =============================================================================
|
||||
|
||||
- name: "Proxmox | Migrate VMs"
|
||||
hosts: proxmox_cluster
|
||||
gather_facts: true
|
||||
run_once: true
|
||||
|
||||
vars:
|
||||
# Mode: drain | rebalance | restore | targeted
|
||||
migrate_mode: drain
|
||||
|
||||
# Source node (required for drain and restore modes)
|
||||
migrate_source_node: ""
|
||||
|
||||
# Target node (required for targeted mode, optional for drain)
|
||||
migrate_target_node: ""
|
||||
|
||||
# Targeted mode filters
|
||||
migrate_vmids: [] # list of VMIDs to migrate
|
||||
migrate_tags: [] # list of tags to match
|
||||
|
||||
# Rebalance threshold — don't migrate if imbalance is below this % of total memory
|
||||
rebalance_threshold_pct: 10
|
||||
|
||||
# Shared drain role vars
|
||||
drain_target_strategy: "{{ 'explicit' if migrate_target_node != '' else 'resources' }}"
|
||||
drain_target_node: "{{ migrate_target_node }}"
|
||||
drain_state_dir: "/tmp/proxmox_drain_state"
|
||||
|
||||
# Restore vars
|
||||
restore_state_dir: "/tmp/proxmox_drain_state"
|
||||
|
||||
pre_tasks:
|
||||
- name: "Migrate | Validate mode"
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Invalid migrate_mode '{{ migrate_mode }}'.
|
||||
Must be one of: drain, rebalance, restore, targeted.
|
||||
when: migrate_mode not in ['drain', 'rebalance', 'restore', 'targeted']
|
||||
|
||||
- name: "Migrate | Validate drain — source node required"
|
||||
ansible.builtin.fail:
|
||||
msg: "migrate_source_node is required for drain mode."
|
||||
when:
|
||||
- migrate_mode == 'drain'
|
||||
- migrate_source_node == ''
|
||||
|
||||
- name: "Migrate | Validate restore — source node required"
|
||||
ansible.builtin.fail:
|
||||
msg: "migrate_source_node is required for restore mode."
|
||||
when:
|
||||
- migrate_mode == 'restore'
|
||||
- migrate_source_node == ''
|
||||
|
||||
- name: "Migrate | Validate targeted — VMIDs or tags required"
|
||||
ansible.builtin.fail:
|
||||
msg: "migrate_vmids or migrate_tags must be set for targeted mode."
|
||||
when:
|
||||
- migrate_mode == 'targeted'
|
||||
- migrate_vmids | length == 0
|
||||
- migrate_tags | length == 0
|
||||
|
||||
- name: "Migrate | Log operation"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Proxmox VM migration —
|
||||
client={{ client_name | default('Unknown') }}
|
||||
mode={{ migrate_mode }}
|
||||
{% if migrate_source_node != '' %}source={{ migrate_source_node }}{% endif %}
|
||||
{% if migrate_target_node != '' %}target={{ migrate_target_node }}{% endif %}
|
||||
{% if migrate_vmids | length > 0 %}vmids={{ migrate_vmids }}{% endif %}
|
||||
{% if migrate_tags | length > 0 %}tags={{ migrate_tags }}{% endif %}
|
||||
|
||||
roles:
|
||||
- role: proxmox_preflight
|
||||
|
||||
tasks:
|
||||
# ── DRAIN mode ─────────────────────────────────────────────────────────────
|
||||
- name: "Migrate | DRAIN mode"
|
||||
ansible.builtin.include_role:
|
||||
name: proxmox_drain
|
||||
vars:
|
||||
current_node: "{{ migrate_source_node }}"
|
||||
when: migrate_mode == 'drain'
|
||||
|
||||
# ── RESTORE mode ───────────────────────────────────────────────────────────
|
||||
- name: "Migrate | RESTORE mode"
|
||||
ansible.builtin.include_role:
|
||||
name: proxmox_restore
|
||||
vars:
|
||||
current_node: "{{ migrate_source_node }}"
|
||||
when: migrate_mode == 'restore'
|
||||
|
||||
# ── REBALANCE mode ─────────────────────────────────────────────────────────
|
||||
- name: "Migrate | REBALANCE | Get all node info"
|
||||
community.proxmox.proxmox_node_info:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ api_user }}"
|
||||
api_token_id: "{{ api_token_id }}"
|
||||
api_token_secret: "{{ api_token_secret }}"
|
||||
api_port: "{{ api_port | default(8006) }}"
|
||||
validate_certs: "{{ validate_certs | default(false) }}"
|
||||
register: rebalance_nodes
|
||||
delegate_to: localhost
|
||||
when: migrate_mode == 'rebalance'
|
||||
|
||||
- name: "Migrate | REBALANCE | Get all VM info per node"
|
||||
community.proxmox.proxmox_vm_info:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ api_user }}"
|
||||
api_token_id: "{{ api_token_id }}"
|
||||
api_token_secret: "{{ api_token_secret }}"
|
||||
api_port: "{{ api_port | default(8006) }}"
|
||||
validate_certs: "{{ validate_certs | default(false) }}"
|
||||
node: "{{ item.node }}"
|
||||
loop: >-
|
||||
{{ rebalance_nodes.proxmox_nodes
|
||||
| selectattr('status', 'equalto', 'online')
|
||||
| list }}
|
||||
loop_control:
|
||||
label: "{{ item.node }}"
|
||||
register: rebalance_vms_per_node
|
||||
delegate_to: localhost
|
||||
when: migrate_mode == 'rebalance'
|
||||
|
||||
- name: "Migrate | REBALANCE | Calculate node loads"
|
||||
ansible.builtin.set_fact:
|
||||
rebalance_node_loads: >-
|
||||
{% set loads = [] %}
|
||||
{% for result in rebalance_vms_per_node.results %}
|
||||
{% set node_name = result.item.node %}
|
||||
{% set node_info = rebalance_nodes.proxmox_nodes
|
||||
| selectattr('node', 'equalto', node_name)
|
||||
| first %}
|
||||
{% set vm_mem = result.proxmox_vms
|
||||
| map(attribute='mem')
|
||||
| map('default', 0)
|
||||
| sum %}
|
||||
{% set free_mem = node_info.maxmem - node_info.mem %}
|
||||
{% set load_pct = (node_info.mem / node_info.maxmem * 100) | round(1) %}
|
||||
{% set _ = loads.append({
|
||||
'node': node_name,
|
||||
'used_mem': node_info.mem,
|
||||
'max_mem': node_info.maxmem,
|
||||
'free_mem': free_mem,
|
||||
'load_pct': load_pct,
|
||||
'vm_count': result.proxmox_vms | rejectattr('template', 'equalto', true) | list | length,
|
||||
'vms': result.proxmox_vms | rejectattr('template', 'equalto', true) | list
|
||||
}) %}
|
||||
{% endfor %}
|
||||
{{ loads | sort(attribute='load_pct', reverse=true) }}
|
||||
delegate_to: localhost
|
||||
when: migrate_mode == 'rebalance'
|
||||
|
||||
- name: "Migrate | REBALANCE | Log current distribution"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Current cluster load:
|
||||
{% for n in rebalance_node_loads %}
|
||||
{{ n.node }}: {{ n.load_pct }}% memory used, {{ n.vm_count }} VMs
|
||||
{% endfor %}
|
||||
when: migrate_mode == 'rebalance'
|
||||
|
||||
- name: "Migrate | REBALANCE | Build migration plan"
|
||||
ansible.builtin.set_fact:
|
||||
rebalance_migrations: >-
|
||||
{% set moves = [] %}
|
||||
{% set loads = rebalance_node_loads | list %}
|
||||
{% set total_mem = loads | map(attribute='used_mem') | sum %}
|
||||
{% set avg_mem = total_mem / loads | length %}
|
||||
{% for vm in (loads | map(attribute='vms') | flatten
|
||||
| rejectattr('status', 'equalto', 'stopped')
|
||||
| list) %}
|
||||
{% set src_node = vm.node %}
|
||||
{% set src_info = loads | selectattr('node', 'equalto', src_node) | first %}
|
||||
{% if src_info.load_pct | float > (avg_mem / src_info.max_mem * 100 + rebalance_threshold_pct) %}
|
||||
{% set target = loads
|
||||
| rejectattr('node', 'equalto', src_node)
|
||||
| sort(attribute='load_pct')
|
||||
| first %}
|
||||
{% if target.load_pct | float < src_info.load_pct | float - rebalance_threshold_pct %}
|
||||
{% set _ = moves.append({
|
||||
'vmid': vm.vmid,
|
||||
'name': vm.name,
|
||||
'type': vm.type,
|
||||
'status': vm.status,
|
||||
'from': src_node,
|
||||
'to': target.node
|
||||
}) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ moves }}
|
||||
delegate_to: localhost
|
||||
when: migrate_mode == 'rebalance'
|
||||
|
||||
- name: "Migrate | REBALANCE | Log migration plan"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Rebalance plan ({{ rebalance_migrations | length }} migration(s)):
|
||||
{% if rebalance_migrations | length == 0 %}
|
||||
Cluster is already balanced within {{ rebalance_threshold_pct }}% threshold — no migrations needed.
|
||||
{% else %}
|
||||
{% for m in rebalance_migrations %}
|
||||
{{ m.name }} (VMID {{ m.vmid }}) {{ m.from }} → {{ m.to }}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
when: migrate_mode == 'rebalance'
|
||||
|
||||
- name: "Migrate | REBALANCE | Execute KVM migrations"
|
||||
ansible.builtin.command: >
|
||||
qm migrate {{ item.vmid }} {{ item.to }}
|
||||
{% if item.status == 'running' %}--online{% endif %}
|
||||
--with-local-disks 0
|
||||
loop: "{{ rebalance_migrations | selectattr('type', 'equalto', 'qemu') | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }} ({{ item.from }} → {{ item.to }})"
|
||||
changed_when: true
|
||||
delegate_to: "{{ item.from }}"
|
||||
when:
|
||||
- migrate_mode == 'rebalance'
|
||||
- rebalance_migrations | length > 0
|
||||
|
||||
- name: "Migrate | REBALANCE | Execute LXC migrations"
|
||||
ansible.builtin.command: >
|
||||
pct migrate {{ item.vmid }} {{ item.to }} --restart --timeout 120
|
||||
loop: "{{ rebalance_migrations | selectattr('type', 'equalto', 'lxc') | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.name | default(item.vmid) }} ({{ item.from }} → {{ item.to }})"
|
||||
changed_when: true
|
||||
delegate_to: "{{ item.from }}"
|
||||
when:
|
||||
- migrate_mode == 'rebalance'
|
||||
- rebalance_migrations | length > 0
|
||||
|
||||
- name: "Migrate | REBALANCE | Complete"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
✓ Rebalance complete —
|
||||
{{ rebalance_migrations | length }} VM(s) redistributed.
|
||||
when: migrate_mode == 'rebalance'
|
||||
|
||||
# ── TARGETED mode ──────────────────────────────────────────────────────────
|
||||
- name: "Migrate | TARGETED | Get all VMs"
|
||||
community.proxmox.proxmox_vm_info:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ api_user }}"
|
||||
api_token_id: "{{ api_token_id }}"
|
||||
api_token_secret: "{{ api_token_secret }}"
|
||||
api_port: "{{ api_port | default(8006) }}"
|
||||
validate_certs: "{{ validate_certs | default(false) }}"
|
||||
register: targeted_all_vms
|
||||
delegate_to: localhost
|
||||
when: migrate_mode == 'targeted'
|
||||
|
||||
- name: "Migrate | TARGETED | Filter VMs by VMID"
|
||||
ansible.builtin.set_fact:
|
||||
targeted_vms: >-
|
||||
{{ targeted_all_vms.proxmox_vms
|
||||
| selectattr('vmid', 'in', migrate_vmids)
|
||||
| list }}
|
||||
delegate_to: localhost
|
||||
when:
|
||||
- migrate_mode == 'targeted'
|
||||
- migrate_vmids | length > 0
|
||||
|
||||
- name: "Migrate | TARGETED | Filter VMs by tag"
|
||||
ansible.builtin.set_fact:
|
||||
targeted_vms: >-
|
||||
{{ targeted_all_vms.proxmox_vms
|
||||
| selectattr('tags', 'defined')
|
||||
| selectattr('tags', 'search', migrate_tags | join('|'))
|
||||
| list }}
|
||||
delegate_to: localhost
|
||||
when:
|
||||
- migrate_mode == 'targeted'
|
||||
- migrate_tags | length > 0
|
||||
- migrate_vmids | length == 0
|
||||
|
||||
- name: "Migrate | TARGETED | Resolve target node"
|
||||
ansible.builtin.set_fact:
|
||||
targeted_resolved_target: "{{ migrate_target_node }}"
|
||||
when:
|
||||
- migrate_mode == 'targeted'
|
||||
- migrate_target_node != ''
|
||||
|
||||
- name: "Migrate | TARGETED | Auto-select target by resources"
|
||||
block:
|
||||
- name: "Migrate | TARGETED | Get node resources"
|
||||
community.proxmox.proxmox_node_info:
|
||||
api_host: "{{ api_host }}"
|
||||
api_user: "{{ api_user }}"
|
||||
api_token_id: "{{ api_token_id }}"
|
||||
api_token_secret: "{{ api_token_secret }}"
|
||||
api_port: "{{ api_port | default(8006) }}"
|
||||
validate_certs: "{{ validate_certs | default(false) }}"
|
||||
register: targeted_nodes
|
||||
delegate_to: localhost
|
||||
|
||||
- name: "Migrate | TARGETED | Pick best target"
|
||||
ansible.builtin.set_fact:
|
||||
targeted_resolved_target: >-
|
||||
{{ (targeted_nodes.proxmox_nodes
|
||||
| selectattr('status', 'equalto', 'online')
|
||||
| sort(attribute='mem')
|
||||
| first).node }}
|
||||
delegate_to: localhost
|
||||
when:
|
||||
- migrate_mode == 'targeted'
|
||||
- migrate_target_node == ''
|
||||
|
||||
- name: "Migrate | TARGETED | Log plan"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Targeted migration: {{ targeted_vms | length }} VM(s) → {{ targeted_resolved_target }}
|
||||
VMIDs: {{ targeted_vms | map(attribute='vmid') | list }}
|
||||
when: migrate_mode == 'targeted'
|
||||
|
||||
- name: "Migrate | TARGETED | Migrate KVM VMs"
|
||||
ansible.builtin.command: >
|
||||
qm migrate {{ item.vmid }} {{ targeted_resolved_target }}
|
||||
{% if item.status == 'running' %}--online{% endif %}
|
||||
--with-local-disks 0
|
||||
loop: "{{ targeted_vms | selectattr('type', 'equalto', 'qemu') | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }} (VMID {{ item.vmid }}) → {{ targeted_resolved_target }}"
|
||||
changed_when: true
|
||||
delegate_to: "{{ item.node }}"
|
||||
when: migrate_mode == 'targeted'
|
||||
|
||||
- name: "Migrate | TARGETED | Migrate LXC containers"
|
||||
ansible.builtin.command: >
|
||||
pct migrate {{ item.vmid }} {{ targeted_resolved_target }} --restart --timeout 120
|
||||
loop: "{{ targeted_vms | selectattr('type', 'equalto', 'lxc') | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.name | default(item.vmid) }} (VMID {{ item.vmid }}) → {{ targeted_resolved_target }}"
|
||||
changed_when: true
|
||||
delegate_to: "{{ item.node }}"
|
||||
when: migrate_mode == 'targeted'
|
||||
|
||||
- name: "Migrate | TARGETED | Complete"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
✓ Targeted migration complete —
|
||||
{{ targeted_vms | length }} VM(s) moved to {{ targeted_resolved_target }}.
|
||||
when: migrate_mode == 'targeted'
|
||||
Reference in New Issue
Block a user