From de044674d2d367b54f9edfa51418f265efc07674 Mon Sep 17 00:00:00 2001 From: Satish Tagirisapu Date: Tue, 4 Aug 2026 14:47:52 -0400 Subject: [PATCH] Functest: keep_alive + wait_client_online after ACPI SHUTDOWN Boot functest clients with keep_alive so QMP SHUTDOWN restarts QEMU, and add an explicit wait_client_online step so WinRM is back before the next mid-batch case (NetKVM hotplug shutdown paths). Signed-off-by: Satish Tagirisapu Co-authored-by: Cursor --- docs/Functest-Engine.md | 20 ++++++++++++++++++-- lib/auxiliary/command_execution_manager.rb | 11 ++++++++++- lib/engines/functest/functest.rb | 5 ++++- lib/engines/functest/step_handler.rb | 2 +- lib/engines/hcktest/tools.rb | 6 ++++++ lib/models/command_info.rb | 3 +++ 6 files changed, 42 insertions(+), 5 deletions(-) diff --git a/docs/Functest-Engine.md b/docs/Functest-Engine.md index 92f7119d..85e71563 100644 --- a/docs/Functest-Engine.md +++ b/docs/Functest-Engine.md @@ -262,7 +262,7 @@ See [`lib/engines/functest/tests/cases/driver_sign_check.json`](../lib/engines/f ## Step Types -> Each step object must have **exactly one** step-type field (`guest_run`, `guest_run_file`, `guest_reboot`, `host_run`, `host_run_file`, `files_action`, `qmp_command`, `qmp_wait_event`, `barrier`, `set_variable`). All other fields are optional modifiers. +> Each step object must have **exactly one** step-type field (`guest_run`, `guest_run_file`, `guest_reboot`, `host_run`, `host_run_file`, `files_action`, `qmp_command`, `qmp_wait_event`, `wait_client_online`, `barrier`, `set_variable`). All other fields are optional modifiers. ### Common Step Fields @@ -276,7 +276,7 @@ See [`lib/engines/functest/tests/cases/driver_sign_check.json`](../lib/engines/f | `expected_output_contains` | The step fails if the output does not contain this string. Only for `guest_run`/`guest_run_file`/`host_run`/`host_run_file`. Checked against every client the step ran on. | | `expected_output_matches` | The step fails if the output does not match this regex. Only for `guest_run`/`guest_run_file`/`host_run`/`host_run_file`. Checked against every client the step ran on. | | `expected_output_matches_encoding` | Controls regex encoding options for `expected_output_matches`. Currently only `Regexp::NOENCODING` is supported (binary match). Do not set to use default encoding. | -| `clients` | Client ids (e.g. `[2]`) this step targets. Applies to `guest_run`/`guest_run_file`, `guest_reboot`, `files_action`, `qmp_command`, `qmp_wait_event`; `host_run`/`host_run_file` always run once on the host regardless. Omitted/empty broadcasts to every client booted for the test case. See [Multi-Client Support](#multi-client-support). | +| `clients` | Client ids (e.g. `[2]`) this step targets. Applies to `guest_run`/`guest_run_file`, `guest_reboot`, `files_action`, `qmp_command`, `qmp_wait_event`, `wait_client_online`; `host_run`/`host_run_file` always run once on the host regardless. Omitted/empty broadcasts to every client booted for the test case. See [Multi-Client Support](#multi-client-support). | --- @@ -433,6 +433,22 @@ Blocks until one of the given QEMU events is received from the client VM. `event --- +### `wait_client_online` + +Blocks until WinRM is reachable on the target client(s). Use after an intentional guest ACPI poweroff / QMP `SHUTDOWN` when the VM was started with functest `keep_alive` (QEMU restarts; Windows must finish booting before the next step or minidump collection). + +```json +{ + "desc": "Wait for client online after keep_alive QEMU restart", + "wait_client_online": true, + "timeout": 600 +} +``` + +If the guest never returns, the step fails (and the run stops) — dump collection stays strict and is not skipped. + +--- + ### `barrier` A named synchronization point. It only logs the barrier name and does nothing else. diff --git a/lib/auxiliary/command_execution_manager.rb b/lib/auxiliary/command_execution_manager.rb index c280175b..1baf0f78 100644 --- a/lib/auxiliary/command_execution_manager.rb +++ b/lib/auxiliary/command_execution_manager.rb @@ -26,7 +26,7 @@ class CommandExecutionManager STEP_TYPE_FIELDS = T.let(%i[ guest_run guest_run_file guest_reboot files_action host_run host_run_file - barrier set_variable qmp_command qmp_wait_event + barrier set_variable qmp_command qmp_wait_event wait_client_online ].freeze, T::Array[Symbol]) sig { params(init_opts: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) } @@ -91,6 +91,7 @@ def execute(command_info, replacement: ReplacementMap.new) execute_barrier(command_info) if command_info.barrier result[:qmp_result] = execute_qmp_command(command_info, replacement) if command_info.qmp_command result[:qmp_event] = execute_qmp_wait_event(command_info) if command_info.qmp_wait_event + execute_wait_client_online(command_info) if command_info.wait_client_online result end @@ -295,6 +296,14 @@ def execute_qmp_wait_event(command_info) outputs end + sig { params(command_info: Models::CommandInfo).void } + def execute_wait_client_online(command_info) + target_machines(command_info).each do |machine_name| + @logger.info("Waiting for client #{machine_name} to come online") + @tools.wait_for_client_online(machine_name) + end + end + sig do params( command_info: Models::CommandInfo, diff --git a/lib/engines/functest/functest.rb b/lib/engines/functest/functest.rb index 83208ebe..555f0aa7 100644 --- a/lib/engines/functest/functest.rb +++ b/lib/engines/functest/functest.rb @@ -150,7 +150,10 @@ def boot_clients(scope) 'in the platform configuration' end - @project.setup_manager.run_functest_client(scope, client.name) + # Same as HCKTest run_clients(..., keep_alive: true): guest ACPI + # poweroff / QMP SHUTDOWN exits QEMU; restart so mid-batch cases + # (e.g. netkvm_hotplug_shutdown) can continue on a live client. + @project.setup_manager.run_functest_client(scope, client.name, { keep_alive: true }) end end diff --git a/lib/engines/functest/step_handler.rb b/lib/engines/functest/step_handler.rb index 65356946..57be2fd4 100644 --- a/lib/engines/functest/step_handler.rb +++ b/lib/engines/functest/step_handler.rb @@ -52,7 +52,7 @@ def step_type_set?(step, field) value = step.public_send(field) case field when :files_action then value.any? - when :guest_reboot then value == true + when :guest_reboot, :wait_client_online then value == true when :set_variable then value.is_a?(Hash) && !value.empty? when :guest_run, :guest_run_file, :host_run, :host_run_file, :barrier value.is_a?(String) ? !value.empty? : !value.nil? diff --git a/lib/engines/hcktest/tools.rb b/lib/engines/hcktest/tools.rb index d91d60d9..bcc6784c 100644 --- a/lib/engines/hcktest/tools.rb +++ b/lib/engines/hcktest/tools.rb @@ -262,6 +262,12 @@ def restart_machine_and_wait(_machine) raise ToolsHCKError, 'Unimplemented method: restart_machine_and_wait' end + # Poll until WinRM is reachable. FunctestTools implements this; used by + # the wait_client_online step after ACPI poweroff + keep_alive restart. + def wait_for_client_online(_machine) + raise ToolsHCKError, 'Unimplemented method: wait_for_client_online' + end + def restart_machine(machine) retries ||= 0 act_with_tools { _1.machine_shutdown(machine, restart: true) } diff --git a/lib/models/command_info.rb b/lib/models/command_info.rb index 9027fca5..3780f91e 100644 --- a/lib/models/command_info.rb +++ b/lib/models/command_info.rb @@ -81,6 +81,9 @@ class CommandInfo < T::Struct const :set_variable, T.nilable(T::Hash[String, String]) const :qmp_command, T.nilable(QmpCommandConfig) const :qmp_wait_event, T.nilable(QmpWaitEventConfig) + # Block until WinRM is reachable again (e.g. after ACPI poweroff when + # the VM was started with keep_alive and QEMU has restarted). + const :wait_client_online, T::Boolean, default: false const :expected_output_contains, T.nilable(String) const :expected_output_matches, T.nilable(String)