Skip to content

Commit

Permalink
Implement daemon with logging and TFTP server
Browse files Browse the repository at this point in the history
  • Loading branch information
affenull2345 committed Jan 10, 2024
1 parent ff1a347 commit a374e83
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 34 deletions.
Empty file added changelog.d/21.added
Empty file.
60 changes: 47 additions & 13 deletions src/cobbler_tftp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
Cobbler-tftp will be managable as a command-line service.
"""

import os
from pathlib import Path
from signal import SIGTERM
from typing import List, Optional

import click
import yaml
from daemon import DaemonContext

try:
import importlib.metadata as importlib_metadata
except ImportError: # use backport for Python versions older than 3.8
import importlib_metadata

from cobbler_tftp.server import run_server
from cobbler_tftp.settings import SettingsFactory

try:
Expand Down Expand Up @@ -74,20 +78,29 @@ def start(
"""
click.echo(cli.__doc__)
click.echo("Initializing Cobbler-tftp server...")
settings_factory: SettingsFactory = SettingsFactory()
# settings_file = SettingsFactory.load_config_file(settings_factory, config)
# environment_variables = SettingsFactory.load_env_variables(settings_factory)
# cli_arguments = SettingsFactory.load_cli_options(
# settings_factory, daemon, enable_automigration, settings
# )
if config is None:
config_path = None
else:
config_path = Path(config)
application_settings = SettingsFactory.build_settings(
settings_factory, config_path, daemon, enable_automigration, settings
application_settings = SettingsFactory().build_settings(
config_path, daemon, enable_automigration, settings
)
print(application_settings)
if application_settings.is_daemon:
click.echo("Starting daemon...")
with DaemonContext():
# All previously open file descriptors are invalid now.
# Files and connections needed for the daemon should be opened
# in run_server or listed in the files_preserve option
# of DaemonContext.

application_settings.pid_file_path.write_text(str(os.getpid()))
try:
run_server(application_settings)
finally:
application_settings.pid_file_path.unlink()
else:
click.echo("Daemon mode disabled, running in foreground.")
run_server(application_settings)


@cli.command()
Expand All @@ -103,16 +116,37 @@ def print_default_config():
"""
Print the default application parameters.
"""
settings_factory: SettingsFactory = SettingsFactory()
click.echo(settings_factory.build_settings(None))
click.echo(SettingsFactory().build_settings(None))


@cli.command()
def stop():
@click.option(
"--config", "-c", type=click.Path(), help="Set location of configuration file."
)
@click.option("--pid-file", "-p", type=click.Path(), help="Set location of PID file.")
def stop(config: Optional[str], pid_file: Optional[str]):
"""
Stop the cobbler-tftp server daemon if it is running
"""
pass
if pid_file is None:
if config is None:
config_path = None
else:
config_path = Path(config)
application_settings = SettingsFactory().build_settings(config_path)
pid_file_path = application_settings.pid_file_path
else:
pid_file_path = Path(pid_file)
try:
pid = int(pid_file_path.read_text(encoding="UTF-8"))
except OSError:
click.echo("Unable to read PID file. The daemon is probably not running.")
return
try:
os.kill(pid, SIGTERM)
except ProcessLookupError:
click.echo("Stale PID file. The daemon is no longer running.")
pid_file_path.unlink()


cli.add_command(start)
Expand Down
39 changes: 39 additions & 0 deletions src/cobbler_tftp/server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
This package contains the actual TFTP server.
"""

import logging
import logging.config

from cobbler_tftp.server.tftp import TFTPServer
from cobbler_tftp.settings import Settings

try:
from importlib.resources import files
except ImportError:
from importlib_resources import files


def run_server(application_settings: Settings):
"""Set up logging, initialize the server and run it."""

logging_conf = application_settings.logging_conf
if logging_conf is None or not logging_conf.exists():
logging_conf = files("cobbler_tftp.settings.data").joinpath("logging.conf")
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)
except: # pylint: disable=bare-except
logging.exception("Fatal exception while setting up server")
return
try:
server.run()
except: # pylint: disable=bare-except
logging.exception("Fatal exception in server")
# fbtftp doesn't clean up after exceptions, so we do it here ourselves
server._metrics_timer.cancel()
88 changes: 88 additions & 0 deletions src/cobbler_tftp/server/tftp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""
This module contains the main TFTP server class.
"""

import logging

from fbtftp import BaseHandler, BaseServer, ResponseData


class CobblerResponseData(ResponseData):
"""File-like object representing the response from the TFTP server."""

def __init__(self):
pass

def read(self, n):
return b""

def size(self):
return 0

def close(self):
pass


def handler_stats_cb(stats):
duration = stats.duration() * 1000
logging.info(
"Spent %fms processing request for %r from %r",
duration,
stats.file_path,
stats.peer,
)
logging.info(
"Error: %r, sent %d bytes with %d retransmits",
stats.error,
stats.bytes_sent,
stats.retransmits,
)


def server_stats_cb(stats):
"""
Called by the fbtftp to log server stats. Currently unused.
"""


class CobblerRequestHandler(BaseHandler):
"""
Handles TFTP requests using the Cobbler API.
"""

def __init__(self, server_addr, peer, path, options):
"""
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.
"""
# Future arguments can be handled here
super().__init__(server_addr, peer, path, options, handler_stats_cb)

def get_response_data(self):
return CobblerResponseData()


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

def __init__(self, address, port, retries, timeout):
"""
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.
"""
# Future arguments can be handled here
self.handler_stats_cb = handler_stats_cb
super().__init__(address, port, retries, timeout, server_stats_cb)

def get_handler(self, server_addr, peer, path, options):
return CobblerRequestHandler(server_addr, peer, path, options)
38 changes: 33 additions & 5 deletions src/cobbler_tftp/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ def __init__(
self,
auto_migrate_settings: bool,
is_daemon: bool,
pid_file_path: Path,
uri: str,
username: str,
password: Union[str, None],
password_file: Union[Path, None],
password: Optional[str],
password_file: Optional[Path],
tftp_addr: str,
tftp_port: int,
tftp_retries: int,
tftp_timeout: int,
logging_conf: Optional[Path],
) -> None:
"""
Initialize a new instance of the Settings.
Expand All @@ -45,10 +51,16 @@ def __init__(

self.auto_migrate_settings: bool = auto_migrate_settings
self.is_daemon: bool = is_daemon
self.pid_file_path: Path = pid_file_path
self.uri: str = uri
self.user: str = username
self.__password: Union[str, None] = password
self.__password_file: Union[Path, None] = password_file
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.__password: Optional[str] = password
self.__password_file: Optional[Path] = password_file

def __repr__(self):
"""
Expand Down Expand Up @@ -126,23 +138,39 @@ def build_settings(
# Type ignores are necessary as at this point it is not known what value comes from that key.
auto_migrate_settings: bool = self._settings_dict.get("auto_migrate_settings", False) # type: ignore
is_daemon: bool = self._settings_dict.get("is_daemon", False) # type: ignore
cobbler_settings = self._settings_dict.get("cobbler", None)
pid_file_path: Path = Path(self._settings_dict.get("pid_file_path", "/run/cobbler-tftp.pid")) # type: ignore
cobbler_settings = self._settings_dict.get("cobbler", {})
uri: str = cobbler_settings.get("uri", "") # type: ignore
username: str = cobbler_settings.get("username", "") # type: ignore
password: str = cobbler_settings.get("password", "") # type: ignore
if cobbler_settings.get("password_file", None) is not None: # type: ignore
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)
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 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:
logging_conf = None

# Create and return a new Settings object
settings = Settings(
auto_migrate_settings,
is_daemon,
pid_file_path,
uri,
username,
password,
password_file,
tftp_addr,
tftp_port,
tftp_retries,
tftp_timeout,
logging_conf,
)

return settings
Expand Down
40 changes: 40 additions & 0 deletions src/cobbler_tftp/settings/data/logging.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# based on https://github.com/cobbler/cobbler/blob/main/config/cobbler/logging_config.conf
[loggers]
keys=root

[handlers]
keys=FileLogger,stdout

[formatters]
keys=Logfile,stdout

[logger_root]
level=DEBUG
handlers=FileLogger,stdout

[logger_parser]
level=DEBUG
handlers=FileLogger
propagate=1
qualname=compiler.parser

[handler_stdout]
class=StreamHandler
level=INFO
formatter=stdout
args=(sys.stdout,)

[handler_FileLogger]
class=FileHandler
level=INFO
formatter=Logfile
args=('/var/log/cobbler-tftp.log', 'a')

[formatter_Logfile]
format=[%(threadName)s] %(asctime)s - %(levelname)s | %(message)s
datefmt=%Y-%m-%dT%H:%M:%S
class=logging.Formatter

[formatter_stdout]
format=%(levelname)s | %(message)s
class=logging.Formatter
10 changes: 9 additions & 1 deletion src/cobbler_tftp/settings/data/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ schema: 1.0
auto_migrate_settings: false
# Run cobbler-tftp as a daemon in the background
is_daemon: true
pid_file_path: "/run/cobbler-tftp.pid"
# Specifications of the cobbler-server
cobbler:
uri: "http://localhost/cobbler_api"
username: "cobbler"
password: "cobbler"
# password_file: "/etc/cobbler-tftp/cobbler_password"
# password_file: "/etc/cobbler-tftp/cobbler_password"
# TFTP server configuration
tftp:
address: "127.0.0.1"
port: 69
retries: 5
timeout: 2
logging_conf: "/etc/cobbler-tftp/logging.conf"
6 changes: 3 additions & 3 deletions src/cobbler_tftp/settings/migrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ def __validate_module(name: ModuleType) -> bool:
"""
# noqa for these lines because we can't use the custom types to check this.
module_methods = {
"validate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]])->bool", # noqa
"normalize": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]]", # noqa
"migrate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[str,pathlib.Path]]]]", # noqa
"validate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]])->bool", # noqa
"normalize": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]]", # noqa
"migrate": "(settings_dict:Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]])->Dict[str,Union[float,bool,str,pathlib.Path,Dict[str,Union[int,str,pathlib.Path]]]]", # noqa
}

for key, value in module_methods.items():
Expand Down
Loading

0 comments on commit a374e83

Please sign in to comment.