[PATCH v2 4/4] bootlinux: detect 9P configuration changes and trigger power cycle
Daniel Gomez <[email protected]> Mon, 01 Dec 2025 20:14:41 +0100
| Newsgroups | dev.linux.lists.kdevops |
|---|---|
| Message-ID | <20251201-custom-linux-9p-tree-and-mount-point-v2-4-c720a83cab91@samsung.com> |
From: Daniel Gomez <[email protected]> When kernel tree configuration changes, the 9P filesystem cannot be reconfigured with simple remounting because QEMU caches the virtio-9p fsdev path at VM start time. This affects mount point changes and host path changes where guests see stale cached data from the previous configuration. Add detection for both scenarios by checking the current mount point against target_linux_dir_path and parsing the libvirt domain XML to compare the fsdev source path against bootlinux_9p_host_path. When either mismatch is detected, remove the old fstab entry and execute a full VM power cycle with libvirt domain undefine and redefine to force QEMU to regenerate its command line with the updated 9P configuration. The power cycle uses graceful shutdown with a 120-second timeout. If shutdown fails, the playbook pauses and prompts the user to manually shut down the VM before continuing. The undefine step tries --nvram first for UEFI VMs and falls back to plain undefine for legacy BIOS. Generated-by: Claude AI Signed-off-by: Daniel Gomez <[email protected]> --- playbooks/roles/bootlinux/tasks/main.yml | 91 ++++++++++++ playbooks/roles/bootlinux/tasks/power-cycle-vm.yml | 165 +++++++++++++++++++++ 2 files changed, 256 insertions(+) diff --git a/playbooks/roles/bootlinux/tasks/main.yml b/playbooks/roles/bootlinux/tasks/main.yml index 8fcb9e47..f1f50efc 100644 --- a/playbooks/roles/bootlinux/tasks/main.yml +++ b/playbooks/roles/bootlinux/tasks/main.yml @@ -195,6 +195,97 @@ when: - not workflow_linux_packaged|bool +- name: Check if 9p mount tag is already mounted elsewhere + become: true + become_flags: "su - -c" + become_method: sudo + ansible.builtin.shell: "mount | grep '{{ bootlinux_9p_mount_tag }}' | awk '{print $3}'" + register: existing_9p_mount + changed_when: false + failed_when: false + tags: ["data_partition", "9p_mount"] + when: + - bootlinux_9p|bool + +- name: Get running VM domain XML to check 9P fsdev path + delegate_to: localhost + community.libvirt.virt: + command: get_xml + name: "{{ inventory_hostname }}" + register: vm_domain_xml + failed_when: false + tags: ["data_partition", "9p_mount"] + when: + - bootlinux_9p|bool + +- name: Extract current 9P source path from domain XML + ansible.builtin.set_fact: + qemu_fsdev_path: >- + {{ + vm_domain_xml.get_xml | + default('') | + regex_search("<source dir=['\"]([^'\"]+)['\"]", '\1') | + default([''], true) | + first + }} + tags: ["data_partition", "9p_mount"] + when: + - bootlinux_9p|bool + - vm_domain_xml.get_xml is defined + +- name: Set 9p_needs_power_cycle fact + ansible.builtin.set_fact: + bootlinux_9p_needs_power_cycle: >- + {{ + (existing_9p_mount.stdout is defined and + existing_9p_mount.stdout != "" and + existing_9p_mount.stdout != target_linux_dir_path) or + (qemu_fsdev_path | default('') != '' and + qemu_fsdev_path != bootlinux_9p_host_path) + }} + tags: ["data_partition", "9p_mount"] + when: + - bootlinux_9p|bool + +- name: Debug 9P path mismatch detection + ansible.builtin.debug: + msg: | + 9P Path Check Results: + - Guest mount point: {{ existing_9p_mount.stdout | default('not mounted') }} + - Expected mount point: {{ target_linux_dir_path }} + - Libvirt 9P source path: {{ qemu_fsdev_path | default('not found') }} + - Expected host path: {{ bootlinux_9p_host_path }} + - Needs power cycle: {{ bootlinux_9p_needs_power_cycle | default(false) }} + tags: ["data_partition", "9p_mount"] + when: + - bootlinux_9p|bool + vars: + output_verbosity: 1 + +- name: Remove old 9P mount from fstab before power cycle + become: true + become_flags: "su - -c" + become_method: sudo + ansible.posix.mount: + name: "{{ existing_9p_mount.stdout }}" + src: "{{ bootlinux_9p_mount_tag }}" + fstype: "9p" + state: "absent" + when: + - bootlinux_9p|bool + - bootlinux_9p_needs_power_cycle | default(false) | bool + - existing_9p_mount.stdout is defined + - existing_9p_mount.stdout != "" + tags: ["data_partition", "9p_mount"] + +- name: Power cycle VM if 9p host path changed (QEMU caches fsdev path) + ansible.builtin.include_tasks: + file: "{{ role_path }}/tasks/power-cycle-vm.yml" + when: + - bootlinux_9p|bool + - bootlinux_9p_needs_power_cycle | default(false) | bool + tags: ["data_partition", "9p_mount"] + - name: Mount bootlinux 9p on each target node become: true become_flags: "su - -c" diff --git a/playbooks/roles/bootlinux/tasks/power-cycle-vm.yml b/playbooks/roles/bootlinux/tasks/power-cycle-vm.yml new file mode 100644 index 00000000..b7e4116c --- /dev/null +++ b/playbooks/roles/bootlinux/tasks/power-cycle-vm.yml @@ -0,0 +1,165 @@ +--- +# Power cycle VM to clear 9P cache and reload virtio device configuration +# This is necessary when the 9P host path changes because: +# - Simple remount doesn't clear virtio device state +# - Restart doesn't properly clear 9P caches +# - Full shutdown + start is required + +- name: Shutdown VM to clear 9P cache + community.libvirt.virt: + name: "{{ ansible_hostname }}" + state: shutdown + uri: "{{ libvirt_uri }}" + delegate_to: localhost + become: true + become_method: ansible.builtin.sudo + become_flags: "su - -c" + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Wait for SSH to become unreachable (VM shutting down) + ansible.builtin.wait_for: + host: "{{ inventory_hostname }}" + port: 22 + state: stopped + timeout: 120 + delay: 2 + delegate_to: localhost + register: shutdown_wait_result + ignore_errors: true + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Give VM a moment to fully transition to shut off state + ansible.builtin.pause: + seconds: 3 + delegate_to: localhost + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Check if VM actually shut down + community.libvirt.virt: + name: "{{ ansible_hostname }}" + command: status + uri: "{{ libvirt_uri }}" + register: vm_state_after_shutdown + delegate_to: localhost + become: true + become_method: ansible.builtin.sudo + become_flags: "su - -c" + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Debug VM state after shutdown check + delegate_to: localhost + ansible.builtin.debug: + msg: "VM {{ ansible_hostname }} state after shutdown: {{ vm_state_after_shutdown.status }}" + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Notify user if graceful shutdown failed + delegate_to: localhost + ansible.builtin.pause: + prompt: | + + ============================================================ + WARNING: VM {{ ansible_hostname }} did not shut down gracefully + ============================================================ + + Current VM state: {{ vm_state_after_shutdown.status }} + + The VM needs to be shut down to reload the 9P filesystem configuration. + Graceful shutdown (virsh shutdown) timed out after 120 seconds. + + Please manually shut down the VM using one of these methods: + + 1. PREFERRED - Investigate why shutdown failed: + sudo virsh console {{ ansible_hostname }} + (Check if system is hung, then try clean shutdown from inside VM) + + 2. Force shutdown (may cause data loss): + sudo virsh destroy {{ ansible_hostname }} + + 3. Try graceful shutdown again: + sudo virsh shutdown {{ ansible_hostname }} + (Wait and check: sudo virsh domstate {{ ansible_hostname }}) + + After the VM shows "shut off" state, press ENTER to continue. + The playbook will then redefine and restart the VM with the new 9P path. + + To check VM state: sudo virsh domstate {{ ansible_hostname }} + + ============================================================ + when: + - vm_state_after_shutdown.status != "shutdown" + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Verify VM is shut off before continuing + community.libvirt.virt: + name: "{{ ansible_hostname }}" + command: status + uri: "{{ libvirt_uri }}" + register: vm_state_verified + failed_when: vm_state_verified.status != "shutdown" + delegate_to: localhost + become: true + become_method: ansible.builtin.sudo + become_flags: "su - -c" + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +# Try undefine with --nvram first (for UEFI VMs), fall back to plain undefine +- name: Undefine VM to clear cached device configuration (UEFI) + community.libvirt.virt: + name: "{{ ansible_hostname }}" + command: undefine + flags: "--nvram" + uri: "{{ libvirt_uri }}" + delegate_to: localhost + become: true + become_method: ansible.builtin.sudo + become_flags: "su - -c" + register: undefine_nvram_result + failed_when: false + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Undefine VM to clear cached device configuration (non-UEFI fallback) + community.libvirt.virt: + name: "{{ ansible_hostname }}" + command: undefine + uri: "{{ libvirt_uri }}" + delegate_to: localhost + become: true + become_method: ansible.builtin.sudo + become_flags: "su - -c" + when: undefine_nvram_result.failed | default(false) + failed_when: false + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Redefine VM with updated 9P host path from XML + community.libvirt.virt: + command: define + xml: "{{ lookup('file', topdir_path + '/guestfs/' + ansible_hostname + '/' + ansible_hostname + '.xml') }}" + uri: "{{ libvirt_uri }}" + delegate_to: localhost + become: true + become_method: ansible.builtin.sudo + become_flags: "su - -c" + failed_when: false + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Start VM with fresh 9P device configuration + community.libvirt.virt: + name: "{{ ansible_hostname }}" + state: running + uri: "{{ libvirt_uri }}" + delegate_to: localhost + become: true + become_method: ansible.builtin.sudo + become_flags: "su - -c" + failed_when: false + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Wait for VM to become reachable after restart + ansible.builtin.wait_for_connection: + timeout: 120 + delay: 5 + tags: ["data_partition", "9p_mount", "vm_power_cycle"] + +- name: Gather facts after VM restart + ansible.builtin.setup: + tags: ["data_partition", "9p_mount", "vm_power_cycle"] -- 2.52.0