Skip to content

Commit

Permalink
Implement file access via Cobbler XMLRPC API with static fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
affenull2345 committed Jan 15, 2024
1 parent 22d5994 commit f8ffd18
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 23 deletions.
9 changes: 4 additions & 5 deletions src/cobbler_tftp/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import logging
import logging.config
import xmlrpc.client

from cobbler_tftp.server.tftp import TFTPServer
from cobbler_tftp.settings import Settings
Expand All @@ -23,11 +24,9 @@ def run_server(application_settings: Settings):
logging.config.fileConfig(str(logging_conf))
logging.debug("Server starting...")
try:
address = application_settings.tftp_addr
port = application_settings.tftp_port
retries = application_settings.tftp_retries
timeout = application_settings.tftp_timeout
server = TFTPServer(address, port, retries, timeout)
# TODO: use username and password
api = xmlrpc.client.Server(application_settings.uri)
server = TFTPServer(api, application_settings)
except: # pylint: disable=bare-except
logging.exception("Fatal exception while setting up server")
return
Expand Down
118 changes: 101 additions & 17 deletions src/cobbler_tftp/server/tftp.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,91 @@
"""

import logging
import os
import xmlrpc.client

from fbtftp import BaseHandler, BaseServer, ResponseData


class CobblerResponseData(ResponseData):
"""File-like object representing the response from the TFTP server."""
"""
File-like object representing the response from the TFTP server.
Data is fetched from the API in chunks. These chunks may be larger
than the TFTP request chunks, so the returned chunks are cached.
"""

def __init__(self):
pass
def __init__(self, api, path, prefetch_size, static_dir):
self._api = api
self._path = path
self._size = None
self._chunk = None
self._chunk_offset = 0
self._file_offset = 0
self._prefetch_size = prefetch_size
self._static_file = None
self._file_exists = False
self._static_dir = static_dir
self._file_error = None

def _file_fallback(self, err):
logging.warning("Could not fetch %s from server: %r", self._path, err)
if self._static_dir is None:
raise err
path = os.path.normpath(os.path.join("/", self._path)).strip("/")
self._static_file = open(self._static_dir / path, "rb")
self._size = self._static_file.seek(0, os.SEEK_END)
self._static_file.seek(0)

def read(self, n):
return b""
if self._file_error is not None:
raise self._file_error
if self._static_file is not None:
return self._static_file.read(n)

if n > self._prefetch_size:
raise ValueError("Chunk too large")
if self._chunk is not None:
end = self._chunk_offset + n
if end <= self._prefetch_size:
data = self._chunk[self._chunk_offset : end]
self._chunk_offset = end
return data
self._file_offset += self._chunk_offset
try:
binary, self._size = self._api.get_tftp_file(
self._path, self._file_offset, self._prefetch_size
)
except xmlrpc.client.Error as err:
if self._file_exists:
raise err
self._file_fallback(err)
return self.read(n)
self._file_exists = True
self._chunk = binary.data
data = self._chunk[:n]
self._chunk_offset = len(data)
return data

def size(self):
return 0
if self._chunk is None:
try:
binary, self._size = self._api.get_tftp_file(
self._path, 0, self._prefetch_size
)
except xmlrpc.client.Error as err:
# fbtftp does not handle errors in size(), so the exception
# needs to be cached and raised in read().
if self._file_exists:
self._file_error = err
else:
try:
self._file_fallback(err)
except Exception as err:
self._file_error = err
else:
self._chunk = binary.data
self._file_exists = True
return self._size

def close(self):
pass
Expand Down Expand Up @@ -50,39 +120,53 @@ class CobblerRequestHandler(BaseHandler):
Handles TFTP requests using the Cobbler API.
"""

def __init__(self, server_addr, peer, path, options):
def __init__(self, server_addr, peer, path, options, api, settings):
"""
Initialize a handler for a specific request.
:param server_addr: Tuple containing the server address and port.
:param peer: Tuple containing the client address and port.
:param path: Request file path.
:param options: Options requested by the client.
:param api: The Cobbler API object.
:param static_fallback_dir: Path to directory with static TFTP files.
"""
# Future arguments can be handled here
self._api = api
self._settings = settings
super().__init__(server_addr, peer, path, options, handler_stats_cb)

def get_response_data(self):
return CobblerResponseData()
return CobblerResponseData(
self._api,
self._path,
self._settings.prefetch_size,
self._settings.static_fallback_dir,
)


class TFTPServer(BaseServer):
"""
Implements a TFTP server for the Cobbler API using the CobblerRequestHandler.
"""

