Skip to content

Commit a69b551

Browse files
committed
Changes based on review commands
Introduce NonSshExecutor. It Internally uses SerialConsole/RunCommand
1 parent bd57824 commit a69b551

File tree

7 files changed

+134
-75
lines changed

7 files changed

+134
-75
lines changed

lisa/features/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from .nested_virtualization import NestedVirtualization
2424
from .network_interface import NetworkInterface, Sriov, Synthetic
2525
from .nfs import Nfs
26+
from .non_ssh_executor import NonSshExecutor
2627
from .nvme import Nvme, NvmeSettings
2728
from .password_extension import PasswordExtension
2829
from .resize import Resize, ResizeAction
@@ -65,7 +66,6 @@
6566
"PasswordExtension",
6667
"Resize",
6768
"ResizeAction",
68-
"RunCommand",
6969
"SecureBootEnabled",
7070
"SecurityProfile",
7171
"SecurityProfileSettings",
@@ -75,4 +75,5 @@
7575
"VMStatus",
7676
"Synthetic",
7777
"StartStop",
78+
"NonSshExecutor",
7879
]

lisa/features/non_ssh_executor.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from lisa.feature import Feature
2+
from lisa.features.serial_console import SerialConsole
3+
4+
5+
class NonSshExecutor(Feature):
6+
"""
7+
NonSshExecutor is used to run commands on the node when SSH is not available.
8+
Lisa by default uses SSH for connection, but this feature provides an alternative
9+
execution method for scenarios where SSH connectivity is not possible or desired.
10+
"""
11+
12+
COMMANDS_TO_EXECUTE = [
13+
"ip addr show",
14+
"ip link show",
15+
"systemctl status NetworkManager --no-pager --plain",
16+
"systemctl status network --no-pager --plain",
17+
"systemctl status systemd-networkd --no-pager --plain",
18+
"ping -c 3 -n 8.8.8.8",
19+
]
20+
21+
@classmethod
22+
def name(cls) -> str:
23+
return "NonSshExecutor"
24+
25+
def enabled(self) -> bool:
26+
return True
27+
28+
def execute(self, commands: list[str] = COMMANDS_TO_EXECUTE) -> list[str]:
29+
"""
30+
Executes a list of commands on the node and returns their outputs.
31+
32+
:param commands: A list of shell commands to execute.
33+
:return: A string containing the output of the executed commands.
34+
"""
35+
out = []
36+
serial_console = self._node.features[SerialConsole]
37+
serial_console.login()
38+
# clear the console before executing commands
39+
serial_console.write("\n")
40+
_ = serial_console.read()
41+
for command in commands:
42+
serial_console.write(self._add_newline(command))
43+
out.append(serial_console.read())
44+
return out
45+
46+
def _add_newline(self, command: str) -> str:
47+
"""
48+
Adds a newline character to the command if it does not already end with one.
49+
newline is required to run the command in serial console.
50+
"""
51+
if not command.endswith("\n"):
52+
return f"{command}\n"
53+
return command

lisa/features/run_command.py

Lines changed: 0 additions & 22 deletions
This file was deleted.

lisa/features/serial_console.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
get_datetime_path,
1414
get_matched_str,
1515
)
16+
from lisa.util.constants import ENVIRONMENTS_NODES_REMOTE_USERNAME
1617

1718
FEATURE_NAME_SERIAL_CONSOLE = "SerialConsole"
1819
NAME_SERIAL_CONSOLE_LOG = "serial_console.log"
@@ -190,6 +191,34 @@ def check_initramfs(
190191
f"{initramfs_logs} {filesystem_exception_logs}"
191192
)
192193

194+
def login(self) -> None:
195+
# Clear the serial console and try to get the login prompt
196+
self.read()
197+
self.write("\n")
198+
serial_output = self.read()
199+
200+
if "login" not in serial_output:
201+
self._log.debug(
202+
"No login prompt found, serial console is already logged in."
203+
)
204+
return
205+
206+
from lisa.node import RemoteNode
207+
208+
if not isinstance(self._node, RemoteNode):
209+
raise LisaException(
210+
"SerialConsole login is only implemented for RemoteNode"
211+
)
212+
213+
username = self._node.connection_info[ENVIRONMENTS_NODES_REMOTE_USERNAME]
214+
password = self._node.get_password()
215+
216+
self.write(f"{username}\n")
217+
password_prompt = self.read()
218+
219+
if "password" in password_prompt.lower():
220+
self.write(f"{password}\n")
221+
193222
def read(self) -> str:
194223
raise NotImplementedError
195224

lisa/node.py

Lines changed: 24 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -694,9 +694,16 @@ def _initialize(self, *args: Any, **kwargs: Any) -> None:
694694
try:
695695
super()._initialize(*args, **kwargs)
696696
except TcpConnectionException as e:
697-
vm_logs = self._collect_logs_using_platform()
698-
if vm_logs:
699-
self.log.info(f"Collected information using platform:\n{vm_logs}")
697+
try:
698+
vm_logs = self._collect_logs_using_non_ssh_executor()
699+
if vm_logs:
700+
self.log.info(
701+
f"Collected information using non-ssh executor:\n{vm_logs}"
702+
)
703+
except Exception as log_error:
704+
self.log.debug(
705+
f"Failed to collect logs using non-ssh executor: {log_error}"
706+
)
700707
raise e
701708

702709
def get_working_path(self) -> PurePath:
@@ -741,31 +748,22 @@ def check_sudo_password_required(self) -> None:
741748
raise RequireUserPasswordException("Reset password failed")
742749
self._check_password_and_store_prompt()
743750

