Skip to content

Commit 9d5c64b

Browse files
committed
Improve OpenVMM test integration, diagnostics
(cherry picked from commit ac4c15c)
1 parent 25af75d commit 9d5c64b

File tree

9 files changed

+743
-143
lines changed

9 files changed

+743
-143
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
name: openvmm azure smoke
2+
extension:
3+
- ../../testsuites
4+
variable:
5+
- name: host_admin_username
6+
value: lisatest
7+
- name: host_admin_password
8+
value: ""
9+
is_secret: true
10+
- name: host_admin_private_key_file
11+
value: ""
12+
is_secret: true
13+
- name: guest_admin_username
14+
value: lisatest
15+
- name: guest_admin_password
16+
value: ""
17+
is_secret: true
18+
- name: guest_admin_private_key_file
19+
value: ""
20+
is_secret: true
21+
- name: guest_extra_user_data
22+
value: ""
23+
- name: subscription_id
24+
value: ""
25+
- name: location
26+
value: "westus3"
27+
- name: marketplace_image
28+
value: ""
29+
- name: vm_size
30+
value: ""
31+
- name: openvmm_binary
32+
value: /usr/local/bin/openvmm
33+
- name: openvmm_install_path
34+
value: /usr/local/bin/openvmm
35+
- name: openvmm_installer_repo
36+
value: https://github.com/microsoft/openvmm.git
37+
- name: openvmm_installer_ref
38+
value: ""
39+
- name: openvmm_installer_force_install
40+
value: false
41+
- name: openvmm_host_working_dir
42+
value: /var/tmp
43+
- name: uefi_firmware_path
44+
value: ""
45+
- name: uefi_firmware_is_remote_path
46+
value: false
47+
- name: disk_img_path
48+
value: ""
49+
- name: disk_img_is_remote_path
50+
value: false
51+
- name: tap_name
52+
value: tap0
53+
- name: bridge_name
54+
value: ovmbr0
55+
- name: tap_host_cidr
56+
value: 10.0.0.1/24
57+
- name: forwarded_port
58+
value: 60022
59+
notifier:
60+
- type: html
61+
transformer:
62+
- type: openvmm_installer
63+
phase: environment_connected
64+
installer:
65+
type: source
66+
repo: $(openvmm_installer_repo)
67+
ref: $(openvmm_installer_ref)
68+
force_install: $(openvmm_installer_force_install)
69+
install_path: $(openvmm_install_path)
70+
platform:
71+
- type: azure
72+
admin_username: $(host_admin_username)
73+
admin_password: $(host_admin_password)
74+
admin_private_key_file: $(host_admin_private_key_file)
75+
guest_enabled: true
76+
guests:
77+
- type: openvmm
78+
use_parent_capability: false
79+
username: $(guest_admin_username)
80+
password: $(guest_admin_password)
81+
private_key_file: $(guest_admin_private_key_file)
82+
cloud_init:
83+
extra_user_data: $(guest_extra_user_data)
84+
lisa_working_dir: $(openvmm_host_working_dir)
85+
openvmm_binary: $(openvmm_binary)
86+
boot_mode: uefi
87+
capability:
88+
core_count: 2
89+
memory_mb: 2048
90+
uefi:
91+
firmware_path: $(uefi_firmware_path)
92+
firmware_is_remote_path: $(uefi_firmware_is_remote_path)
93+
disk_img: $(disk_img_path)
94+
disk_img_is_remote_path: $(disk_img_is_remote_path)
95+
serial:
96+
mode: file
97+
network:
98+
mode: tap
99+
address_mode: discover
100+
tap_name: $(tap_name)
101+
bridge_name: $(bridge_name)
102+
tap_host_cidr: $(tap_host_cidr)
103+
forward_ssh_port: true
104+
forwarded_port: $(forwarded_port)
105+
azure:
106+
subscription_id: $(subscription_id)
107+
requirement:
108+
azure:
109+
marketplace: $(marketplace_image)
110+
location: $(location)
111+
vm_size: $(vm_size)
112+
testcase:
113+
- criteria:
114+
name:
115+
- verify_openvmm_guest_boot
116+
- verify_openvmm_restart_via_platform
117+
- verify_openvmm_stop_start_in_platform
Lines changed: 88 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,118 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the MIT license.
33

4+
from pathlib import Path
5+
from typing import Any
6+
47
from assertpy import assert_that
58

6-
from lisa import Node, SkippedException, TestCaseMetadata, TestSuite, TestSuiteMetadata
9+
from lisa import (
10+
Logger,
11+
RemoteNode,
12+
SkippedException,
13+
TestCaseMetadata,
14+
TestSuite,
15+
TestSuiteMetadata,
16+
simple_requirement,
17+
)
18+
from lisa.environment import EnvironmentStatus
719
from lisa.features import StartStop
820
from lisa.sut_orchestrator.openvmm.node import OpenVmmGuestNode
9-
from lisa.testsuite import simple_requirement
21+
from lisa.tools import Uname
1022

1123

