-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add basic configuration module (#15)
* Add pydantic-settings dependency * Add basic types to represent configuration * Change minimum python version to 3.11 * Read configuration from file and add basic exception type * Add pyfakefs as test dependency * Add tomli-w test dependency * Add tests for config parsing * Implement TOML parsing with passing tests * Add docstring to `__init__` * Add tests for initializing FRR & Cabourotte configurations from configuration * Implement `from_configuration` classmethod for `FRRPrefix` passing tests * Implement `from_configuration` for `CabourotteHealthcheck` * Use seconds as unit for interval configuration * Add docstrings * Add raised exceptions to `from_configuration` docstrings * Remove pydantic-settings Only needed for settings from env vars which we do not support(yet) * Rename `sample_configuration` fixture to `sample_configuration_file` and move it to conftest * Add sample configuration fixture * Add basic test for creating a configuration from a TOML file * Add basic config initialization implementation passing tests * Add private marker to configuration module while removing it from all internal modules * Move ServiceConfiguration to module * Move healthcheck and prefix configurations into modules * Organize tests * Add `get_type_by_name` to get configuration types by their name * Use `get_type_by_name` within `ServiceConfiguration` * Add missing type hint * Generalize healthcheck and prefix configuration types * Upgrade pydantic to `2.5.2` * Add new parent class for sub-configurations, creating a generic `from_configuration` method * Flatten and simplify configuration module * Expose `MainConfiguration` and `ConfigurationError` from configuration module * Flatten and DRY tests * Add some tests for `MainConfiguration` * Add test for configuration exception * Handle all possible `Exception` types in `ConfigurationError` * Add type ignore unused for pylance error * Add comment about type ignore * Fix double punctuation typo * Fix typo "sucessfully" * Add example configuration schema to README
- Loading branch information
Showing
19 changed files
with
712 additions
and
110 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
from anycastd._configuration.exceptions import ConfigurationError | ||
from anycastd._configuration.main import MainConfiguration |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import tomllib | ||
from pathlib import Path | ||
|
||
from pydantic import ValidationError | ||
|
||
|
||
class ConfigurationError(Exception): | ||
"""There was an error with the configuration file.""" | ||
|
||
def __init__( | ||
self, | ||
path: Path, | ||
exc: OSError | ||
| tomllib.TOMLDecodeError | ||
| KeyError | ||
| ValueError | ||
| ValidationError | ||
| TypeError, | ||
): | ||
msg = f"Could not read from configuration file {path}" | ||
match exc: | ||
case OSError(): | ||
msg += f" due to an I/O error: {exc}" | ||
case tomllib.TOMLDecodeError(): | ||
msg += f" due to a TOML syntax error: {exc}" | ||
case KeyError(): | ||
msg += f" due to missing required key: {exc}" | ||
case ValueError() | ValidationError() | TypeError(): | ||
msg += f": {exc}" | ||
case _: | ||
msg += f", an unexpected exception occurred: {exc!r}" | ||
raise TypeError(msg) from exc # type: ignore[unused-ignore] | ||
# Pylance sees the above as a type error, while mypy does not. | ||
# Unfortunately, Pylance does not implement its own per-line | ||
# ignore mechanism, so the generic type ignore is used here. | ||
|
||
super().__init__(msg) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import datetime | ||
from typing import Literal, TypeAlias | ||
|
||
from anycastd._configuration.sub import SubConfiguration | ||
|
||
|
||
class HealthcheckConfiguration(SubConfiguration): | ||
"""A generic class to group all healtcheck configuration classes.""" | ||
|
||
|
||
class CabourotteHealthcheck(HealthcheckConfiguration): | ||
"""The configuration for a Cabourotte healthcheck. | ||
Attributes: | ||
name: The name of the healthcheck. | ||
url: The URL of the cabourotte http endpoint. | ||
interval: The interval in seconds at which the healthcheck should be executed. | ||
""" | ||
|
||
name: str | ||
url: str = "http://127.0.0.1:9013" | ||
interval: datetime.timedelta = datetime.timedelta(seconds=5) | ||
|
||
|
||
Name: TypeAlias = Literal["cabourotte"] | ||
|
||
_type_by_name: dict[Name, type[HealthcheckConfiguration]] = { | ||
"cabourotte": CabourotteHealthcheck, | ||
} | ||
|
||
|
||
def get_type_by_name(name: Name) -> type[HealthcheckConfiguration]: | ||
"""Get a healthcheck configuration class by it's name as used in the configuration. | ||
Args: | ||
name: The name of the healtcheck type. | ||
Returns: | ||
The confiuration class for the type of healthcheck. | ||
Raises: | ||
ValueError: There is no healthcheck type with the given name. | ||
""" | ||
try: | ||
return _type_by_name[name] | ||
except KeyError as exc: | ||
raise ValueError(f"Unknown healthcheck type: {name}") from exc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import tomllib | ||
from dataclasses import dataclass | ||
from pathlib import Path | ||
from typing import Self | ||
|
||
from pydantic import ValidationError | ||
|
||
from anycastd._configuration.exceptions import ConfigurationError | ||
from anycastd._configuration.service import ServiceConfiguration | ||
|
||
|
||
@dataclass | ||
class MainConfiguration: | ||
"""The top-level configuration object.""" | ||
|
||
services: tuple[ServiceConfiguration, ...] | ||
|
||
@classmethod | ||
def from_toml_file(cls, path: Path) -> Self: | ||
"""Create a configuration instance from a TOML configuration file. | ||
Args: | ||
path: The path to the configuration file. | ||
Raises: | ||
ConfigurationError: The configuration could not be read or parsed. | ||
""" | ||
config = _read_toml_configuration(path) | ||
try: | ||
return cls( | ||
services=tuple( | ||
ServiceConfiguration.from_name_and_options(name, options) | ||
for name, options in config["services"].items() | ||
) | ||
) | ||
except (KeyError, ValueError, TypeError, ValidationError) as exc: | ||
raise ConfigurationError(path, exc) from exc | ||
|
||
|
||
def _read_toml_configuration(path: Path) -> dict: | ||
"""Read a TOML configuration file. | ||
Args: | ||
path: The path to the configuration file. | ||
Returns: | ||
The parsed configuration data. | ||
Raises: | ||
ConfigurationError: The configuration could not be read or parsed. | ||
""" | ||
try: | ||
with path.open("rb") as f: | ||
data = tomllib.load(f) | ||
except (OSError, tomllib.TOMLDecodeError) as exc: | ||
raise ConfigurationError(path, exc) from exc | ||
|
||
return data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from ipaddress import IPv4Network, IPv6Network | ||
from pathlib import Path | ||
from typing import Literal, TypeAlias | ||
|
||
from anycastd._configuration.sub import SubConfiguration | ||
|
||
|
||
class PrefixConfiguration(SubConfiguration): | ||
"""A generic class to group all prefix configuration classes.""" | ||
|
||
|
||
class FRRPrefix(PrefixConfiguration): | ||
"""The configuration for a FRRouting prefix. | ||
Attributes: | ||
prefix: The prefix to advertise. | ||
vrf: The VRF to advertise the prefix in. | ||
vtysh: The path to the vtysh binary. | ||
""" | ||
|
||
prefix: IPv4Network | IPv6Network | ||
vrf: int | None = None | ||
vtysh: Path = Path("/usr/bin/vtysh") | ||
|
||
|
||
Name: TypeAlias = Literal["frrouting"] | ||
|
||
_type_by_name: dict[Name, type[PrefixConfiguration]] = { | ||
"frrouting": FRRPrefix, | ||
} | ||
|
||
|
||
def get_type_by_name(name: Name) -> type[PrefixConfiguration]: | ||
"""Get a prefix configuration class by it's name as used in the configuration. | ||
Args: | ||
name: The name of the prefix type. | ||
Returns: | ||
The confiuration class for the type of prefix. | ||
Raises: | ||
ValueError: There is no prefix type with the given name. | ||
""" | ||
try: | ||
return _type_by_name[name] | ||
except KeyError as exc: | ||
raise ValueError(f"Unknown prefix type: {name}") from exc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
from dataclasses import dataclass | ||
from typing import Self | ||
|
||
from anycastd._configuration import healthcheck, prefix | ||
from anycastd._configuration.healthcheck import HealthcheckConfiguration | ||
from anycastd._configuration.prefix import PrefixConfiguration | ||
|
||
|
||
@dataclass | ||
class ServiceConfiguration: | ||
"""The configuration for a service.""" | ||
|
||
name: str | ||
prefixes: tuple[PrefixConfiguration, ...] | ||
checks: tuple[HealthcheckConfiguration, ...] | ||
|
||
@classmethod | ||
def from_name_and_options(cls, name: str, options: dict) -> Self: | ||
"""Create an instance from the configuration format. | ||
Args: | ||
name: The name of the service. | ||
options: The configuration options for the service. | ||
Returns: | ||
A new ServiceConfiguration instance. | ||
Raises: | ||
KeyError: The configuration is missing a required key. | ||
ValueError: The configuration contains an invalid value. | ||
TypeError: The configuration has an invalid type. | ||
ValidationError: Failed to validate the configuration. | ||
""" | ||
prefixes = [] | ||
for prefix_type, prefix_configs in options["prefixes"].items(): | ||
prefix_class = prefix.get_type_by_name(prefix_type) | ||
|
||
for config in prefix_configs: | ||
prefixes.append(prefix_class.from_configuration(config)) | ||
|
||
checks = [] | ||
for check_type, check_configs in options["checks"].items(): | ||
check_class = healthcheck.get_type_by_name(check_type) | ||
|
||
for config in check_configs: | ||
checks.append(check_class.from_configuration(config)) | ||
|
||
return cls(name=name, prefixes=tuple(prefixes), checks=tuple(checks)) |
Oops, something went wrong.