From 3d5e14590d526ad5b66af67db45103af20ab8204 Mon Sep 17 00:00:00 2001 From: Benny Zlotnik Date: Sat, 8 Feb 2025 16:36:05 +0200 Subject: [PATCH 1/2] tftp: add docs Signed-off-by: Benny Zlotnik --- docs/source/api-reference/drivers/index.md | 2 +- docs/source/api-reference/drivers/tftp.md | 89 +++++++++++++++++++ .../jumpstarter_driver_tftp/driver.py | 72 ++++++++++++++- 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 docs/source/api-reference/drivers/tftp.md diff --git a/docs/source/api-reference/drivers/index.md b/docs/source/api-reference/drivers/index.md index 59d7467b5..cc3aa3b92 100644 --- a/docs/source/api-reference/drivers/index.md +++ b/docs/source/api-reference/drivers/index.md @@ -10,6 +10,6 @@ Drivers packages from the [drivers](https://github.com/jumpstarter-dev/jumpstart can.md pyserial.md sdwire.md +tftp.md ustreamer.md yepkit.md -``` diff --git a/docs/source/api-reference/drivers/tftp.md b/docs/source/api-reference/drivers/tftp.md new file mode 100644 index 000000000..544611d60 --- /dev/null +++ b/docs/source/api-reference/drivers/tftp.md @@ -0,0 +1,89 @@ +# TFTP Driver + +**driver**: `jumpstarter_driver_tftp.driver.Tftp` + +The TFTP driver provides a read-only TFTP server that can be used to serve files. + +## Driver Configuration +```yaml +export: + tftp: + type: jumpstarter_driver_tftp.driver.Tftp + config: + root_dir: /var/lib/tftpboot # Directory to serve files from + host: 192.168.1.100 # Host IP to bind to (optional) + port: 69 # Port to listen on (optional) +``` + +### Config parameters + +| Parameter | Description | Type | Required | Default | +|-----------|-------------|------|----------|---------| +| root_dir | Root directory for the TFTP server | str | no | "/var/lib/tftpboot" | +| host | IP address to bind the server to | str | no | auto-detect | +| port | Port number to listen on | int | no | 69 | + +## TftpServerClient API + +```{eval-rst} +.. autoclass:: jumpstarter_driver_tftp.client.TftpServerClient + :members: + :show-inheritance: +``` + +## Exception Classes + +```{eval-rst} +.. autoclass:: jumpstarter_driver_tftp.driver.TftpError + :members: + :show-inheritance: + +.. autoclass:: jumpstarter_driver_tftp.driver.ServerNotRunning + :members: + :show-inheritance: + +.. autoclass:: jumpstarter_driver_tftp.driver.FileNotFound + :members: + :show-inheritance: +``` + +## Examples + +```{doctest} +>>> import tempfile +>>> import os +>>> from jumpstarter_driver_tftp.driver import Tftp +>>> with tempfile.TemporaryDirectory() as tmp_dir: +... # Create a test file +... test_file = os.path.join(tmp_dir, "test.txt") +... with open(test_file, "w") as f: +... _ = f.write("hello") +... +... # Start TFTP server +... tftp = Tftp(root_dir=tmp_dir, host="127.0.0.1", port=6969) +... tftp.start() +... +... # List files +... files = tftp.list_files() +... assert "test.txt" in files +... +... tftp.stop() +``` + +```{testsetup} * +import tempfile +import os +from jumpstarter_driver_tftp.driver import Tftp +from jumpstarter.common.utils import serve + +# Create a persistent temp dir that won't be removed by the example +TEST_DIR = tempfile.mkdtemp(prefix='tftp-test-') +instance = serve(Tftp(root_dir=TEST_DIR, host="127.0.0.1")) +client = instance.__enter__() +``` + +```{testcleanup} * +instance.__exit__(None, None, None) +import shutil +shutil.rmtree(TEST_DIR, ignore_errors=True) +``` diff --git a/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py b/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py index 5ad988090..5d526e2bd 100644 --- a/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py +++ b/packages/jumpstarter-driver-tftp/jumpstarter_driver_tftp/driver.py @@ -29,7 +29,15 @@ class FileNotFound(TftpError): @dataclass(kw_only=True) class Tftp(Driver): - """TFTP Server driver for Jumpstarter""" + """TFTP Server driver for Jumpstarter + + This driver implements a TFTP read-only server. + + Attributes: + root_dir (str): Root directory for the TFTP server. Defaults to "/var/lib/tftpboot" + host (str): IP address to bind the server to. If empty, will use the default route interface + port (int): Port number to listen on. Defaults to 69 (standard TFTP port) + """ root_dir: str = "/var/lib/tftpboot" host: str = field(default='') @@ -97,6 +105,14 @@ async def _wait_for_shutdown(self): @export def start(self): + """Start the TFTP server. + + The server will start listening for incoming TFTP requests on the configured + host and port. If the server is already running, a warning will be logged. + + Raises: + TftpError: If the server fails to start or times out during initialization + """ if self.server_thread is not None and self.server_thread.is_alive(): self.logger.warning("TFTP server is already running") return @@ -116,6 +132,11 @@ def start(self): @export def stop(self): + """Stop the TFTP server. + + Initiates a graceful shutdown of the server and waits for all active transfers + to complete. If the server is not running, a warning will be logged. + """ if self.server_thread is None or not self.server_thread.is_alive(): self.logger.warning("stop called - TFTP server is not running") return @@ -131,10 +152,28 @@ def stop(self): @export def list_files(self) -> list[str]: + """List all files available in the TFTP server root directory. + + Returns: + list[str]: A list of filenames present in the root directory + """ return os.listdir(self.root_dir) @export async def put_file(self, filename: str, src_stream, client_checksum: str): + """Upload a file to the TFTP server. + + Args: + filename (str): Name of the file to create + src_stream: Source stream to read the file data from + client_checksum (str): SHA256 checksum of the file for verification + + Returns: + str: The filename that was uploaded + + Raises: + TftpError: If the file upload fails or path validation fails + """ file_path = os.path.join(self.root_dir, filename) try: @@ -152,6 +191,18 @@ async def put_file(self, filename: str, src_stream, client_checksum: str): @export def delete_file(self, filename: str): + """Delete a file from the TFTP server. + + Args: + filename (str): Name of the file to delete + + Returns: + str: The filename that was deleted + + Raises: + FileNotFound: If the specified file does not exist + TftpError: If the deletion operation fails + """ file_path = os.path.join(self.root_dir, filename) if not os.path.exists(file_path): @@ -165,6 +216,15 @@ def delete_file(self, filename: str): @export def check_file_checksum(self, filename: str, client_checksum: str) -> bool: + """Check if a file matches the expected checksum. + + Args: + filename (str): Name of the file to check + client_checksum (str): Expected SHA256 checksum + + Returns: + bool: True if the file exists and matches the checksum, False otherwise + """ file_path = os.path.join(self.root_dir, filename) self.logger.debug(f"checking checksum for file: {filename}") self.logger.debug(f"file path: {file_path}") @@ -181,10 +241,20 @@ def check_file_checksum(self, filename: str, client_checksum: str) -> bool: @export def get_host(self) -> str: + """Get the host address the server is bound to. + + Returns: + str: The IP address or hostname + """ return self.host @export def get_port(self) -> int: + """Get the port number the server is listening on. + + Returns: + int: The port number + """ return self.port def close(self): From f0f240eaaec82f2648105e9090d75b1525e7cf1c Mon Sep 17 00:00:00 2001 From: Benny Zlotnik <2139890+bennyz@users.noreply.github.com> Date: Mon, 10 Feb 2025 16:44:15 +0200 Subject: [PATCH 2/2] Update docs/source/api-reference/drivers/tftp.md Co-authored-by: Nick Cao --- docs/source/api-reference/drivers/tftp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/api-reference/drivers/tftp.md b/docs/source/api-reference/drivers/tftp.md index 544611d60..68cc8c261 100644 --- a/docs/source/api-reference/drivers/tftp.md +++ b/docs/source/api-reference/drivers/tftp.md @@ -26,7 +26,7 @@ export: ## TftpServerClient API ```{eval-rst} -.. autoclass:: jumpstarter_driver_tftp.client.TftpServerClient +.. autoclass:: jumpstarter_driver_tftp.client.TftpServerClient() :members: :show-inheritance: ```