Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit 17156eb

Browse files
authored
Merge pull request #196 from bennyz/tftp-driver
introduce tftp driver
2 parents fc95707 + 11a2b58 commit 17156eb

File tree

14 files changed

+1973
-0
lines changed

14 files changed

+1973
-0
lines changed

contrib/drivers/tftp/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
__pycache__/
2+
.coverage
3+
coverage.xml
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

contrib/drivers/tftp/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Jumpstarter Driver for TFTP
2+
3+
Simple TFTP server driver for Jumpstarter
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: jumpstarter.dev/v1alpha1
2+
kind: ExporterConfig
3+
endpoint: grpc.jumpstarter.192.168.0.203.nip.io:8082
4+
token: "<token>"
5+
export:
6+
serial:
7+
type: "jumpstarter.drivers.pyserial.driver.PySerial"
8+
config:
9+
url: "/dev/ttyUSB0"
10+
baudrate: 1843200
11+
tftp:
12+
type: jumpstarter_driver_tftp.driver.TftpServer
13+
config:
14+
root_dir: "/var/lib/tftpboot/"
15+
host: "192.168.1.111"
16+
port: 6969
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import time
2+
3+
from jumpstarter_driver_tftp.driver import FileNotFound, TftpError
4+
5+
from jumpstarter.common.utils import env
6+
7+
8+
def test_tftp_upload():
9+
with env() as client:
10+
try:
11+
client.tftp.start()
12+
print("TFTP server started")
13+
14+
time.sleep(1)
15+
16+
test_file = "test.bin"
17+
with open(test_file, "wb") as f:
18+
f.write(b"Hello from TFTP streaming test!")
19+
20+
try:
21+
client.tftp.put_local_file(test_file)
22+
print(f"Successfully uploaded {test_file}")
23+
24+
files = client.tftp.list_files()
25+
print(f"Files in TFTP root: {files}")
26+
27+
if test_file in files:
28+
client.tftp.delete_file(test_file)
29+
print(f"Successfully deleted {test_file}")
30+
else:
31+
print(f"Warning: {test_file} not found in TFTP root")
32+
33+
except TftpError as e:
34+
print(f"TFTP operation failed: {e}")
35+
except FileNotFound as e:
36+
print(f"File not found: {e}")
37+
38+
except Exception as e:
39+
print(f"Error: {e}")
40+
finally:
41+
try:
42+
client.tftp.stop()
43+
print("TFTP server stopped")
44+
except Exception as e:
45+
print(f"Error stopping server: {e}")
46+
47+
48+
if __name__ == "__main__":
49+
test_tftp_upload()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
[project]
2+
name = "jumpstarter-driver-tftp"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
authors = [
7+
{ name = "Benny Zlotnik", email = "bzlotnik@redhat.com" }
8+
]
9+
requires-python = ">=3.12"
10+
dependencies = [
11+
"anyio>=4.6.2.post1",
12+
"jumpstarter",
13+
"aiofiles>=24.1.0"
14+
]
15+
16+
[tool.hatch.version]
17+
source = "vcs"
18+
raw-options = { 'root' = '../../../'}
19+
20+
[tool.hatch.metadata.hooks.vcs.urls]
21+
Homepage = "https://jumpstarter.dev"
22+
source_archive = "https://github.com/jumpstarter-dev/repo/archive/{commit_hash}.zip"
23+
24+
[tool.pytest.ini_options]
25+
#addopts = "--cov --cov-report=html --cov-report=xml"
26+
log_cli = true
27+
log_cli_level = "INFO"
28+
testpaths = ["src"]
29+
asyncio_mode = "auto"
30+
31+
[build-system]
32+
requires = ["hatchling", "hatch-vcs"]
33+
build-backend = "hatchling.build"
34+
35+
[dependency-groups]
36+
dev = [
37+
"pytest-cov>=6.0.0",
38+
"pytest>=8.3.3",
39+
"ruff>=0.7.1",
40+
41+
]

contrib/drivers/tftp/src/jumpstarter_driver_tftp/__init__.py

Whitespace-only changes.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from dataclasses import dataclass
2+
from pathlib import Path
3+
4+
from opendal import Operator
5+
6+
from jumpstarter.client import DriverClient
7+
from jumpstarter.client.adapters import OpendalAdapter
8+
9+
10+
@dataclass(kw_only=True)
11+
class TftpServerClient(DriverClient):
12+
"""
13+
Client interface for TFTP Server driver
14+
15+
This client provides methods to control a TFTP server and manage files on it.
16+
Supports file operations like uploading from various storage backends through OpenDAL.
17+
"""
18+
19+
def start(self):
20+
"""
21+
Start the TFTP server
22+
23+
Initializes and starts the TFTP server if it's not already running.
24+
The server will listen on the configured host and port.
25+
"""
26+
self.call("start")
27+
28+
def stop(self):
29+
"""
30+
Stop the TFTP server
31+
32+
Stops the running TFTP server and releases associated resources.
33+
34+
Raises:
35+
ServerNotRunning: If the server is not currently running
36+
"""
37+
self.call("stop")
38+
39+
def list_files(self) -> list[str]:
40+
"""
41+
List files in the TFTP server root directory
42+
43+
Returns:
44+
list[str]: A list of filenames present in the TFTP server's root directory
45+
"""
46+
return self.call("list_files")
47+
48+
def put_file(self, operator: Operator, path: str):
49+
"""
50+
Upload a file to the TFTP server using an OpenDAL operator
51+
52+
Args:
53+
operator (Operator): OpenDAL operator for accessing the source storage
54+
path (str): Path to the file in the source storage system
55+
56+
Returns:
57+
str: Name of the uploaded file
58+
"""
59+
filename = Path(path).name
60+
with OpendalAdapter(client=self, operator=operator, path=path, mode="rb") as handle:
61+
return self.call("put_file", filename, handle)
62+
63+
def put_local_file(self, filepath: str):
64+
"""
65+
Upload a file from the local filesystem to the TFTP server
66+
Note: this doesn't use TFTP to upload.
67+
68+
Args:
69+
filepath (str): Path to the local file to upload
70+
71+
Returns:
72+
str: Name of the uploaded file
73+
74+
Example:
75+
>>> client.put_local_file("/path/to/local/file.txt")
76+
"""
77+
absolute = Path(filepath).resolve()
78+
with OpendalAdapter(client=self, operator=Operator("fs", root="/"), path=str(absolute), mode="rb") as handle:
79+
return self.call("put_file", absolute.name, handle)
80+
81+
def delete_file(self, filename: str):
82+
"""
83+
Delete a file from the TFTP server
84+
85+
Args:
86+
filename (str): Name of the file to delete
87+
88+
Raises:
89+
FileNotFound: If the specified file doesn't exist
90+
TftpError: If deletion fails for other reasons
91+
"""
92+
return self.call("delete_file", filename)
93+
94+
def get_host(self) -> str:
95+
"""
96+
Get the host address the TFTP server is listening on
97+
98+
Returns:
99+
str: The IP address or hostname the server is bound to
100+
"""
101+
return self.call("get_host")
102+
103+
def get_port(self) -> int:
104+
"""
105+
Get the port number the TFTP server is listening on
106+
107+
Returns:
108+
int: The port number (default is 69)
109+
"""
110+
return self.call("get_port")

0 commit comments

Comments
 (0)