1224
@TestSuiteMetadata(
1325
area="openvmm",
1426
category="functional",
1527
description="""
16-
Smoke coverage for OpenVMM guest provisioning and platform lifecycle.
28+
This test suite validates OpenVMM guests running on a prepared L1 host.
1729
""",
1830
)
19-
class OpenVmmSmokeTestSuite(TestSuite):
31+
class OpenVmmPlatform(TestSuite):
32+
def before_case(self, log: Logger, **kwargs: Any) -> None:
33+
node = kwargs["node"]
34+
if not isinstance(node, OpenVmmGuestNode):
35+
raise SkippedException(
36+
"This suite only applies to OpenVMM guest nodes. "
37+
f"Actual node type: {type(node).__name__}."
38+
)
39+
40+
def _assert_log_path_exists(self, log_path: object) -> None:
41+
resolved_log_path = Path(str(log_path))
42+
assert_that(resolved_log_path.exists()).described_as(
43+
f"log path should exist: {resolved_log_path}"
44+
).is_true()
45+
2046
@TestCaseMetadata(
2147
description="""
22-
Validate an OpenVMM guest is provisioned, reachable over SSH, and can
23-
execute a simple command after launch.
48+
This case validates that an OpenVMM guest is reachable over SSH and that
49+
the guest booted successfully.
2450
""",
25-
priority=1,
26-
requirement=simple_requirement(supported_features=[StartStop]),
51+
priority=0,
52+
requirement=simple_requirement(
53+
environment_status=EnvironmentStatus.Deployed,
54+
),
2755
)
28-
def verify_openvmm_provisioning(self, node: Node) -> None:
29-
openvmm_node = self._get_openvmm_guest(node)
56+
def verify_openvmm_guest_boot(
57+
self,
58+
log: Logger,
59+
node: RemoteNode,
60+
log_path: Path,
61+
) -> None:
62+
kernel_release = node.tools[Uname].get_linux_information().kernel_version_raw
63+
log.info(f"Connected to OpenVMM guest kernel {kernel_release}")
64+
self._assert_log_path_exists(log_path)
3065

31-
result = openvmm_node.execute("echo openvmm-smoke", shell=True)
66+
@TestCaseMetadata(
67+
description="""
68+
This case validates that platform restart keeps the OpenVMM guest
69+
reachable and that serial console capture still works after the restart.
70+
""",
71+
priority=0,
72+
requirement=simple_requirement(
73+
environment_status=EnvironmentStatus.Deployed,
74+
supported_features=[StartStop],
75+
),
76+
)
77+
def verify_openvmm_restart_via_platform(
78+
self,
79+
log: Logger,
80+
node: RemoteNode,
81+
log_path: Path,
82+
) -> None:
83+
start_stop = node.features[StartStop]
84+
start_stop.restart()
3285

33-
result.assert_exit_code()
34-
assert_that(result.stdout.strip()).is_equal_to("openvmm-smoke")
86+
kernel_release = node.tools[Uname].get_linux_information().kernel_version_raw
87+
log.info(f"OpenVMM guest returned after restart on kernel {kernel_release}")
88+
self._assert_log_path_exists(log_path)
3589

3690
@TestCaseMetadata(
3791
description="""
38-
Validate the OpenVMM StartStop feature can stop and start a guest while
39-
preserving SSH connectivity for subsequent command execution.
92+
This case validates that platform stop/start keeps the OpenVMM guest
93+
reachable for subsequent command execution.
4094
""",
41-
priority=1,
42-
requirement=simple_requirement(supported_features=[StartStop]),
95+
priority=0,
96+
requirement=simple_requirement(
97+
environment_status=EnvironmentStatus.Deployed,
98+
supported_features=[StartStop],
99+
),
43100
)
44-
def verify_openvmm_stop_start_in_platform(self, node: Node) -> None:
45-
openvmm_node = self._get_openvmm_guest(node)
46-
47-
start_stop = openvmm_node.features[StartStop]
101+
def verify_openvmm_stop_start_in_platform(
102+
self,
103+
log: Logger,
104+
node: RemoteNode,
105+
log_path: Path,
106+
) -> None:
107+
start_stop = node.features[StartStop]
108+
log.info("Stopping OpenVMM guest via platform")
48109
start_stop.stop(wait=True)
110+
log.info("Starting OpenVMM guest via platform")
49111
start_stop.start(wait=True)
50112

51-
result = openvmm_node.execute("echo openvmm-recovered", shell=True)
52-
113+
result = node.execute("echo openvmm-recovered", shell=True)
53114
result.assert_exit_code()
54-
assert_that(result.stdout.strip()).is_equal_to("openvmm-recovered")
55-
56-
def _get_openvmm_guest(self, node: Node) -> OpenVmmGuestNode:
57-
if not isinstance(node, OpenVmmGuestNode):
58-
raise SkippedException("This suite only applies to OpenVMM guest nodes.")
59-
60-
return node
115+
assert_that(result.stdout.strip()).described_as(
116+
"OpenVMM guest should remain reachable over SSH after platform stop/start"
117+
).is_equal_to("openvmm-recovered")
118+
self._assert_log_path_exists(log_path)

