-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
use a config.yaml instead of cli args
- Loading branch information
1 parent
60da68d
commit e75f2a1
Showing
7 changed files
with
582 additions
and
80 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
timezone: America/Los_Angeles | ||
sources: | ||
- path: /foo | ||
snapshots: /snapshots | ||
preserve: 1y 3m 30d 24h 60M 60s | ||
backups: | ||
s3: | ||
bucket: btrfs2s3-test | ||
endpoint: https://foo | ||
region: | ||
profile: | ||
verify: | ||
pipe_through: | ||
- zstd -T0 -12 | ||
- gpg --encrypt -r 0xFE592029B2CB9D04 --trust-model always |
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,198 @@ | ||
"""Code for manipulating config for btrfs2s3.""" | ||
|
||
from __future__ import annotations | ||
|
||
from collections import namedtuple | ||
from typing import Any | ||
from typing import cast | ||
from typing import TYPE_CHECKING | ||
from typing import TypedDict | ||
|
||
from cfgv import Array | ||
from cfgv import check_array | ||
from cfgv import check_string | ||
from cfgv import check_type | ||
from cfgv import load_from_filename | ||
from cfgv import Map | ||
from cfgv import OptionalNoDefault | ||
from cfgv import OptionalRecurse | ||
from cfgv import Required | ||
from cfgv import RequiredRecurse | ||
from typing_extensions import NotRequired | ||
from yaml import safe_load | ||
|
||
from btrfs2s3.preservation import Params | ||
|
||
if TYPE_CHECKING: | ||
from os import PathLike | ||
|
||
|
||
class Error(Exception): | ||
"""The top-level class for exceptions generated by this module.""" | ||
|
||
|
||
class InvalidConfigError(Error): | ||
"""An error for invalid config.""" | ||
|
||
|
||
def _check_preserve(v: Any) -> None: # noqa: ANN401 | ||
check_string(v) | ||
try: | ||
Params.parse(v) | ||
except ValueError as ex: | ||
msg = "Expected a valid schedule" | ||
raise InvalidConfigError(msg) from ex | ||
|
||
|
||
# this is the same style used in cfgv | ||
_OptionalRecurseNoDefault = namedtuple( # noqa: PYI024 | ||
"_OptionalRecurseNoDefault", ("key", "schema") | ||
) | ||
_OptionalRecurseNoDefault.check = OptionalRecurse.check # type: ignore[attr-defined] | ||
_OptionalRecurseNoDefault.check_fn = OptionalRecurse.check_fn # type: ignore[attr-defined] | ||
_OptionalRecurseNoDefault.apply_default = OptionalNoDefault.apply_default # type: ignore[attr-defined] | ||
_OptionalRecurseNoDefault.remove_default = OptionalNoDefault.remove_default # type: ignore[attr-defined] | ||
|
||
|
||
class S3EndpointConfig(TypedDict): | ||
"""A config dict for how to talk to an S3 endpoint.""" | ||
|
||
aws_access_key_id: NotRequired[str] | ||
aws_secret_access_key: NotRequired[str] | ||
region_name: NotRequired[str] | ||
profile_name: NotRequired[str] | ||
api_version: NotRequired[str] | ||
verify: NotRequired[bool | str] | ||
endpoint_url: NotRequired[str] | ||
|
||
|
||
_S3_ENDPOINT_SCHEMA = Map( | ||
"S3EndpointConfig", | ||
None, | ||
OptionalNoDefault("aws_access_key_id", check_string), | ||
OptionalNoDefault("aws_secret_access_key", check_string), | ||
OptionalNoDefault("region_name", check_string), | ||
OptionalNoDefault("profile_name", check_string), | ||
OptionalNoDefault("api_version", check_string), | ||
OptionalNoDefault("verify", check_type((bool, str), typename="bool or path")), | ||
OptionalNoDefault("endpoint_url", check_string), | ||
) | ||
|
||
|
||
class S3RemoteConfig(TypedDict): | ||
"""A config dict for how to access an S3 remote.""" | ||
|
||
bucket: str | ||
endpoint: NotRequired[S3EndpointConfig] | ||
|
||
|
||
_S3_SCHEMA = Map( | ||
"S3RemoteConfig", | ||
None, | ||
Required("bucket", check_string), | ||
_OptionalRecurseNoDefault("endpoint", _S3_ENDPOINT_SCHEMA), | ||
) | ||
|
||
|
||
class RemoteConfig(TypedDict): | ||
"""A config dict for how to access a remote.""" | ||
|
||
id: str | ||
s3: NotRequired[S3RemoteConfig] | ||
|
||
|
||
_REMOTE_SCHEMA = Map( | ||
"RemoteConfig", | ||
None, | ||
Required("id", check_string), | ||
RequiredRecurse("s3", _S3_SCHEMA), | ||
) | ||
|
||
|
||
class UploadToRemoteConfig(TypedDict): | ||
"""A config dict for uploading a source to a remote.""" | ||
|
||
id: str | ||
preserve: str | ||
pipe_through: NotRequired[list[list[str]]] | ||
|
||
|
||
_UPLOAD_TO_REMOTE_SCHEMA = Map( | ||
"UploadToRemoteConfig", | ||
"id", | ||
Required("preserve", _check_preserve), | ||
OptionalNoDefault("pipe_through", check_array(check_array(check_string))), | ||
) | ||
|
||
|
||
class SourceConfig(TypedDict): | ||
"""A config dict for a source.""" | ||
|
||
path: str | ||
snapshots: str | ||
upload_to_remotes: list[UploadToRemoteConfig] | ||
|
||
|
||
_SOURCE_SCHEMA = Map( | ||
"SourceConfig", | ||
"path", | ||
Required("path", check_string), | ||
Required("snapshots", check_string), | ||
RequiredRecurse( | ||
"upload_to_remotes", Array(_UPLOAD_TO_REMOTE_SCHEMA, allow_empty=False) | ||
), | ||
) | ||
|
||
|
||
class Config(TypedDict): | ||
"""The top-level config dict. | ||
This just matches the data as it's stored in config.yaml. We don't do any | ||
transformation up front (for example "preserve" values are just their | ||
strings, not Policy objects). | ||
""" | ||
|
||
timezone: str | ||
sources: list[SourceConfig] | ||
remotes: list[RemoteConfig] | ||
|
||
|
||
_SCHEMA = Map( | ||
"Config", | ||
None, | ||
Required("timezone", check_string), | ||
RequiredRecurse("sources", Array(_SOURCE_SCHEMA, allow_empty=False)), | ||
RequiredRecurse("remotes", Array(_REMOTE_SCHEMA, allow_empty=False)), | ||
) | ||
|
||
|
||
def load_from_path(path: str | PathLike[str]) -> Config: | ||
"""Load config from a file path. | ||
This performs some basic syntactic validation on the config, to ensure it | ||
really conforms to the return type. | ||
Args: | ||
path: The path to the config. | ||
Returns: | ||
A Config instance. | ||
Raises: | ||
InvalidConfigError: If the config does not pass validation. | ||
""" | ||
config = cast( | ||
Config, load_from_filename(path, _SCHEMA, safe_load, exc_tp=InvalidConfigError) | ||
) | ||
|
||
remote_ids = {remote["id"] for remote in config["remotes"]} | ||
for source in config["sources"]: | ||
for upload_to_remote in source["upload_to_remotes"]: | ||
if upload_to_remote["id"] not in remote_ids: | ||
msg = ( | ||
f'remote id {upload_to_remote["id"]!r} for source ' | ||
f'{source["path"]!r} is not defined in the list of remotes' | ||
) | ||
raise InvalidConfigError(msg) | ||
|
||
return config |
Oops, something went wrong.