Skip to content

Commit ec5d15c

Browse files
committed
feat: implement HTTP server for remote UEFI shell capability
Signed-off-by: Lasota, Adrian <adrian.lasota@intel.com>
1 parent 998b5d3 commit ec5d15c

File tree

7 files changed

+561
-2
lines changed

7 files changed

+561
-2
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,6 +1101,32 @@ if code cannot establish connection, it will retry using deploy port (+1 for def
11011101

11021102
If code cannot establish connection, it will start deployment of python using [Deployment setup tool](#deployment-setup-tool) and establish connection again.
11031103

1104+
## RShell connection
1105+
1106+
`RShellConnection` is a connection type that leverages `httplib.client` library to establish and manage connections using RESTful API over HTTP protocol in EFI Shell environment. `rshell_client.py` must be present on the EFI Shell target system.
1107+
1108+
`rshell_server.py` is a server script that needs to be executed on the host machine to facilitate communication between the host and the EFI Shell target system. Server works on queue with address ip -> command to execute, it provides a RESTful API that allows the host to send commands and receive responses from the EFI Shell:
1109+
1110+
* `/execute_command` - Endpoint to execute commands on the EFI Shell target system:
1111+
Form fields:
1112+
* `timeout` - Timeout for command execution.
1113+
* `command` - Command to be executed.
1114+
* `ip` - IP address of the EFI Shell target system.
1115+
* `/post_result` - Endpoint to post results back to the host.
1116+
Headers fields:
1117+
* `CommandID` - Unique identifier for the command.
1118+
* `rc` - Return code of the executed command.
1119+
Body:
1120+
* Command output.
1121+
* `/exception` - Endpoint to handle exceptions that may occur during communication.
1122+
Headers fields:
1123+
* `CommandID` - Unique identifier for the command.
1124+
Body:
1125+
* Exception details.
1126+
* `/getCommandToExecute` - Endpoint to retrieve commands to be executed on the EFI Shell target system. Returns commandline with generated CommandID.
1127+
* `/health/<ip>` - Endpoint to check the health status of the connection.
1128+
1129+
`rshell.py` is a Connection class that calls RESTful API endpoints provided by `rshell_server.py` to execute commands on the EFI Shell target system. If required, starts `rshell_server.py` on the host machine.
11041130

11051131
## OS supported:
11061132
* LNX

examples/rshell_example.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (C) 2025 Intel Corporation
2+
# SPDX-License-Identifier: MIT
3+
import logging
4+
logging.basicConfig(level=logging.DEBUG)
5+
from mfd_connect.rshell import RShellConnection
6+
7+
# LINUX
8+
conn = RShellConnection(ip="10.10.10.10") # start and connect to rshell server
9+
# conn = RShellConnection(ip="10.10.10.10", server_ip="10.10.10.11") # connect to rshell server
10+
conn.execute_command("ls")
11+
conn.disconnect(True)

mfd_connect/rshell.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
# Copyright (C) 2025 Intel Corporation
2+
# SPDX-License-Identifier: MIT
3+
"""RShell Connection Class."""
4+
5+
import logging
6+
import sys
7+
import time
8+
import typing
9+
from ipaddress import IPv4Address, IPv6Address
10+
from subprocess import CalledProcessError
11+
12+
import requests
13+
from mfd_common_libs import add_logging_level, log_levels, TimeoutCounter
14+
from mfd_typing.cpu_values import CPUArchitecture
15+
from mfd_typing.os_values import OSBitness, OSName, OSType
16+
17+
from mfd_connect.local import LocalConnection
18+
from mfd_connect.pathlib.path import CustomPath, custom_path_factory
19+
from mfd_connect.process.base import RemoteProcess
20+
21+
from .base import Connection, ConnectionCompletedProcess
22+
23+
if typing.TYPE_CHECKING:
24+
from pydantic import (
25+
BaseModel, # from pytest_mfd_config.models.topology import ConnectionModel
26+
)
27+
28+
29+
logger = logging.getLogger(__name__)
30+
add_logging_level(level_name="MODULE_DEBUG", level_value=log_levels.MODULE_DEBUG)
31+
add_logging_level(level_name="CMD", level_value=log_levels.CMD)
32+
add_logging_level(level_name="OUT", level_value=log_levels.OUT)
33+
34+
35+
class RShellConnection(Connection):
36+
"""RShell Connection Class."""
37+
38+
def __init__(
39+
self,
40+
ip: str | IPv4Address | IPv6Address,
41+
server_ip: str | IPv4Address | IPv6Address | None = "127.0.0.1",
42+
model: "BaseModel | None" = None,
43+
cache_system_data: bool = True,
44+
connection_timeout: int = 60,
45+
):
46+
"""
47+
Initialize RShellConnection.
48+
49+
:param ip: The IP address of the RShell server.
50+
:param server_ip: The IP address of the server to connect to (optional).
51+
:param model: The Pydantic model to use for the connection (optional).
52+
:param cache_system_data: Whether to cache system data (default: True).
53+
"""
54+
super().__init__(model=model, cache_system_data=cache_system_data)
55+
self._ip = ip
56+
self.server_ip = server_ip if server_ip else "127.0.0.1"
57+
self.server_process = None
58+
if server_ip == "127.0.0.1":
59+
# start Rshell server
60+
self.server_process = self._run_server()
61+
time.sleep(5)
62+
timeout = TimeoutCounter(connection_timeout)
63+
while not timeout:
64+
logger.log(level=log_levels.MODULE_DEBUG, msg="Checking RShell server health")
65+
status_code = requests.get(
66+
f"http://{self.server_ip}/health/{self._ip}", proxies={"no_proxy": "*"}
67+
).status_code
68+
if status_code == 200:
69+
logger.log(level=log_levels.MODULE_DEBUG, msg="RShell server is healthy")
70+
break
71+
time.sleep(5)
72+
else:
73+
raise TimeoutError("Connection of Client to RShell server timed out")
74+
75+
def disconnect(self, stop_client: bool = False) -> None:
76+
"""
77+
Disconnect connection.
78+
79+
Stop local RShell server if established.
80+
81+
:param stop_client: Whether to stop the RShell client (default: False).
82+
"""
83+
if stop_client:
84+
logger.log(level=log_levels.MODULE_DEBUG, msg="Stopping RShell client")
85+
self.execute_command("end")
86+
if self.server_process:
87+
logger.log(level=log_levels.MODULE_DEBUG, msg="Stopping RShell server")
88+
self.server_process.kill()
89+
logger.log(level=log_levels.MODULE_DEBUG, msg="RShell server stopped")
90+
logger.log(level=log_levels.MODULE_DEBUG, msg=self.server_process.stdout_text)
91+
92+
def _run_server(self) -> RemoteProcess:
93+
"""Run RShell server locally."""
94+
conn = LocalConnection()
95+
server_file = conn.path(__file__).parent / "rshell_server.py"
96+
return conn.start_process(f"{conn.modules().sys.executable} {server_file}")
97+
98+
def execute_command(
99+
self,
100+
command: str,
101+
*,
102+
input_data: str | None = None,
103+
cwd: str | None = None,
104+
timeout: int | None = None,
105+
env: dict | None = None,
106+
stderr_to_stdout: bool = False,
107+
discard_stdout: bool = False,
108+
discard_stderr: bool = False,
109+
skip_logging: bool = False,
110+
expected_return_codes: list[int] | None = None,
111+
shell: bool = False,
112+
custom_exception: type[CalledProcessError] | None = None,
113+
) -> ConnectionCompletedProcess:
114+
"""
115+
Execute a command on the remote server.
116+
117+
:param command: The command to execute.
118+
:param timeout: The timeout for the command execution (optional).
119+
:return: The result of the command execution.
120+
"""
121+
if input_data is not None:
122+
logger.log(
123+
level=log_levels.MODULE_DEBUG,
124+
msg="Input data is not supported for RShellConnection and will be ignored.",
125+
)
126+
127+
if cwd is not None:
128+
logger.log(
129+
level=log_levels.MODULE_DEBUG,
130+
msg="CWD is not supported for RShellConnection and will be ignored.",
131+
)
132+
133+
if env is not None:
134+
logger.log(
135+
level=log_levels.MODULE_DEBUG,
136+
msg="Environment variables are not supported for RShellConnection and will be ignored.",
137+
)
138+
139+
if env is not None:
140+
logger.log(
141+
level=log_levels.MODULE_DEBUG,
142+
msg="Environment variables are not supported for RShellConnection and will be ignored.",
143+
)
144+
145+
if stderr_to_stdout:
146+
logger.log(
147+
level=log_levels.MODULE_DEBUG,
148+
msg="Redirecting stderr to stdout is not supported for RShellConnection and will be ignored.",
149+
)
150+
151+
if discard_stdout:
152+
logger.log(
153+
level=log_levels.MODULE_DEBUG,
154+
msg="Discarding stdout is not supported for RShellConnection and will be ignored.",
155+
)
156+
157+
if discard_stderr:
158+
logger.log(
159+
level=log_levels.MODULE_DEBUG,
160+
msg="Discarding stderr is not supported for RShellConnection and will be ignored.",
161+
)
162+
163+
if skip_logging:
164+
logger.log(
165+
level=log_levels.MODULE_DEBUG,
166+
msg="Skipping logging is not supported for RShellConnection and will be ignored.",
167+
)
168+
169+
if expected_return_codes is not None:
170+
logger.log(
171+
level=log_levels.MODULE_DEBUG,
172+
msg="Expected return codes are not supported for RShellConnection and will be ignored.",
173+
)
174+
175+
if shell:
176+
logger.log(
177+
level=log_levels.MODULE_DEBUG,
178+
msg="Shell execution is not supported for RShellConnection and will be ignored.",
179+
)
180+
181+
if custom_exception:
182+
logger.log(
183+
level=log_levels.MODULE_DEBUG,
184+
msg="Custom exceptions are not supported for RShellConnection and will be ignored.",
185+
)
186+
timeout_string = f" with timeout {timeout} seconds" if timeout is not None else ""
187+
logger.log(level=log_levels.CMD, msg=f"Executing >{self._ip}> '{command}',{timeout_string}")
188+
189+
response = requests.post(
190+
f"http://{self.server_ip}/execute_command",
191+
data={"command": command, "timeout": timeout, "ip": self._ip},
192+
proxies={"no_proxy": "*"},
193+
)
194+
completed_process = ConnectionCompletedProcess(
195+
args=command,
196+
stdout=response.text,
197+
return_code=int(response.headers.get("rc", -1)),
198+
)
199+
logger.log(
200+
level=log_levels.MODULE_DEBUG,
201+
msg=f"Finished executing '{command}', rc={completed_process.return_code}",
202+
)
203+
if skip_logging:
204+
return completed_process
205+
206+
stdout = completed_process.stdout
207+
if stdout:
208+
logger.log(level=log_levels.OUT, msg=f"stdout>>\n{stdout}")
209+
210+
return completed_process
211+
212+
def path(self, *args, **kwargs) -> CustomPath:
213+
"""Path represents a filesystem path."""
214+
if sys.version_info >= (3, 12):
215+
kwargs["owner"] = self
216+
return custom_path_factory(*args, **kwargs)
217+
218+
return CustomPath(*args, owner=self, **kwargs)
219+
220+
def get_os_name(self) -> OSName: # noqa: D102
221+
raise NotImplementedError
222+
223+
def get_os_type(self) -> OSType: # noqa: D102
224+
raise NotImplementedError
225+
226+
def get_os_bitness(self) -> OSBitness: # noqa: D102
227+
raise NotImplementedError
228+
229+
def get_cpu_architecture(self) -> CPUArchitecture: # noqa: D102
230+
raise NotImplementedError
231+
232+
def restart_platform(self) -> None: # noqa: D102
233+
raise NotImplementedError
234+
235+
def shutdown_platform(self) -> None: # noqa: D102
236+
raise NotImplementedError
237+
238+
def wait_for_host(self, timeout: int = 60) -> None: # noqa: D102
239+
raise NotImplementedError

0 commit comments

Comments
 (0)