Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement daemon with logging and TFTP server #20

Merged
merged 2 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .prospector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pycodestyle:
# https://github.com/psf/black/issues/354#issuecomment-397685631
- E203
- W503
# Disabled due to the overlap with pylint
- E722

pydocstyle:
run: true
Expand Down
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 @@
"""

Check notice on line 1 in src/cobbler_tftp/server/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/__init__.py#L1

One-line docstring should fit on one line with quotes (found 3) (D200)
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):

Check notice on line 17 in src/cobbler_tftp/server/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/__init__.py#L17

Unused function 'run_server' (unused-function)
"""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() # type: ignore

Check notice on line 39 in src/cobbler_tftp/server/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/__init__.py#L39

Access to a protected member _metrics_timer of a client class (protected-access)
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 @@
"""

Check notice on line 1 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L1

One-line docstring should fit on one line with quotes (found 3) (D200)
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):

Check notice on line 13 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L13

Missing docstring in __init__ (D107)
pass

def read(self, n):

Check notice on line 16 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L16

Missing docstring in public method (D102)

Check notice on line 16 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L16

Unused argument 'n' (unused-argument)

Check notice on line 16 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L16

Unused variable 'n' (unused-variable)
return b""

def size(self):

Check notice on line 19 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L19

Missing docstring in public method (D102)
return 0

def close(self):

Check notice on line 22 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L22

Missing docstring in public method (D102)
pass


def handler_stats_cb(stats):

Check notice on line 26 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L26

Missing docstring in public function (D103)
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):

Check notice on line 48 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L48

Too few public methods (1/2) (too-few-public-methods)
"""
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):

Check notice on line 65 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L65

Missing docstring in public method (D102)
return CobblerResponseData()


class TFTPServer(BaseServer):

Check notice on line 69 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L69

Too few public methods (1/2) (too-few-public-methods)
"""
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):

Check notice on line 87 in src/cobbler_tftp/server/tftp.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/server/tftp.py#L87

Missing docstring in public method (D102)
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
Loading