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 f2ff027
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 34 deletions.
Empty file added changelog.d/21.added
Empty file.
63 changes: 50 additions & 13 deletions src/cobbler_tftp/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,22 @@
Cobbler-tftp will be managable as a command-line service.
"""

import os
import sys
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 +79,31 @@ 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)
except:

Check warning on line 100 in src/cobbler_tftp/cli.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/cli.py#L100

No exception type(s) specified

Check notice on line 100 in src/cobbler_tftp/cli.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/cli.py#L100

No exception type(s) specified (bare-except)
# Nothing can be done if the logging setup fails
pass
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 +119,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())
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
38 changes: 38 additions & 0 deletions src/cobbler_tftp/server/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""

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
import sys

Check notice on line 7 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#L7

'sys' imported but unused (F401)

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):
"""Main entry point for the server."""

Check notice on line 19 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#L19

First line should be in imperative mood; try rephrasing (found 'Main') (D401)
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:

Check warning on line 31 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#L31

No exception type(s) specified

Check notice on line 31 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#L31

No exception type(s) specified (bare-except)
logging.exception("Fatal exception while setting up server")
return
try:
server.run()
except:
logging.exception("Fatal exception in server")
server.close()
60 changes: 60 additions & 0 deletions src/cobbler_tftp/server/tftp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""

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

Check warning on line 5 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#L5

Unused import logging

Check notice on line 5 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#L5

Unused import logging (unused-import)

from fbtftp import BaseHandler, BaseServer, ResponseData


class CobblerResponseData(ResponseData):

Check notice on line 10 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#L10

Missing docstring in public class (D101)
def __init__(self):
pass

def read(self, n):

Check notice on line 14 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#L14

Unused argument 'n' (unused-argument)

Check notice on line 14 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#L14

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

def size(self):

Check notice on line 17 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#L17

Missing docstring in public method (D102)
return 0

def close(self):

Check notice on line 20 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#L20

Missing docstring in public method (D102)
pass


def handler_stats_cb(stats):

Check notice on line 24 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#L24

Unused variable 'stats' (unused-variable)
"""
Called by the fbtftp to log handler stats. Currently unused.
"""


def server_stats_cb(stats):

Check notice on line 30 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#L30

Unused variable 'stats' (unused-variable)
"""
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):

Check notice on line 41 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#L41

Missing docstring in __init__ (D107)
# 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 45 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#L45

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


class TFTPServer(BaseServer):

Check notice on line 49 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#L49

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):

Check notice on line 54 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#L54

Missing docstring in __init__ (D107)
# 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 59 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#L59

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", "/tmp/cobbler-tftp.pid")) # type: ignore

Check warning on line 141 in src/cobbler_tftp/settings/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/settings/__init__.py#L141

Probable insecure usage of temp file/directory.
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", "0.0.0.0") # type: ignore

Check warning on line 151 in src/cobbler_tftp/settings/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/cobbler_tftp/settings/__init__.py#L151

Possible binding to all interfaces.
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: "/tmp/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: "0.0.0.0"
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
8 changes: 8 additions & 0 deletions src/cobbler_tftp/settings/migrations/v1_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@
Optional("schema"): float,
Optional("auto_migrate_settings"): bool,
Optional("is_daemon"): bool,
Optional("pid_file_path"): str,
Optional("cobbler"): {
Optional("uri"): str,
Optional("username"): str,
Optional(Or("password", "password_file", only_one=True)): Or(str, Path),
},
Optional("tftp"): {
Optional("address"): str,
Optional("port"): int,
Optional("retries"): int,
Optional("timeout"): int,
},
Optional("logging_conf"): str,
}
)

Expand Down
4 changes: 3 additions & 1 deletion src/cobbler_tftp/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@

# Dictionary type for configuration parameters
# if this type changes: changes __valdiate_module function in migrations/__init__.py
SettingsDict = Dict[str, Union[float, bool, str, Path, Dict[str, Union[str, Path]]]]
SettingsDict = Dict[
str, Union[float, bool, str, Path, Dict[str, Union[int, str, Path]]]
]
7 changes: 6 additions & 1 deletion tests/test_data/valid_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ is_daemon: false
cobbler:
uri: 'http://testmachine.testnetwork.com/api'
username: 'cobbler'
password_file: 'tests/test_data/password_file'
password_file: 'tests/test_data/password_file'
tftp:
address: '127.0.0.1'
port: 1969
retries: 10
timeout: 3
Loading

0 comments on commit f2ff027

Please sign in to comment.