container.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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. if config.ssh_auth_sock:
  105. volumes += [f"{config.ssh_auth_sock}:{config.ssh_auth_sock}:Z"]
  106. volumes += [
  107. f"{vol}:{self.to_source_path(vol)}:ro" for vol in config.data_sources
  108. ]
  109. volume_args = [a for vol in volumes for a in ["-v", vol]]
  110. secrets_args = [
  111. a
  112. for s in config.secret_sources
  113. for a in ["--secret", f"{s.name},mode=0{s.mode:o}"]
  114. ]
  115. args = (
  116. [
  117. "-h",
  118. self.hostname,
  119. "--detach",
  120. "--name",
  121. container_name,
  122. "-e",
  123. "SSH_AUTH_SOCK",
  124. "-e",
  125. "TZ=Europe/Paris",
  126. "-e",
  127. "SSH_KEY_NAME",
  128. "-e",
  129. f"HOST_LOGIN={self.login}",
  130. "--security-opt=label=disable",
  131. ]
  132. + volume_args
  133. + secrets_args
  134. + [self.image]
  135. )
  136. Podman.run(args)
  137. def rm(self):
  138. Podman.rm(["-f", self.name])
  139. def exec(self, cmd: list[str], env_vars: list[str] = []):
  140. args = ["-ti"]
  141. args += [a for var in env_vars for a in ["-e", var]]
  142. subprocess.run(args + [self.name] + cmd)
  143. Podman.exec(args + [self.name] + cmd)
  144. @staticmethod
  145. def to_source_path(path: Path):
  146. mount_base = PurePosixPath("/mnt") / "source"
  147. inner_path = PurePosixPath(path)
  148. with_drive = PurePosixPath(inner_path.parts[0].replace(":", "")).joinpath(
  149. *inner_path.parts[1:]
  150. )
  151. return mount_base / with_drive.relative_to(with_drive.anchor)
  152. @classmethod
  153. def new(cls, hostname: str, login: str):
  154. return cls(hostname, login, f"borgmatic_{login}")