[PATCH 2/2] bootlinux: handle 9P remounting when tree configuration changes

Daniel Gomez <[email protected]> Mon, 24 Nov 2025 21:29:42 +0100
Newsgroups dev.linux.lists.kdevops
Message-ID <20251124-custom-linux-9p-tree-and-mount-point-v1-2-cfdd5716b43c@samsung.com>
From: Daniel Gomez <[email protected]>

When kernel tree configuration changes to a different mount point, the
9P filesystem cannot be reconfigured with simple remounting because the
QEMU virtio-9p device retains the original command line fsdev path. This
causes guests to see stale cached data from the previous tree location.

The solution requires a full VM power cycle with libvirt domain undefine
and redefine. The undefine step removes the cached VM definition, and
redefine forces libvirt to regenerate the QEMU command line from the
updated XML file containing the new 9P host path.

Detect when the 9P mount tag is mounted at a different path than the
current target_linux_dir_path. When detected, remove the old fstab entry
to prevent boot failures, then execute the power cycle. After restart,
mount at the new location and update fstab.

The power cycle implementation in
playbooks/roles/bootlinux/tasks/power-cycle-vm.yml uses graceful
shutdown via community.libvirt.virt module, waiting up to 120 seconds
for the VM to reach shut off state. A 3-second pause after port closure
allows libvirt domain state to fully transition before verification.
If graceful shutdown fails, pause the playbook and prompt the user
to manually shut down the VM, providing options to investigate via
console, retry shutdown, or force destroy. Use wait_for_connection
after VM restart to properly test SSH connectivity rather than just
port availability.

This enables seamless switching between kernel trees like
BOOTLINUX_TREE_LINUS and BOOTLINUX_CUSTOM without manual VM recreation.

Generated-by: Claude AI
Signed-off-by: Daniel Gomez <[email protected]>
---
 playbooks/roles/bootlinux/tasks/main.yml           |  38 ++++++
 playbooks/roles/bootlinux/tasks/power-cycle-vm.yml | 150 +++++++++++++++++++++
 2 files changed, 188 insertions(+)

diff --git a/playbooks/roles/bootlinux/tasks/main.yml b/playbooks/roles/bootlinux/tasks/main.yml
index da42612e..98b3eb33 100644
--- a/playbooks/roles/bootlinux/tasks/main.yml
+++ b/playbooks/roles/bootlinux/tasks/main.yml
@@ -195,6 +195,44 @@
   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: 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
+    - existing_9p_mount.stdout is defined
+    - existing_9p_mount.stdout != ""
+    - existing_9p_mount.stdout != target_linux_dir_path
+  tags: ["data_partition", "9p_mount"]
+
+- name: Power cycle VM if 9p mounted at different location (clears cache)
+  ansible.builtin.include_tasks:
+    file: "{{ role_path }}/tasks/power-cycle-vm.yml"
+  when:
+    - bootlinux_9p|bool
+    - existing_9p_mount.stdout is defined
+    - existing_9p_mount.stdout != ""
+    - existing_9p_mount.stdout != target_linux_dir_path
+  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..5e1ea0cc
--- /dev/null
+++ b/playbooks/roles/bootlinux/tasks/power-cycle-vm.yml
@@ -0,0 +1,150 @@
+---
+# 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"]
+
+- name: Undefine VM to clear cached device configuration
+  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"
+  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