nd.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. from typing import List
  2. import enum
  3. import os
  4. import pathlib
  5. import uuid
  6. import dagger
  7. import jinja2
  8. import imageutils
  9. class Platform:
  10. def __init__(self, platform: str):
  11. self.platform = dagger.Platform(platform)
  12. def escaped(self) -> str:
  13. return str(self.platform).removeprefix("linux/").replace("/", "_")
  14. def __eq__(self, other):
  15. if isinstance(other, Platform):
  16. return self.platform == other.platform
  17. elif isinstance(other, dagger.Platform):
  18. return self.platform == other
  19. else:
  20. return NotImplemented
  21. def __ne__(self, other):
  22. return not (self == other)
  23. def __hash__(self):
  24. return hash(self.platform)
  25. def __str__(self) -> str:
  26. return str(self.platform)
  27. SUPPORTED_PLATFORMS = set(
  28. [
  29. Platform("linux/x86_64"),
  30. Platform("linux/arm64"),
  31. Platform("linux/i386"),
  32. Platform("linux/arm/v7"),
  33. Platform("linux/arm/v6"),
  34. Platform("linux/ppc64le"),
  35. Platform("linux/s390x"),
  36. Platform("linux/riscv64"),
  37. ]
  38. )
  39. SUPPORTED_DISTRIBUTIONS = set(
  40. [
  41. "alpine_3_18",
  42. "alpine_3_19",
  43. "amazonlinux2",
  44. "centos7",
  45. "centos-stream8",
  46. "centos-stream9",
  47. "debian10",
  48. "debian11",
  49. "debian12",
  50. "fedora37",
  51. "fedora38",
  52. "fedora39",
  53. "opensuse15.4",
  54. "opensuse15.5",
  55. "opensusetumbleweed",
  56. "oraclelinux8",
  57. "oraclelinux9",
  58. "rockylinux8",
  59. "rockylinux9",
  60. "ubuntu20.04",
  61. "ubuntu22.04",
  62. "ubuntu23.04",
  63. "ubuntu23.10",
  64. ]
  65. )
  66. class Distribution:
  67. def __init__(self, display_name):
  68. self.display_name = display_name
  69. if self.display_name == "alpine_3_18":
  70. self.docker_tag = "alpine:3.18"
  71. self.builder = imageutils.build_alpine_3_18
  72. self.platforms = SUPPORTED_PLATFORMS
  73. elif self.display_name == "alpine_3_19":
  74. self.docker_tag = "alpine:3.19"
  75. self.builder = imageutils.build_alpine_3_19
  76. self.platforms = SUPPORTED_PLATFORMS
  77. elif self.display_name == "amazonlinux2":
  78. self.docker_tag = "amazonlinux:2"
  79. self.builder = imageutils.build_amazon_linux_2
  80. self.platforms = SUPPORTED_PLATFORMS
  81. elif self.display_name == "centos7":
  82. self.docker_tag = "centos:7"
  83. self.builder = imageutils.build_centos_7
  84. self.platforms = SUPPORTED_PLATFORMS
  85. elif self.display_name == "centos-stream8":
  86. self.docker_tag = "quay.io/centos/centos:stream8"
  87. self.builder = imageutils.build_centos_stream_8
  88. self.platforms = SUPPORTED_PLATFORMS
  89. elif self.display_name == "centos-stream9":
  90. self.docker_tag = "quay.io/centos/centos:stream9"
  91. self.builder = imageutils.build_centos_stream_9
  92. self.platforms = SUPPORTED_PLATFORMS
  93. elif self.display_name == "debian10":
  94. self.docker_tag = "debian:10"
  95. self.builder = imageutils.build_debian_10
  96. self.platforms = SUPPORTED_PLATFORMS
  97. elif self.display_name == "debian11":
  98. self.docker_tag = "debian:11"
  99. self.builder = imageutils.build_debian_11
  100. self.platforms = SUPPORTED_PLATFORMS
  101. elif self.display_name == "debian12":
  102. self.docker_tag = "debian:12"
  103. self.builder = imageutils.build_debian_12
  104. self.platforms = SUPPORTED_PLATFORMS
  105. elif self.display_name == "fedora37":
  106. self.docker_tag = "fedora:37"
  107. self.builder = imageutils.build_fedora_37
  108. self.platforms = SUPPORTED_PLATFORMS
  109. elif self.display_name == "fedora38":
  110. self.docker_tag = "fedora:38"
  111. self.builder = imageutils.build_fedora_38
  112. self.platforms = SUPPORTED_PLATFORMS
  113. elif self.display_name == "fedora39":
  114. self.docker_tag = "fedora:39"
  115. self.platforms = SUPPORTED_PLATFORMS
  116. self.builder = imageutils.build_fedora_39
  117. elif self.display_name == "opensuse15.4":
  118. self.docker_tag = "opensuse/leap:15.4"
  119. self.builder = imageutils.build_opensuse_15_4
  120. self.platforms = SUPPORTED_PLATFORMS
  121. elif self.display_name == "opensuse15.5":
  122. self.docker_tag = "opensuse/leap:15.5"
  123. self.builder = imageutils.build_opensuse_15_5
  124. self.platforms = SUPPORTED_PLATFORMS
  125. elif self.display_name == "opensusetumbleweed":
  126. self.docker_tag = "opensuse/tumbleweed:latest"
  127. self.builder = imageutils.build_opensuse_tumbleweed
  128. self.platforms = SUPPORTED_PLATFORMS
  129. elif self.display_name == "oraclelinux8":
  130. self.docker_tag = "oraclelinux:8"
  131. self.builder = imageutils.build_oracle_linux_8
  132. self.platforms = SUPPORTED_PLATFORMS
  133. elif self.display_name == "oraclelinux9":
  134. self.docker_tag = "oraclelinux:9"
  135. self.builder = imageutils.build_oracle_linux_9
  136. self.platforms = SUPPORTED_PLATFORMS
  137. elif self.display_name == "rockylinux8":
  138. self.docker_tag = "rockylinux:8"
  139. self.builder = imageutils.build_rocky_linux_8
  140. self.platforms = SUPPORTED_PLATFORMS
  141. elif self.display_name == "rockylinux9":
  142. self.docker_tag = "rockylinux:9"
  143. self.builder = imageutils.build_rocky_linux_9
  144. self.platforms = SUPPORTED_PLATFORMS
  145. elif self.display_name == "ubuntu20.04":
  146. self.docker_tag = "ubuntu:20.04"
  147. self.builder = imageutils.build_ubuntu_20_04
  148. self.platforms = SUPPORTED_PLATFORMS
  149. elif self.display_name == "ubuntu22.04":
  150. self.docker_tag = "ubuntu:22.04"
  151. self.builder = imageutils.build_ubuntu_22_04
  152. self.platforms = SUPPORTED_PLATFORMS
  153. elif self.display_name == "ubuntu23.04":
  154. self.docker_tag = "ubuntu:23.04"
  155. self.builder = imageutils.build_ubuntu_23_04
  156. self.platforms = SUPPORTED_PLATFORMS
  157. elif self.display_name == "ubuntu23.10":
  158. self.docker_tag = "ubuntu:23.10"
  159. self.builder = imageutils.build_ubuntu_23_10
  160. self.platforms = SUPPORTED_PLATFORMS
  161. else:
  162. raise ValueError(f"Unknown distribution: {self.display_name}")
  163. def _cache_volume(
  164. self, client: dagger.Client, platform: dagger.Platform, path: str
  165. ) -> dagger.CacheVolume:
  166. tag = "_".join([self.display_name, Platform(platform).escaped()])
  167. return client.cache_volume(f"{path}-{tag}")
  168. def build(
  169. self, client: dagger.Client, platform: dagger.Platform
  170. ) -> dagger.Container:
  171. if platform not in self.platforms:
  172. raise ValueError(
  173. f"Building {self.display_name} is not supported on {platform}."
  174. )
  175. ctr = self.builder(client, platform)
  176. ctr = imageutils.install_cargo(ctr)
  177. return ctr
  178. class FeatureFlags(enum.Flag):
  179. DBEngine = enum.auto()
  180. GoPlugin = enum.auto()
  181. ExtendedBPF = enum.auto()
  182. LogsManagement = enum.auto()
  183. MachineLearning = enum.auto()
  184. BundledProtobuf = enum.auto()
  185. class NetdataInstaller:
  186. def __init__(
  187. self,
  188. platform: Platform,
  189. distro: Distribution,
  190. repo_root: pathlib.Path,
  191. prefix: pathlib.Path,
  192. features: FeatureFlags,
  193. ):
  194. self.platform = platform
  195. self.distro = distro
  196. self.repo_root = repo_root
  197. self.prefix = prefix
  198. self.features = features
  199. def _mount_repo(
  200. self, client: dagger.Client, ctr: dagger.Container, repo_root: pathlib.Path
  201. ) -> dagger.Container:
  202. host_repo_root = pathlib.Path(__file__).parent.parent.parent.as_posix()
  203. exclude_dirs = ["build", "fluent-bit/build", "packaging/dag"]
  204. # The installer builds/stores intermediate artifacts under externaldeps/
  205. # We add a volume to speed up rebuilds. The volume has to be unique
  206. # per platform/distro in order to avoid mixing unrelated artifacts
  207. # together.
  208. externaldeps = self.distro._cache_volume(client, self.platform, "externaldeps")
  209. ctr = (
  210. ctr.with_directory(
  211. self.repo_root.as_posix(), client.host().directory(host_repo_root)
  212. )
  213. .with_workdir(self.repo_root.as_posix())
  214. .with_mounted_cache(
  215. os.path.join(self.repo_root, "externaldeps"), externaldeps
  216. )
  217. )
  218. return ctr
  219. def install(self, client: dagger.Client, ctr: dagger.Container) -> dagger.Container:
  220. args = ["--dont-wait", "--dont-start-it", "--disable-telemetry"]
  221. if FeatureFlags.DBEngine not in self.features:
  222. args.append("--disable-dbengine")
  223. if FeatureFlags.GoPlugin not in self.features:
  224. args.append("--disable-go")
  225. if FeatureFlags.ExtendedBPF not in self.features:
  226. args.append("--disable-ebpf")
  227. if FeatureFlags.MachineLearning not in self.features:
  228. args.append("--disable-ml")
  229. if FeatureFlags.BundledProtobuf not in self.features:
  230. args.append("--use-system-protobuf")
  231. args.extend(["--install-prefix", self.prefix.parent.as_posix()])
  232. ctr = self._mount_repo(client, ctr, self.repo_root.as_posix())
  233. ctr = ctr.with_env_variable(
  234. "NETDATA_CMAKE_OPTIONS", "-DCMAKE_BUILD_TYPE=Debug"
  235. ).with_exec(["./netdata-installer.sh"] + args)
  236. return ctr
  237. class Endpoint:
  238. def __init__(self, hostname: str, port: int):
  239. self.hostname = hostname
  240. self.port = port
  241. def __str__(self):
  242. return ":".join([self.hostname, str(self.port)])
  243. class ChildStreamConf:
  244. def __init__(
  245. self,
  246. installer: NetdataInstaller,
  247. destinations: List[Endpoint],
  248. api_key: uuid.UUID,
  249. ):
  250. self.installer = installer
  251. self.substitutions = {
  252. "enabled": "yes",
  253. "destination": " ".join([str(dst) for dst in destinations]),
  254. "api_key": api_key,
  255. "timeout_seconds": 60,
  256. "default_port": 19999,
  257. "send_charts_matching": "*",
  258. "buffer_size_bytes": 1024 * 1024,
  259. "reconnect_delay_seconds": 5,
  260. "initial_clock_resync_iterations": 60,
  261. }
  262. def render(self) -> str:
  263. tmpl_path = pathlib.Path(__file__).parent / "files/child_stream.conf"
  264. with open(tmpl_path) as fp:
  265. tmpl = jinja2.Template(fp.read())
  266. return tmpl.render(**self.substitutions)
  267. class ParentStreamConf:
  268. def __init__(self, installer: NetdataInstaller, api_key: uuid.UUID):
  269. self.installer = installer
  270. self.substitutions = {
  271. "api_key": str(api_key),
  272. "enabled": "yes",
  273. "allow_from": "*",
  274. "default_history": 3600,
  275. "health_enabled_by_default": "auto",
  276. "default_postpone_alarms_on_connect_seconds": 60,
  277. "multiple_connections": "allow",
  278. }
  279. def render(self) -> str:
  280. tmpl_path = pathlib.Path(__file__).parent / "files/parent_stream.conf"
  281. with open(tmpl_path) as fp:
  282. tmpl = jinja2.Template(fp.read())
  283. return tmpl.render(**self.substitutions)
  284. class StreamConf:
  285. def __init__(self, child_conf: ChildStreamConf, parent_conf: ParentStreamConf):
  286. self.child_conf = child_conf
  287. self.parent_conf = parent_conf
  288. def render(self) -> str:
  289. child_section = self.child_conf.render() if self.child_conf else ""
  290. parent_section = self.parent_conf.render() if self.parent_conf else ""
  291. return "\n".join([child_section, parent_section])
  292. class AgentContext:
  293. def __init__(
  294. self,
  295. client: dagger.Client,
  296. platform: dagger.Platform,
  297. distro: Distribution,
  298. installer: NetdataInstaller,
  299. endpoint: Endpoint,
  300. api_key: uuid.UUID,
  301. allow_children: bool,
  302. ):
  303. self.client = client
  304. self.platform = platform
  305. self.distro = distro
  306. self.installer = installer
  307. self.endpoint = endpoint
  308. self.api_key = api_key
  309. self.allow_children = allow_children
  310. self.parent_contexts = []
  311. self.built_distro = False
  312. self.built_agent = False
  313. def add_parent(self, parent_context: "AgentContext"):
  314. self.parent_contexts.append(parent_context)
  315. def build_container(self) -> dagger.Container:
  316. ctr = self.distro.build(self.client, self.platform)
  317. ctr = self.installer.install(self.client, ctr)
  318. if len(self.parent_contexts) == 0 and not self.allow_children:
  319. return ctr.with_exposed_port(self.endpoint.port)
  320. destinations = [parent_ctx.endpoint for parent_ctx in self.parent_contexts]
  321. child_stream_conf = ChildStreamConf(self.installer, destinations, self.api_key)
  322. parent_stream_conf = None
  323. if self.allow_children:
  324. parent_stream_conf = ParentStreamConf(self.installer, self.api_key)
  325. stream_conf = StreamConf(child_stream_conf, parent_stream_conf)
  326. # write the stream conf to localhost and cp it in the container
  327. host_stream_conf_path = pathlib.Path(
  328. f"/tmp/{self.endpoint.hostname}_stream.conf"
  329. )
  330. with open(host_stream_conf_path, "w") as fp:
  331. fp.write(stream_conf.render())
  332. ctr_stream_conf_path = self.installer.prefix / "etc/netdata/stream.conf"
  333. ctr = ctr.with_file(
  334. ctr_stream_conf_path.as_posix(),
  335. self.client.host().file(host_stream_conf_path.as_posix()),
  336. )
  337. ctr = ctr.with_exposed_port(self.endpoint.port)
  338. return ctr