744-
def _collect_logs_using_platform(self) -> Optional[str]:
751+
def _collect_logs_using_non_ssh_executor(self) -> Optional[str]:
745752
"""
746-
Collects information using the RunCommand feature.
753+
Collects information using the NonSshExecutor feature.
747754
This is used when the connection to the node is not stable.
748755
"""
749-
from lisa.features import RunCommand
750-
751-
if self.features.is_supported(RunCommand):
752-
run_command = self.features[RunCommand]
753-
commands = [
754-
"echo 'Executing: ip addr show'",
755-
"ip addr show",
756-
"echo 'Executing: ip link show'",
757-
"ip link show",
758-
"echo 'Executing: systemctl status NetworkManager --no-pager --plain'",
759-
"systemctl status NetworkManager --no-pager --plain",
760-
"echo 'Executing: systemctl status network --no-pager --plain'",
761-
"systemctl status network --no-pager --plain",
762-
"echo 'Executing: systemctl status systemd-networkd --no-pager --plain'",
763-
"systemctl status systemd-networkd --no-pager --plain",
764-
"echo 'Executing: ping -c 3 -n 8.8.8.8'",
765-
"ping -c 3 -n 8.8.8.8",
766-
]
767-
out = run_command.execute(commands=commands)
768-
return out
756+
from lisa.features import NonSshExecutor
757+
758+
if self.features.is_supported(NonSshExecutor):
759+
non_ssh_executor = self.features[NonSshExecutor]
760+
out = non_ssh_executor.execute()
761+
return "\n".join(out)
762+
else:
763+
self.log.debug(
764+
f"NonSshExecutor is not supported on {self.name}, "
765+
"cannot collect logs using non-ssh executor."
766+
)
769767
return None
770768

771769
def _check_password_and_store_prompt(self) -> None:
@@ -815,29 +813,7 @@ def _check_bash_prompt(self) -> None:
815813
ssh_shell.bash_prompt = bash_prompt
816814
self.has_checked_bash_prompt = True
817815

818-
def _login_to_serial_console(self) -> None:
819-
from lisa.features import SerialConsole
820-
821-
if self.features.is_supported(SerialConsole):
822-
serial_console = self.features[SerialConsole]
823-
# clear the serial console
824-
# write \n to serial console to get the prompt
825-
# read the serial console output
826-
_ = serial_console.read()
827-
serial_console.write("\n")
828-
serial_read = serial_console.read()
829-
if "login" in serial_read:
830-
password = self._get_password()
831-
serial_console.write(f"{self._connection_info.username}\n")
832-
password_prompt = serial_console.read()
833-
if "password" in password_prompt.lower():
834-
serial_console.write(f"{password}\n")
835-
else:
836-
self.log.debug(
837-
"No login prompt found, serial console is already logged in."
838-
)
839-
840-
def _get_password(self, generate: bool = True) -> str:
816+
def get_password(self, generate: bool = True) -> str:
841817
"""
842818
Get the password for the node. If the password is not set, it will
843819
generate a strong password and reset it.

lisa/sut_orchestrator/azure/features.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3734,7 +3734,7 @@ def _prepare_azure_file_share(
37343734
)
37353735

37363736

3737-
class RunCommand(AzureFeatureMixin, features.RunCommand):
3737+
class RunCommand(AzureFeatureMixin, Feature):
37383738

37393739
@classmethod
37403740
def create_setting(
@@ -3762,17 +3762,38 @@ def execute(self, commands: List[str]) -> str:
37623762
compute_client = get_compute_client(platform)
37633763

37643764
# Prepare the RunCommandInput for Azure
3765-
command = RunCommandInput(
3765+
run_command_input = RunCommandInput(
37663766
command_id="RunShellScript",
3767-
script=commands,
3767+
script=self._add_echo_before_command(commands),
37683768
)
37693769

37703770
# Execute the command on the VM
37713771
operation = compute_client.virtual_machines.begin_run_command(
37723772
resource_group_name=context.resource_group_name,
37733773
vm_name=context.vm_name,
3774-
parameters=command,
3774+
parameters=run_command_input,
37753775
)
37763776
result = wait_operation(operation=operation, failure_identity="run command")
37773777

37783778
return result["value"][0]["message"]
3779+
3780+
def _add_echo_before_command(self, commands: List[str]):
3781+
"""
3782+
Adds an echo command before each command in the list to ensure
3783+
that the output of each command is captured in the logs.
3784+
"""
3785+
return [f"echo 'Running command: {cmd}' && {cmd}" for cmd in commands]
3786+
3787+
3788+
class NonSshExecutor(AzureFeatureMixin, features.NonSshExecutor):
3789+
def execute(
3790+
self, commands: list[str] = features.NonSshExecutor.COMMANDS_TO_EXECUTE
3791+
) -> list[str]:
3792+
# RunCommand is faster than SerialConsole. Hence attempt to use it first.
3793+
try:
3794+
output = self._node.features[RunCommand].execute(commands)
3795+
return [output]
3796+
except Exception as e:
3797+
self._log.warning(f"RunCommand failed: {e}")
3798+
# Fallback to the default non-SSH executor behavior
3799+
return super().execute(commands)

lisa/sut_orchestrator/azure/platform_.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,7 @@ def supported_features(cls) -> List[Type[feature.Feature]]:
494494
features.Infiniband,
495495
features.Hibernation,
496496
features.RunCommand,
497+
features.NonSshExecutor,
497498
]
498499

499500
def _prepare_environment(self, environment: Environment, log: Logger) -> bool:

0 commit comments

Comments
 (0)