Skip to content

Commit b38783f

Browse files
committed
feat: implement HTTP server for remote UEFI shell capability
Signed-off-by: Lasota, Adrian <[email protected]>
1 parent 998b5d3 commit b38783f

File tree

5 files changed

+489
-1
lines changed

5 files changed

+489
-1
lines changed

examples/rshell_example.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
# print(conn.execute_command("end")) # stop client
12+
conn.disconnect(True)

mfd_connect/rshell.py

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

mfd_connect/rshell_client.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Copyright (C) 2025 Intel Corporation
2+
# SPDX-License-Identifier: MIT
3+
"""This is a sample demonstration HTTP client application
4+
that can run on UEFI shell with the help of Python UEFI
5+
interpreter.
6+
Make sure that the Python UEFI interpreter is compiled with
7+
Socket module support.
8+
"""
9+
10+
__version__ = "1.0.0"
11+
12+
try:
13+
import httplib as client
14+
except ImportError:
15+
from http import client
16+
import sys
17+
import os
18+
import time
19+
20+
# get http server ip
21+
http_server = sys.argv[1]
22+
23+
os_name = os.name
24+
25+
26+
def _sleep(interval):
27+
"""This API simulates the sleep function for EFI shell
28+
as the sleep API from time module is not working on
29+
EFI shell
30+
:param interval time period the system to be in idle
31+
"""
32+
start_ts = time.time()
33+
while time.time() < start_ts + interval:
34+
pass
35+
36+
37+
time.sleep = _sleep
38+
39+
40+
def _get_command():
41+
# construct the list of tests by interacting with server
42+
conn.request("GET", "getCommandToExecute")
43+
rsp = conn.getresponse()
44+
status = rsp.status
45+
_id = rsp.getheader("CommandID")
46+
if status == 204:
47+
return None
48+
49+
print("Waiting for command from server: ")
50+
data_received = rsp.read()
51+
print(data_received)
52+
test_list = data_received.split(b",")
53+
54+
return test_list[0], _id # return only the first command
55+
56+
57+
while True:
58+
# Connect to server
59+
conn = client.HTTPConnection(http_server)
60+
61+
# get the command from server
62+
_command = _get_command()
63+
if not _command:
64+
conn.close()
65+
time.sleep(5)
66+
continue
67+
cmd_str, _id = _command
68+
cmd_str = cmd_str.decode("utf-8")
69+
cmd_name = cmd_str.split(" ")[0]
70+
if cmd_name == "end":
71+
print("No more commands available to run")
72+
conn.close()
73+
exit(0)
74+
75+
print("Executing", cmd_str)
76+
77+
out = cmd_name + ".txt"
78+
cmd = cmd_str + " > " + out
79+
80+
rc = os.system(cmd) # execute command on machine
81+
82+
print("Executed the command")
83+
print("Posting the results to server")
84+
# send response to server
85+
try:
86+
if os_name == "edk2":
87+
encoding = "utf-16"
88+
else:
89+
encoding = "utf-8"
90+
91+
f = open(out, "r", encoding=encoding)
92+
93+
conn.request(
94+
"POST",
95+
"post_result",
96+
body=f.read(),
97+
headers={"Content-Type": "text/plain", "Connection": "keep-alive", "CommandID": _id, "rc": rc},
98+
)
99+
f.close()
100+
os.system("del " + out)
101+
except Exception as exp:
102+
conn.request(
103+
"POST",
104+
str("Exception"),
105+
body=cmd + str(exp),
106+
headers={"Content-Type": "text/plain", "Connection": "keep-alive"},
107+
)
108+
109+
print("output posted to server")
110+
conn.close()
111+
print("closing the connection")
112+
time.sleep(1)

0 commit comments

Comments
 (0)