container.py 5.8 KB

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