container.py 5.2 KB

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