lisa/node.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,16 @@ def execute_async(
355355
)
356356

357357
def cleanup(self) -> None:
358+
for guest in self.guests:
359+
guest.cleanup()
358360
self.log.debug("cleaning up...")
359361
if hasattr(self, "_log_handler") and self._log_handler:
360362
remove_handler(self._log_handler, self.log)
361363
self._log_handler.close()
362364

363365
def close(self) -> None:
366+
for guest in self.guests:
367+
guest.close()
364368
self.log.debug("closing node connection...")
365369
if self._shell:
366370
self._shell.close()
@@ -553,7 +557,12 @@ def mark_dirty(self) -> None:
553557
self._is_dirty = True
554558

555559
def test_connection(self) -> bool:
556-
assert self._shell
560+
if not self._shell:
561+
self.log.debug(
562+
f"connection test failed for node '{self.name}' because its "
563+
"shell is not initialized"
564+
)
565+
return False
557566
if not self._shell.is_remote:
558567
return True
559568
self.log.debug("testing connection...")

lisa/runners/lisa_runner.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,13 @@ def _dispatch_test_result(
200200
# run on deployed environment
201201
can_run_results = [x for x in can_run_results if x.can_run]
202202
if environment.status == EnvironmentStatus.Deployed and can_run_results:
203+
if self._guest_enabled:
204+
return self._generate_task(
205+
task_method=self._initialize_environment_task,
206+
environment=environment,
207+
test_results=can_run_results[:1],
208+
)
209+
203210
selected_test_results = self._get_test_result_to_run(
204211
test_results=test_results, environment=environment
205212
)
@@ -338,6 +345,9 @@ def _initialize_environment_task(
338345
phase=constants.TRANSFORMER_PHASE_ENVIRONMENT_CONNECTED,
339346
environment=environment,
340347
)
348+
if self._guest_enabled:
349+
guest_environment = environment.get_guest_environment()
350+
guest_environment.nodes.initialize()
341351
except Exception as e:
342352
self._attach_failed_environment_to_result(
343353
environment=environment,
@@ -636,8 +646,10 @@ def _get_runnable_test_results(
636646
)
637647
and (
638648
environment_status is None
639-
or x.runtime_data.metadata.requirement.environment_status
640-
== environment_status
649+
or self._matches_environment_status(
650+
x.runtime_data.metadata.requirement.environment_status,
651+
environment_status,
652+
)
641653
)
642654
]
643655
if environment:
@@ -685,6 +697,23 @@ def _get_runnable_test_results(
685697
results = self._sort_test_results(results)
686698
return results
687699

700+
def _matches_environment_status(
701+
self,
702+
requirement_status: EnvironmentStatus,
703+
actual_status: EnvironmentStatus,
704+
) -> bool:
705+
if requirement_status == actual_status:
706+
return True
707+
708+
if (
709+
self._guest_enabled
710+
and actual_status == EnvironmentStatus.Connected
711+
and requirement_status == EnvironmentStatus.Deployed
712+
):
713+
return True
714+
715+
return False
716+
688717
def _get_test_result_to_run(
689718
self, test_results: List[TestResult], environment: Environment
690719
) -> List[TestResult]:

lisa/sut_orchestrator/azure/platform_.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,9 @@ def _get_node_information(self, node: Node) -> Dict[str, str]: # noqa: C901
802802

803803
# Guest nodes (like WslContainerNode) don't have features attribute
804804
# Skip security profile collection for guest nodes
805-
if hasattr(node, "features"):
805+
if hasattr(node, "features") and node.features.is_supported(
806+
SecurityProfile
807+
):
806808
security_profile = node.features[SecurityProfile].get_settings()
807809
else:
808810
security_profile = None
@@ -968,7 +970,11 @@ def _get_kernel_version(self, node: Node) -> str:
968970
linux_information = node.tools[Uname].get_linux_information()
969971
result = linux_information.kernel_version_raw
970972
elif not node.is_connected or node.is_posix:
971-
if not result and hasattr(node, ATTRIBUTE_FEATURES):
973+
if (
974+
not result
975+
and hasattr(node, ATTRIBUTE_FEATURES)
976+
and node.features.is_supported(features.SerialConsole)
977+
):
972978
# try to get kernel version in Azure. use it, when uname doesn't work
973979
node.log.debug("detecting kernel version from serial log...")
974980
serial_console = node.features[features.SerialConsole]
@@ -1004,7 +1010,11 @@ def _get_wala_version(self, node: Node) -> str:
10041010
node.log.debug(f"error on run waagent: {e}")
10051011

10061012
if not node.is_connected or node.is_posix:
1007-
if not result and hasattr(node, ATTRIBUTE_FEATURES):
1013+
if (
1014+
not result
1015+
and hasattr(node, ATTRIBUTE_FEATURES)
1016+
and node.features.is_supported(features.SerialConsole)
1017+
):
10081018
node.log.debug("detecting wala agent version from serial log...")
10091019
serial_console = node.features[features.SerialConsole]
10101020
result = serial_console.get_matched_str(WALA_VERSION_PATTERN)

0 commit comments

Comments
 (0)