2023-10-15 23:19:16 +00:00
|
|
|
import dataclasses
|
|
|
|
import datetime
|
|
|
|
import os
|
|
|
|
import pathlib
|
|
|
|
import sys
|
|
|
|
from typing import List
|
|
|
|
from typing import Optional
|
|
|
|
|
|
|
|
import plumbum.cmd
|
|
|
|
import plumbum.machines
|
|
|
|
|
|
|
|
|
|
|
|
@dataclasses.dataclass
|
|
|
|
class Config:
|
|
|
|
repo_path: pathlib.Path
|
|
|
|
archive_path: List[str]
|
|
|
|
check: bool = True
|
|
|
|
compact: bool = True
|
|
|
|
repo_init: bool = True
|
|
|
|
repo_init_append_only: bool = False
|
|
|
|
repo_init_quota: Optional[str] = None
|
2023-10-17 02:34:23 +00:00
|
|
|
repo_init_encryption: Optional[str] = None
|
2023-10-15 23:19:16 +00:00
|
|
|
repo_init_overwrite_key: bool = False
|
|
|
|
repo_key_file: pathlib.Path = pathlib.Path("/keys")
|
|
|
|
repo_passphrase: Optional[str] = None
|
|
|
|
repo_passphrase_file: Optional[pathlib.Path] = None
|
|
|
|
prune: bool = False
|
|
|
|
prune_interval: Optional[datetime.timedelta] = None
|
|
|
|
prune_secondly: Optional[int] = None
|
|
|
|
prune_minutely: Optional[int] = None
|
|
|
|
prune_hourly: Optional[int] = None
|
|
|
|
prune_daily: Optional[int] = None
|
|
|
|
prune_weekly: Optional[int] = None
|
|
|
|
prune_monthly: Optional[int] = None
|
|
|
|
prune_yearly: Optional[int] = None
|
|
|
|
archive_name: Optional[str] = None
|
|
|
|
archive_comment: Optional[str] = None
|
|
|
|
archive_exclude_pattern: Optional[str] = None
|
|
|
|
archive_exclude_caches: bool = False
|
|
|
|
archive_exclude_if_present: Optional[str] = None
|
|
|
|
archive_keep_exclude_tags: bool = False
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def build(cls):
|
2023-10-17 02:34:23 +00:00
|
|
|
prefix = "BORGDRONE_"
|
|
|
|
|
|
|
|
try:
|
|
|
|
return cls(
|
|
|
|
repo_path=pathlib.Path(os.environ[f"{prefix}REPO_PATH"]),
|
|
|
|
archive_path=[
|
|
|
|
pathlib.Path(item.strip())
|
|
|
|
for item in os.environ[f"{prefix}ARCHIVE_PATH"].split(",")
|
|
|
|
if item
|
|
|
|
],
|
|
|
|
compact=os.environ.get(f"{prefix}COMPACT", str(cls.compact)).lower()
|
|
|
|
== "true",
|
|
|
|
check=os.environ.get(f"{prefix}CHECK", str(cls.check)).lower()
|
|
|
|
== "true",
|
|
|
|
repo_init=os.environ.get(
|
|
|
|
f"{prefix}REPO_INIT", str(cls.repo_init)
|
|
|
|
).lower()
|
|
|
|
== "true",
|
|
|
|
repo_init_append_only=os.environ.get(
|
|
|
|
f"{prefix}REPO_INIT_APPEND_ONLY", str(cls.repo_init_append_only)
|
|
|
|
).lower()
|
|
|
|
== "true",
|
|
|
|
repo_init_quota=os.environ[f"{prefix}REPO_INIT_QUOTA"]
|
|
|
|
if f"{prefix}REPO_INIT_QUOTA" in os.environ
|
|
|
|
else cls.repo_init_quota,
|
|
|
|
repo_init_encryption=os.environ[f"{prefix}REPO_INIT_ENCRYPTION"]
|
|
|
|
if f"{prefix}REPO_INIT_ENCRYPTION" in os.environ
|
|
|
|
else cls.repo_init_encryption,
|
|
|
|
repo_init_overwrite_key=os.environ.get(
|
|
|
|
f"{prefix}REPO_INIT_OVERWRITE_KEY", str(cls.repo_init_overwrite_key)
|
|
|
|
).lower()
|
|
|
|
== "true",
|
|
|
|
repo_key_file=pathlib.Path(os.environ[f"{prefix}REPO_KEY_FILE"])
|
|
|
|
if f"{prefix}REPO_KEY_FILE" in os.environ
|
|
|
|
else cls.repo_key_file,
|
|
|
|
repo_passphrase=os.environ[f"{prefix}REPO_PASSPHRASE"]
|
|
|
|
if f"{prefix}REPO_PASSPHRASE" in os.environ
|
|
|
|
else cls.repo_passphrase,
|
|
|
|
repo_passphrase_file=pathlib.Path(
|
|
|
|
os.environ[f"{prefix}REPO_PASSPHRASE_FILE"]
|
|
|
|
)
|
|
|
|
if f"{prefix}REPO_PASSPHRASE_FILE" in os.environ
|
|
|
|
else cls.repo_passphrase_file,
|
|
|
|
prune=os.environ.get(f"{prefix}PRUNE", str(cls.prune)).lower()
|
|
|
|
== "true",
|
|
|
|
prune_interval=datetime.timedelta(
|
|
|
|
seconds=int(os.environ[f"{prefix}PRUNE_INTERVAL"])
|
|
|
|
)
|
|
|
|
if f"{prefix}PRUNE_INTERVAL" in os.environ
|
|
|
|
else cls.prune_interval,
|
|
|
|
prune_secondly=int(
|
|
|
|
os.environ.get(f"{prefix}PRUNE_SECONDLY", cls.prune_secondly)
|
|
|
|
),
|
|
|
|
prune_minutely=int(
|
|
|
|
os.environ.get(f"{prefix}PRUNE_MINUTELY", cls.prune_minutely)
|
|
|
|
),
|
|
|
|
prune_hourly=int(
|
|
|
|
os.environ.get(f"{prefix}PRUNE_HOURLY", cls.prune_hourly)
|
|
|
|
),
|
|
|
|
prune_daily=int(
|
|
|
|
os.environ.get(f"{prefix}PRUNE_DAILY", cls.prune_daily)
|
|
|
|
),
|
|
|
|
prune_weekly=int(
|
|
|
|
os.environ.get(f"{prefix}PRUNE_WEEKLY", cls.prune_weekly)
|
|
|
|
),
|
|
|
|
prune_monthly=int(
|
|
|
|
os.environ.get(f"{prefix}PRUNE_MONTHLY", cls.prune_monthly)
|
|
|
|
),
|
|
|
|
prune_yearly=int(
|
|
|
|
os.environ.get(f"{prefix}PRUNE_YEARLY", cls.prune_yearly)
|
|
|
|
),
|
|
|
|
archive_name=os.environ[f"{prefix}ARCHIVE_NAME"]
|
|
|
|
if f"{prefix}ARCHIVE_NAME" in os.environ
|
|
|
|
else cls.archive_name,
|
|
|
|
archive_comment=os.environ[f"{prefix}ARCHIVE_COMMENT"]
|
|
|
|
if f"{prefix}ARCHIVE_COMMENT" in os.environ
|
|
|
|
else cls.archive_comment,
|
|
|
|
archive_exclude_pattern=os.environ[f"{prefix}ARCHIVE_EXCLUDE_PATTERN"]
|
|
|
|
if f"{prefix}ARCHIVE_EXCLUDE_PATTERN" in os.environ
|
|
|
|
else cls.archive_exclude_pattern,
|
|
|
|
archive_exclude_caches=os.environ.get(
|
|
|
|
f"{prefix}ARCHIVE_EXCLUDE_CACHES", str(cls.archive_exclude_caches)
|
|
|
|
).lower()
|
|
|
|
== "true",
|
|
|
|
archive_exclude_if_present=os.environ[
|
|
|
|
f"{prefix}ARCHIVE_EXCLUDE_IF_PRESENT"
|
|
|
|
]
|
|
|
|
if f"{prefix}ARCHIVE_EXCLUDE_IF_PRESENT" in os.environ
|
|
|
|
else cls.archive_exclude_if_present,
|
|
|
|
archive_keep_exclude_tags=os.environ.get(
|
|
|
|
f"{prefix}ARCHIVE_KEEP_EXCLUDE_TAGS",
|
|
|
|
str(cls.archive_keep_exclude_tags),
|
|
|
|
).lower()
|
|
|
|
== "true",
|
|
|
|
)
|
|
|
|
except KeyError as err:
|
|
|
|
raise RuntimeError(f"Required configuration parameter {err} not provided")
|
2023-10-15 23:19:16 +00:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _parse_bool(env: str, value: str) -> bool:
|
|
|
|
if value.lower().strip() == "true":
|
|
|
|
return True
|
|
|
|
elif value.lower().strip() == "false":
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
raise RuntimeError(
|
|
|
|
f"Environment variable {env} contains an unexpected value '{value}', expected one of: 'true', 'false'"
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def check_version(borg: plumbum.machines.LocalCommand):
|
|
|
|
version = borg["--version"]().partition(" ")[-1].strip()
|
|
|
|
|
|
|
|
if int(version.partition(".")[0]) < 2:
|
|
|
|
raise RuntimeError("BorgDrone requires BorgBackup version 2.0 or higher")
|
|
|
|
|
|
|
|
print(f"Using BorgBackup {version} from {borg.executable}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
borg = plumbum.cmd.borg
|
|
|
|
code = 0
|
|
|
|
|
|
|
|
try:
|
|
|
|
check_version(borg)
|
|
|
|
|
|
|
|
config = Config.build()
|
|
|
|
except RuntimeError as err:
|
|
|
|
print(f"FATAL: {err}", file=sys.stderr)
|
|
|
|
code = 1
|
|
|
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
sys.exit(main())
|