def __init__(self, address, port, retries, timeout):
def __init__(self, api, settings):
"""
Initialize the TFTP server.
:param address: IP address to listen on.
:param port: UDP Port to listen on.
:param retries: Maximum number of retries when sending a packet fails.
:param timeout: Timeout for sending packets.
:param api: The Cobbler API object.
:param settings: The cobbler-tftp settings.
"""
# Future arguments can be handled here
self.handler_stats_cb = handler_stats_cb
super().__init__(address, port, retries, timeout, server_stats_cb)
self._api = api
self._settings = settings
super().__init__(
settings.tftp_addr,
settings.tftp_port,
settings.tftp_retries,
settings.tftp_timeout,
server_stats_cb,
)

def get_handler(self, server_addr, peer, path, options):
return CobblerRequestHandler(server_addr, peer, path, options)
return CobblerRequestHandler(
server_addr, peer, path, options, self._api, self._settings
)
15 changes: 14 additions & 1 deletion src/cobbler_tftp/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ def __init__(
username: str,
password: Optional[str],
password_file: Optional[Path],
prefetch_size: int,
tftp_addr: str,
tftp_port: int,
tftp_retries: int,
tftp_timeout: int,
logging_conf: Optional[Path],
static_fallback_dir: Optional[Path],
) -> None:
"""
Initialize a new instance of the Settings.
Expand All @@ -46,6 +48,8 @@ def __init__(
:param username: Username to authenticate at Cobbler's API.
:param password: Password for authentication with Cobbler.
:param password_file: Path to the file containing the password.
:param prefetch_size: Chunk size when fetching files from Cobbler.
:param static_fallback_dir: Path to the directory with static TFTP files.
"""
# pylint: disable=R0913

Expand All @@ -54,11 +58,13 @@ def __init__(
self.pid_file_path: Path = pid_file_path
self.uri: str = uri
self.user: str = username
self.prefetch_size: int = prefetch_size
self.tftp_addr: str = tftp_addr
self.tftp_port: int = tftp_port
self.tftp_retries: int = tftp_retries
self.tftp_timeout: int = tftp_timeout
self.logging_conf: Optional[Path] = logging_conf
self.static_fallback_dir: Optional[Path] = static_fallback_dir
self.__password: Optional[str] = password
self.__password_file: Optional[Path] = password_file

Expand Down Expand Up @@ -147,11 +153,16 @@ def build_settings(
password_file: Optional[Path] = Path(cobbler_settings.get("password_file", None)) # type: ignore
else:
password_file = None
tftp_settings = self._settings_dict.get("tftp", None)
prefetch_size: int = self._settings_dict.get("prefetch_size", 4096) # type: ignore
tftp_settings = self._settings_dict.get("tftp", {})
tftp_addr: str = tftp_settings.get("address", "127.0.0.1") # type: ignore
tftp_port: int = tftp_settings.get("port", 69) # type: ignore
tftp_retries: int = tftp_settings.get("retries", 5) # type: ignore
tftp_timeout: int = tftp_settings.get("timeout", 2) # type: ignore
if tftp_settings.get("static_fallback_dir", None) is not None: # type: ignore
static_fallback_dir: Optional[Path] = Path(tftp_settings.get("static_fallback_dir", None)) # type: ignore
else:
static_fallback_dir = None
if self._settings_dict.get("logging_conf", None) is not None: # type: ignore
logging_conf: Optional[Path] = Path(self._settings_dict.get("logging_conf", None)) # type: ignore
else:
Expand All @@ -166,11 +177,13 @@ def build_settings(
username,
password,
password_file,
prefetch_size,
tftp_addr,
tftp_port,
tftp_retries,
tftp_timeout,
logging_conf,
static_fallback_dir,
)

return settings
Expand Down
5 changes: 5 additions & 0 deletions src/cobbler_tftp/settings/data/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ cobbler:
username: "cobbler"
password: "cobbler"
# password_file: "/etc/cobbler-tftp/cobbler_password"
# Chunk size used for fetching files from Cobbler.
# Lower values result in slower transfers, higher values increase memory
# consumption. Extremely large values may cause TFTP timeouts.
prefetch_size: 4096
# TFTP server configuration
tftp:
address: "127.0.0.1"
port: 69
retries: 5
timeout: 2
static_fallback_dir: "/srv/tftpboot"
logging_conf: "/etc/cobbler-tftp/logging.conf"
2 changes: 2 additions & 0 deletions src/cobbler_tftp/settings/migrations/v1_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
Optional("username"): str,
Optional(Or("password", "password_file", only_one=True)): Or(str, Path),
},
Optional("prefetch_size"): int,
Optional("tftp"): {
Optional("address"): str,
Optional("port"): int,
Optional("retries"): int,
Optional("timeout"): int,
Optional("static_fallback_dir"): str,
},
Optional("logging_conf"): str,
}
Expand Down

0 comments on commit f8ffd18

Please sign in to comment.