container.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import os
  2. import subprocess
  3. from pathlib import Path, PurePosixPath
  4. from dataclasses import dataclass
  5. from pc_backup.secret import Secret
  6. from pc_backup.env import is_windows
  7. from pc_backup.podman import Podman
  8. def read_data_sources(file: Path) -> list[Path]:
  9. with open(file) as f:
  10. paths = f.readlines()
  11. return [Path(p_str.strip()).expanduser() for p_str in paths]
  12. @dataclass
  13. class Configuration:
  14. secret_sources: list[Secret]
  15. data_sources: list[Path]
  16. borgmatic_d_path: Path
  17. borgmatic_path: Path
  18. history_file: Path
  19. ssh_auth_sock: Path | None
  20. @staticmethod
  21. def get_config_dir() -> Path:
  22. if is_windows():
  23. program_data = Path(os.getenv("ProgramData"))
  24. return program_data / "pc_backup"
  25. else:
  26. return Path.home() / ".config" / "pc_backup"
  27. def __str__(self) -> str:
  28. ret = []
  29. ret += ["secret_sources:"]
  30. for s in self.secret_sources:
  31. ret += [f" {s.name}: {s}"]
  32. ret += ["data_sources:"]
  33. for d in self.data_sources:
  34. ret += [f" {d}"]
  35. ret += ["borgmatic setups:"]
  36. for f in self.borgmatic_d_path.iterdir():
  37. ret += [f" {f.name}"]
  38. ret += ["paths:"]
  39. for p in ["borgmatic_d_path", "borgmatic_path", "history_file"]:
  40. ret += [f" {p}: {getattr(self, p)}"]
  41. return "\n".join(ret)
  42. @classmethod
  43. def read(cls, hostname: str, login: str, config_dir: Path):
  44. # The configuration directory has to be organized like this :
  45. #
  46. # .
  47. # ├── borgmatic
  48. # │   └── common.yaml
  49. # ├── <host1>
  50. # │   ├── <user1>
  51. # │   │   ├── borgmatic.d
  52. # │   │   │   ├── <config1>.yaml
  53. # │   │   │   ├── <config2>.yaml
  54. # │   │   │   └── windows.yaml
  55. # │   │   ├── data_sources
  56. # │   │   └── secret_sources
  57. # │   └── <user2>
  58. # │   ├── borgmatic.d
  59. # │   │   ├── ...
  60. # │   ├── data_sources
  61. # │   └── secret_sources
  62. # └── <host2>
  63. # ├── <user1>
  64. # │   ├── borgmatic.d
  65. # │   │   ├── ...
  66. # │   ├── data_sources
  67. # │   └── secret_sources
  68. # └── <user2>
  69. # ├── borgmatic.d
  70. # │   ├── ...
  71. # ├── data_sources
  72. # └── secret_sources
  73. specific_config_dir = config_dir / hostname / login
  74. secret_sources_file = specific_config_dir / "secret_sources"
  75. data_sources_file = specific_config_dir / "data_sources"
  76. ssh_auth_sock = os.getenv("SSH_AUTH_SOCK")
  77. return cls(
  78. secret_sources=Secret.read_sources(secret_sources_file),
  79. data_sources=read_data_sources(data_sources_file),
  80. borgmatic_d_path=specific_config_dir / "borgmatic.d",
  81. borgmatic_path=config_dir / "borgmatic",
  82. history_file=specific_config_dir / ".bash_history",
  83. ssh_auth_sock=Path(ssh_auth_sock) if ssh_auth_sock else None,
  84. )
  85. @dataclass
  86. class BorgmaticContainer:
  87. hostname: str
  88. login: str
  89. name: str
  90. image: str = "ghcr.io/borgmatic-collective/borgmatic"
  91. def run(self, config: Configuration):
  92. container_name = f"borgmatic_{self.login}"
  93. config.history_file.touch()
  94. volumes = [
  95. f"{config.borgmatic_d_path}:/etc/borgmatic.d/",
  96. f"{config.borgmatic_path}:/etc/borgmatic/",
  97. f"{config.history_file}:/root/.bash_history",
  98. "borg_ssh_dir:/root/.ssh",
  99. "borg_config:/root/.config/borg",
  100. "borg_cache:/root/.cache/borg",
  101. "borgmatic_state:/root/.local/state/borgmatic",
  102. "borgmatic_log:/root/.local/share/borgmatic",
  103. ]
  104. volumes += [
  105. f"{vol}:{self.to_source_path(vol)}:ro" for vol in config.data_sources
  106. ]
  107. Podman.run(
  108. self.image,
  109. container_name,
  110. hostname=self.hostname,
  111. env=["TZ=Europe/Paris", f"HOST_LOGIN={self.login}"],
  112. ssh_auth_sock=config.ssh_auth_sock,
  113. volumes=volumes,
  114. secrets=config.secret_sources,
  115. )
  116. def rm(self):
  117. Podman.rm(self.name)
  118. def exec(self, cmd: list[str], *, interactive=False):
  119. Podman.exec(self.name, *cmd, interactive=interactive)
  120. @staticmethod
  121. def to_source_path(path: Path):
  122. mount_base = PurePosixPath("/mnt") / "source"
  123. inner_path = PurePosixPath(path)
  124. with_drive = PurePosixPath(inner_path.parts[0].replace(":", "")).joinpath(
  125. *inner_path.parts[1:]
  126. )
  127. return mount_base / with_drive.relative_to(with_drive.anchor)
  128. @classmethod
  129. def new(cls, hostname: str, login: str):
  130. return cls(hostname, login, f"borgmatic_{login}")