main.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. #!/usr/bin/env python3
  2. from typing import Callable, List, Tuple
  3. import asyncio
  4. import enum
  5. import os
  6. import pathlib
  7. import sys
  8. import tempfile
  9. import time
  10. import uuid
  11. import anyio
  12. import click
  13. import dagger
  14. import jinja2
  15. import images as oci_images
  16. class Platform:
  17. def __init__(self, platform: str):
  18. self.platform = dagger.Platform(platform)
  19. def escaped(self) -> str:
  20. return str(self.platform).removeprefix("linux/").replace('/', '_')
  21. def __eq__(self, other):
  22. if isinstance(other, Platform):
  23. return self.platform == other.platform
  24. elif isinstance(other, dagger.Platform):
  25. return self.platform == other
  26. else:
  27. return NotImplemented
  28. def __ne__(self, other):
  29. return not (self == other)
  30. def __hash__(self):
  31. return hash(self.platform)
  32. SUPPORTED_PLATFORMS = set([
  33. Platform("linux/x86_64"),
  34. Platform("linux/arm64"),
  35. Platform("linux/i386"),
  36. Platform("linux/arm/v7"),
  37. Platform("linux/arm/v6"),
  38. Platform("linux/ppc64le"),
  39. Platform("linux/s390x"),
  40. Platform("linux/riscv64"),
  41. ])
  42. class Distribution:
  43. def __init__(self, display_name):
  44. self.display_name = display_name
  45. if self.display_name == "alpine_3_18":
  46. self.docker_tag = "alpine:3.18"
  47. self.builder = oci_images.build_alpine_3_18
  48. self.platforms = SUPPORTED_PLATFORMS
  49. elif self.display_name == "alpine_3_19":
  50. self.docker_tag = "alpine:3.19"
  51. self.builder = oci_images.build_alpine_3_19
  52. self.platforms = SUPPORTED_PLATFORMS
  53. elif self.display_name == "amazonlinux2":
  54. self.docker_tag = "amazonlinux:2"
  55. self.builder = oci_images.build_amazon_linux_2
  56. self.platforms = SUPPORTED_PLATFORMS
  57. elif self.display_name == "centos7":
  58. self.docker_tag = "centos:7"
  59. self.builder = oci_images.build_centos_7
  60. self.platforms = SUPPORTED_PLATFORMS
  61. elif self.display_name == "centos-stream8":
  62. self.docker_tag = "quay.io/centos/centos:stream8"
  63. self.builder = oci_images.build_centos_stream_8
  64. self.platforms = SUPPORTED_PLATFORMS
  65. elif self.display_name == "centos-stream9":
  66. self.docker_tag = "quay.io/centos/centos:stream9"
  67. self.builder = oci_images.build_centos_stream_9
  68. self.platforms = SUPPORTED_PLATFORMS
  69. elif self.display_name == "debian10":
  70. self.docker_tag = "debian:10"
  71. self.builder = oci_images.build_debian_10
  72. self.platforms = SUPPORTED_PLATFORMS
  73. elif self.display_name == "debian11":
  74. self.docker_tag = "debian:11"
  75. self.builder = oci_images.build_debian_11
  76. self.platforms = SUPPORTED_PLATFORMS
  77. elif self.display_name == "debian12":
  78. self.docker_tag = "debian:12"
  79. self.builder = oci_images.build_debian_12
  80. self.platforms = SUPPORTED_PLATFORMS
  81. elif self.display_name == "fedora37":
  82. self.docker_tag = "fedora:37"
  83. self.builder = oci_images.build_fedora_37
  84. self.platforms = SUPPORTED_PLATFORMS
  85. elif self.display_name == "fedora38":
  86. self.docker_tag = "fedora:38"
  87. self.builder = oci_images.build_fedora_38
  88. self.platforms = SUPPORTED_PLATFORMS
  89. elif self.display_name == "fedora39":
  90. self.docker_tag = "fedora:39"
  91. self.platforms = SUPPORTED_PLATFORMS
  92. self.builder = oci_images.build_fedora_39
  93. elif self.display_name == "opensuse15.4":
  94. self.docker_tag = "opensuse/leap:15.4"
  95. self.builder = oci_images.build_opensuse_15_4
  96. self.platforms = SUPPORTED_PLATFORMS
  97. elif self.display_name == "opensuse15.5":
  98. self.docker_tag = "opensuse/leap:15.5"
  99. self.builder = oci_images.build_opensuse_15_5
  100. self.platforms = SUPPORTED_PLATFORMS
  101. elif self.display_name == "opensusetumbleweed":
  102. self.docker_tag = "opensuse/tumbleweed:latest"
  103. self.builder = oci_images.build_opensuse_tumbleweed
  104. self.platforms = SUPPORTED_PLATFORMS
  105. elif self.display_name == "oraclelinux8":
  106. self.docker_tag = "oraclelinux:8"
  107. self.builder = oci_images.build_oracle_linux_8
  108. self.platforms = SUPPORTED_PLATFORMS
  109. elif self.display_name == "oraclelinux9":
  110. self.docker_tag = "oraclelinux:9"
  111. self.builder = oci_images.build_oracle_linux_9
  112. self.platforms = SUPPORTED_PLATFORMS
  113. elif self.display_name == "rockylinux8":
  114. self.docker_tag = "rockylinux:8"
  115. self.builder = oci_images.build_rocky_linux_8
  116. self.platforms = SUPPORTED_PLATFORMS
  117. elif self.display_name == "rockylinux9":
  118. self.docker_tag = "rockylinux:9"
  119. self.builder = oci_images.build_rocky_linux_9
  120. self.platforms = SUPPORTED_PLATFORMS
  121. elif self.display_name == "ubuntu20.04":
  122. self.docker_tag = "ubuntu:20.04"
  123. self.builder = oci_images.build_ubuntu_20_04
  124. self.platforms = SUPPORTED_PLATFORMS
  125. elif self.display_name == "ubuntu22.04":
  126. self.docker_tag = "ubuntu:22.04"
  127. self.builder = oci_images.build_ubuntu_22_04
  128. self.platforms = SUPPORTED_PLATFORMS
  129. elif self.display_name == "ubuntu23.04":
  130. self.docker_tag = "ubuntu:23.04"
  131. self.builder = oci_images.build_ubuntu_23_04
  132. self.platforms = SUPPORTED_PLATFORMS
  133. elif self.display_name == "ubuntu23.10":
  134. self.docker_tag = "ubuntu:23.10"
  135. self.builder = oci_images.build_ubuntu_23_10
  136. self.platforms = SUPPORTED_PLATFORMS
  137. else:
  138. raise ValueError(f"Unknown distribution: {self.display_name}")
  139. def _cache_volume(self, client: dagger.Client, platform: dagger.Platform, path: str) -> dagger.CacheVolume:
  140. tag = "_".join([self.display_name, Platform(platform).escaped()])
  141. return client.cache_volume(f"{path}-{tag}")
  142. def build(self, client: dagger.Client, platform: dagger.Platform) -> dagger.Container:
  143. if platform not in self.platforms:
  144. raise ValueError(f"Building {self.display_name} is not supported on {platform}.")
  145. ctr = self.builder(client, platform)
  146. ctr = oci_images.install_cargo(ctr)
  147. return ctr
  148. class FeatureFlags(enum.Flag):
  149. DBEngine = enum.auto()
  150. GoPlugin = enum.auto()
  151. ExtendedBPF = enum.auto()
  152. LogsManagement = enum.auto()
  153. MachineLearning = enum.auto()
  154. BundledProtobuf = enum.auto()
  155. class NetdataInstaller:
  156. def __init__(self,
  157. platform: Platform,
  158. distro: Distribution,
  159. repo_root: pathlib.Path,
  160. prefix: pathlib.Path,
  161. features: FeatureFlags):
  162. self.platform = platform
  163. self.distro = distro
  164. self.repo_root = repo_root
  165. self.prefix = prefix
  166. self.features = features
  167. def _mount_repo(self, client: dagger.Client, ctr: dagger.Container, repo_root: pathlib.Path) -> dagger.Container:
  168. host_repo_root = pathlib.Path(__file__).parent.parent.parent.as_posix()
  169. exclude_dirs = ["build", "fluent-bit/build"]
  170. # The installer builds/stores intermediate artifacts under externaldeps/
  171. # We add a volume to speed up rebuilds. The volume has to be unique
  172. # per platform/distro in order to avoid mixing unrelated artifacts
  173. # together.
  174. externaldeps = self.distro._cache_volume(client, self.platform, "externaldeps")
  175. ctr = (
  176. ctr.with_directory(self.repo_root.as_posix(), client.host().directory(host_repo_root))
  177. .with_workdir(self.repo_root.as_posix())
  178. .with_mounted_cache(os.path.join(self.repo_root, "externaldeps"), externaldeps)
  179. )
  180. return ctr
  181. def install(self, client: dagger.Client, ctr: dagger.Container) -> dagger.Container:
  182. args = ["--dont-wait", "--dont-start-it", "--disable-telemetry"]
  183. if FeatureFlags.DBEngine not in self.features:
  184. args.append("--disable-dbengine")
  185. if FeatureFlags.GoPlugin not in self.features:
  186. args.append("--disable-go")
  187. if FeatureFlags.ExtendedBPF not in self.features:
  188. args.append("--disable-ebpf")
  189. if FeatureFlags.LogsManagement not in self.features:
  190. args.append("--disable-logsmanagement")
  191. if FeatureFlags.MachineLearning not in self.features:
  192. args.append("--disable-ml")
  193. if FeatureFlags.BundledProtobuf not in self.features:
  194. args.append("--use-system-protobuf")
  195. args.extend(["--install-prefix", self.prefix.as_posix()])
  196. ctr = self._mount_repo(client, ctr, self.repo_root.as_posix())
  197. ctr = (
  198. ctr.with_env_variable('NETDATA_CMAKE_OPTIONS', '-DCMAKE_BUILD_TYPE=Debug')
  199. .with_exec(["./netdata-installer.sh"] + args)
  200. )
  201. # The installer will place everything under "<install-prefix>/netdata"
  202. if self.prefix != "/":
  203. self.prefix = self.prefix / "netdata"
  204. return ctr
  205. class ChildStreamConf:
  206. def __init__(self, installer: NetdataInstaller, destination: str, api_key: uuid.UUID):
  207. self.installer = installer
  208. self.substitutions = {
  209. "enabled": "yes",
  210. "destination": destination,
  211. "api_key": api_key,
  212. "timeout_seconds": 60,
  213. "default_port": 19999,
  214. "send_charts_matching": "*",
  215. "buffer_size_bytes": 1024 * 1024,
  216. "reconnect_delay_seconds": 5,
  217. "initial_clock_resync_iterations": 60,
  218. }
  219. def render(self) -> str:
  220. tmpl_path = pathlib.Path(__file__).parent / "child_stream.conf"
  221. with open(tmpl_path) as fp:
  222. tmpl = jinja2.Template(fp.read())
  223. return tmpl.render(**self.substitutions)
  224. class ParentStreamConf:
  225. def __init__(self, installer: NetdataInstaller, api_key: str):
  226. self.installer = installer
  227. self.substitutions = {
  228. "api_key": api_key,
  229. "enabled": "yes",
  230. "allow_from": "*",
  231. "default_history": 3600,
  232. "health_enabled_by_default": "auto",
  233. "default_postpone_alarms_on_connect_seconds": 60,
  234. "multiple_connections": "allow",
  235. }
  236. def render(self) -> str:
  237. tmpl_path = pathlib.Path(__file__).parent / "parent_stream.conf"
  238. with open(tmpl_path) as fp:
  239. tmpl = jinja2.Template(fp.read())
  240. return tmpl.render(**self.substitutions)
  241. class StreamConf:
  242. def __init__(self, child_conf: ChildStreamConf, parent_conf: ParentStreamConf):
  243. self.child_conf = child_conf
  244. self.parent_conf = parent_conf
  245. def render(self) -> str:
  246. child_section = self.child_conf.render() if self.child_conf else ''
  247. parent_section = self.parent_conf.render() if self.parent_conf else ''
  248. return '\n'.join([child_section, parent_section])
  249. class Agent:
  250. def __init__(self, installer: NetdataInstaller):
  251. self.identifier = uuid.uuid4()
  252. self.installer = installer
  253. def _binary(self) -> pathlib.Path:
  254. return os.path.join(self.installer.prefix, "usr/sbin/netdata")
  255. def buildinfo(self, ctr: dagger.Container, installer: NetdataInstaller, output: pathlib.Path) -> dagger.Container:
  256. ctr = (
  257. ctr.with_exec([self._binary(), "-W", "buildinfo"], redirect_stdout=output)
  258. )
  259. return ctr
  260. def unittest(self, ctr: dagger.Container) -> dagger.Container:
  261. ctr = (
  262. ctr.with_exec([self._binary(), "-W", "unittest"])
  263. )
  264. return ctr
  265. def run(self, client: dagger.Client, ctr: dagger.Container, stream_conf: StreamConf, port, parent) -> dagger.Container:
  266. # Write stream.conf
  267. if stream_conf:
  268. host_stream_conf_path = str(self.identifier) + ".stream.conf"
  269. with open(host_stream_conf_path, 'w') as fp:
  270. fp.write(stream_conf.render())
  271. dest = self.installer.prefix / "etc/netdata/stream.conf"
  272. ctr = (
  273. ctr.with_file(dest.as_posix(), client.host().file(host_stream_conf_path))
  274. )
  275. if parent:
  276. ctr = ctr.with_service_binding("tilestora", parent)
  277. # Exec the binary
  278. ctr = (
  279. ctr.with_exposed_port(port)
  280. .with_exec([self._binary(), "-D", "-i", "0.0.0.0", "-p", str(port)])
  281. )
  282. return ctr
  283. class Digraph:
  284. def __init__(self):
  285. self.nodes = {} # Stores Agent instances
  286. self.children_of = {} # Stores children: {parent_id: [child_ids]}
  287. self.parents_of = {} # Stores parents: {child_id: [parent_ids]}
  288. def add_node(self, node):
  289. self.nodes[node.identifier] = node
  290. if node.identifier not in self.children_of:
  291. self.children_of[node.identifier] = []
  292. if node.identifier not in self.parents_of:
  293. self.parents_of[node.identifier] = []
  294. def add_children(self, node, children):
  295. if node.identifier not in self.nodes :
  296. raise ValueError("Node not found")
  297. for child in children:
  298. if child.identifier not in self.nodes :
  299. raise ValueError("Child node not found")
  300. if node.identifier not in self.children_of[child.identifier]:
  301. self.children_of[node.identifier].append(child.identifier)
  302. if child.identifier not in self.parents_of[node.identifier]:
  303. self.parents_of[child.identifier].append(node.identifier)
  304. def get_children(self, node):
  305. return [self.nodes [child_id] for child_id in self.children_of.get(node.identifier, [])]
  306. def get_parents(self, node):
  307. return [self.nodes [parent_id] for parent_id in self.parents_of.get(node.identifier, [])]
  308. def get_siblings(self, node):
  309. siblings = set()
  310. for parent_id in self.parents_of.get(node.identifier, []):
  311. siblings.update(self.children_of.get(parent_id, []))
  312. siblings.discard(node.identifier)
  313. return [self.nodes [sibling_id] for sibling_id in siblings]
  314. def render(self, filename="digraph"):
  315. import graphviz
  316. dot = graphviz.Digraph(comment='Agent Topology')
  317. for identifier, node in self.nodes.items():
  318. dot.node(str(identifier), label=str(identifier))
  319. for parent_id, children_ids in self.children_of.items():
  320. for child_id in children_ids:
  321. dot.edge(str(parent_id), str(child_id))
  322. dot.render(filename, format='svg', cleanup=True)
  323. class Context:
  324. def __init__(self,
  325. client: dagger.Client,
  326. platform: dagger.Platform,
  327. distro: Distribution,
  328. installer: NetdataInstaller,
  329. agent: Agent):
  330. self.client = client
  331. self.platform = platform
  332. self.distro = distro
  333. self.installer = installer
  334. self.agent = agent
  335. self.built_distro = False
  336. self.built_agent = False
  337. def build_distro(self) -> dagger.Container:
  338. ctr = self.distro.build(self.client, self.platform)
  339. self.built_distro = True
  340. return ctr
  341. def build_agent(self, ctr: dagger.Container) -> dagger.Container:
  342. if not self.built_distro:
  343. ctr = self.build_distro()
  344. ctr = self.installer.install(self.client, ctr)
  345. self.built_agent = True
  346. return ctr
  347. def buildinfo(self, ctr: dagger.Container, output: pathlib.Path) -> dagger.Container:
  348. if self.built_agent == False:
  349. self.build_agent(ctr)
  350. ctr = self.agent.buildinfo(ctr, self.installer, output)
  351. return ctr
  352. def exec(self, ctr: dagger.Container) -> dagger.Container:
  353. if self.built_agent == False:
  354. self.build_agent(ctr)
  355. ctr = self.agent.run(ctr)
  356. return ctr
  357. def run_async(func):
  358. def wrapper(*args, **kwargs):
  359. return asyncio.run(func(*args, **kwargs))
  360. return wrapper
  361. @run_async
  362. async def main():
  363. config = dagger.Config(log_output=sys.stdout)
  364. async with dagger.Connection(config) as client:
  365. platform = dagger.Platform("linux/x86_64")
  366. distro = Distribution("debian10")
  367. repo_root = pathlib.Path("/netdata")
  368. prefix_path = pathlib.Path("/opt")
  369. installer = NetdataInstaller(platform, distro, repo_root, prefix_path, FeatureFlags.DBEngine)
  370. parent_agent = Agent(installer)
  371. child_agent = Agent(installer)
  372. ctx = Context(client, platform, distro, installer, parent_agent)
  373. # build base image with packages we need
  374. ctr = ctx.build_distro()
  375. # build agent from source
  376. ctr = ctx.build_agent(ctr)
  377. # get the buildinfo
  378. # output = os.path.join(installer.prefix, "buildinfo.log")
  379. # ctr = ctx.buildinfo(ctr, output)
  380. api_key = uuid.uuid4()
  381. def setup_parent():
  382. child_stream_conf = None
  383. parent_stream_conf = ParentStreamConf(installer, api_key)
  384. stream_conf = StreamConf(child_stream_conf, parent_stream_conf)
  385. return stream_conf
  386. parent_stream_conf = setup_parent()
  387. parent = parent_agent.run(client, ctr, parent_stream_conf, 19999, None)
  388. parent_service = parent.as_service()
  389. def setup_child():
  390. child_stream_conf = ChildStreamConf(installer, "tilestora:19999", api_key)
  391. parent_stream_conf = None
  392. stream_conf = StreamConf(child_stream_conf, parent_stream_conf)
  393. return stream_conf
  394. child_stream_conf = setup_child()
  395. child = child_agent.run(client, ctr, child_stream_conf, 20000, parent_service)
  396. tunnel = await client.host().tunnel(parent_service, native=True).start()
  397. endpoint = await tunnel.endpoint()
  398. await child
  399. # await child.with_service_binding("tilestora", parent_service)
  400. # await child.with_service_binding("tilestora", parent_service)
  401. # tunnel = await client.host().tunnel(parent_service, native=True).start()
  402. # endpoint = await tunnel.endpoint()
  403. # tunnel = await client.host().tunnel(child_service, native=True).start()
  404. # endpoint = await tunnel.endpoint()
  405. time.sleep(600)
  406. # run unittests
  407. # ctr = agent.unittest(ctr)
  408. # await ctr
  409. if __name__ == '__main__':
  410. # agent1 = Agent("Data1")
  411. # agent2 = Agent("Data2")
  412. # agent3 = Agent("Data3")
  413. # agent4 = Agent("Data4")
  414. # dg = Digraph()
  415. # dg.add_node(agent1)
  416. # dg.add_node(agent2)
  417. # dg.add_node(agent3)
  418. # dg.add_node(agent4)
  419. # dg.add_children(agent1, [agent2, agent3])
  420. # dg.add_children(agent4, [agent2, agent3])
  421. # dg.render()
  422. main()