nullforge 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. nullforge/__init__.py +1 -0
  2. nullforge/foundry/README.md +0 -0
  3. nullforge/foundry/__init__.py +7 -0
  4. nullforge/foundry/full_cast.py +22 -0
  5. nullforge/inventories/README.md +0 -0
  6. nullforge/inventories/example.py +87 -0
  7. nullforge/models/__init__.py +1 -0
  8. nullforge/models/containers.py +60 -0
  9. nullforge/models/dns.py +222 -0
  10. nullforge/models/monitoring/__init__.py +29 -0
  11. nullforge/models/monitoring/base.py +19 -0
  12. nullforge/models/monitoring/nezha.py +13 -0
  13. nullforge/models/netsec.py +43 -0
  14. nullforge/models/profiles.py +33 -0
  15. nullforge/models/system.py +15 -0
  16. nullforge/models/users.py +9 -0
  17. nullforge/models/warp.py +65 -0
  18. nullforge/models/zerotrust.py +9 -0
  19. nullforge/molds/__init__.py +38 -0
  20. nullforge/molds/base_mold.py +59 -0
  21. nullforge/molds/containers.py +26 -0
  22. nullforge/molds/defaults.py +10 -0
  23. nullforge/molds/dns.py +70 -0
  24. nullforge/molds/features.py +75 -0
  25. nullforge/molds/haproxy.py +23 -0
  26. nullforge/molds/monitoring/__init__.py +45 -0
  27. nullforge/molds/monitoring/nezha.py +118 -0
  28. nullforge/molds/netsec.py +237 -0
  29. nullforge/molds/profiles.py +30 -0
  30. nullforge/molds/system.py +140 -0
  31. nullforge/molds/telemt.py +140 -0
  32. nullforge/molds/tor.py +28 -0
  33. nullforge/molds/user.py +77 -0
  34. nullforge/molds/utils.py +115 -0
  35. nullforge/molds/warp.py +54 -0
  36. nullforge/molds/xray.py +16 -0
  37. nullforge/molds/zerotrust.py +59 -0
  38. nullforge/runes/__init__.py +46 -0
  39. nullforge/runes/base.py +327 -0
  40. nullforge/runes/containers.py +212 -0
  41. nullforge/runes/dns.py +256 -0
  42. nullforge/runes/haproxy.py +135 -0
  43. nullforge/runes/monitoring.py +21 -0
  44. nullforge/runes/netsec.py +547 -0
  45. nullforge/runes/prepare.py +30 -0
  46. nullforge/runes/profiles.py +583 -0
  47. nullforge/runes/telemt.py +252 -0
  48. nullforge/runes/tor.py +57 -0
  49. nullforge/runes/users.py +159 -0
  50. nullforge/runes/warp.py +273 -0
  51. nullforge/runes/xray.py +81 -0
  52. nullforge/runes/zerotrust.py +91 -0
  53. nullforge/smithy/__init__.py +1 -0
  54. nullforge/smithy/admin.py +40 -0
  55. nullforge/smithy/arch.py +36 -0
  56. nullforge/smithy/blocky.py +37 -0
  57. nullforge/smithy/cloudflare.py +32 -0
  58. nullforge/smithy/github.py +256 -0
  59. nullforge/smithy/http.py +85 -0
  60. nullforge/smithy/install.py +149 -0
  61. nullforge/smithy/monitoring/__init__.py +1 -0
  62. nullforge/smithy/monitoring/nezha/__init__.py +0 -0
  63. nullforge/smithy/monitoring/nezha/agent.py +54 -0
  64. nullforge/smithy/monitoring/nezha/dashboard.py +137 -0
  65. nullforge/smithy/monitoring/nezha/deploy.py +81 -0
  66. nullforge/smithy/network.py +95 -0
  67. nullforge/smithy/packages.py +176 -0
  68. nullforge/smithy/service.py +37 -0
  69. nullforge/smithy/sni.py +358 -0
  70. nullforge/smithy/swap.py +132 -0
  71. nullforge/smithy/system.py +86 -0
  72. nullforge/smithy/versions.py +282 -0
  73. nullforge/templates/__init__.py +32 -0
  74. nullforge/templates/cloudflared/tunnel.yml.j2 +6 -0
  75. nullforge/templates/dns/blocky.yaml.j2 +11 -0
  76. nullforge/templates/dns/dns.yaml.j2 +6 -0
  77. nullforge/templates/dns/resolv.conf.j2 +1 -0
  78. nullforge/templates/dns/resolved.conf.j2 +13 -0
  79. nullforge/templates/etc/default/zramswap.j2 +13 -0
  80. nullforge/templates/nvim/nvim_patch.lua.j2 +5 -0
  81. nullforge/templates/profiles/direnv.toml +2 -0
  82. nullforge/templates/profiles/starship.toml +186 -0
  83. nullforge/templates/profiles/tmux.conf +32 -0
  84. nullforge/templates/profiles/zshrc.j2 +46 -0
  85. nullforge/templates/scripts/telemt-synfix.sh +89 -0
  86. nullforge/templates/scripts/teleproxy-warp.sh +127 -0
  87. nullforge/templates/scripts/warp-v6-policy.sh +118 -0
  88. nullforge/templates/scripts/zt-tunnel-warp.sh +28 -0
  89. nullforge/templates/systemd/blocky.service.j2 +19 -0
  90. nullforge/templates/systemd/cloudflare-tunnel.service.j2 +27 -0
  91. nullforge/templates/systemd/cloudflare-warp.service.j2 +21 -0
  92. nullforge/templates/systemd/dns-internal.service.j2 +15 -0
  93. nullforge/templates/systemd/telemt.service.j2 +36 -0
  94. nullforge/templates/telemt/telemt.toml.j2 +49 -0
  95. nullforge/templates/tor/torrc.j2 +5 -0
  96. nullforge-0.1.0.dist-info/METADATA +39 -0
  97. nullforge-0.1.0.dist-info/RECORD +99 -0
  98. nullforge-0.1.0.dist-info/WHEEL +4 -0
  99. nullforge-0.1.0.dist-info/licenses/LICENSE +7 -0
@@ -0,0 +1,212 @@
1
+ """Containers deployment module."""
2
+
3
+ from pyinfra.context import host
4
+ from pyinfra.facts.files import File
5
+ from pyinfra.facts.server import Arch, Which
6
+ from pyinfra.operations import apt, files, server, systemd
7
+
8
+ from nullforge.models.containers import ContainersBackendType
9
+ from nullforge.molds import ContainersMold, FeaturesMold, UserMold
10
+ from nullforge.smithy.arch import deb_arch
11
+ from nullforge.smithy.http import CURL_ARGS
12
+ from nullforge.smithy.packages import get_pm
13
+ from nullforge.smithy.versions import GPG_KEYS, STATIC_URLS
14
+
15
+
16
+ def deploy_containers() -> None:
17
+ """Deploy containers runtime and related tools."""
18
+
19
+ features: FeaturesMold = host.data.features
20
+ containers_opts: ContainersMold = features.containers
21
+ user_opts: UserMold = features.users
22
+
23
+ match containers_opts.backend_type:
24
+ case ContainersBackendType.DOCKER:
25
+ _install_docker()
26
+ if user_opts.manage:
27
+ _add_user_to_docker_group(user_opts.name)
28
+ _install_gvisor()
29
+ case ContainersBackendType.PODMAN:
30
+ _install_crun()
31
+ _install_podman()
32
+ case ContainersBackendType.CRIO:
33
+ raise ValueError("CRIO is not supported yet")
34
+
35
+ if containers_opts.skopeo:
36
+ _install_skopeo()
37
+
38
+
39
+ def _install_gvisor() -> None:
40
+ """Install gVisor runtime."""
41
+
42
+ pm = get_pm()
43
+ if pm.is_debian_based:
44
+ _install_gvisor_debian()
45
+ elif pm.is_rhel_based:
46
+ _install_gvisor_rhel()
47
+ else:
48
+ host.noop(f"gVisor install skipped: unsupported distro {pm.distro_name}")
49
+
50
+
51
+ def _install_gvisor_debian() -> None:
52
+ """Install gVisor on Debian/Ubuntu via the official APT repo."""
53
+
54
+ gvisor_key_path = "/usr/share/keyrings/gvisor-archive-keyring.gpg"
55
+ apt.key(
56
+ name="Install gVisor GPG key",
57
+ src=GPG_KEYS["gvisor"],
58
+ dest=gvisor_key_path,
59
+ _sudo=True,
60
+ _retries=3,
61
+ _retry_delay=10,
62
+ )
63
+
64
+ apt.sources_file(
65
+ name="Write gVisor repository source",
66
+ filename="gvisor",
67
+ uris="https://storage.googleapis.com/gvisor/releases",
68
+ suites="release",
69
+ components="main",
70
+ architectures=deb_arch(),
71
+ signed_by=gvisor_key_path,
72
+ _sudo=True,
73
+ )
74
+
75
+ pm = get_pm()
76
+ pm.update(
77
+ name="Update package repositories after adding gVisor repository",
78
+ _sudo=True,
79
+ )
80
+
81
+ pm.install(
82
+ name="Install gVisor",
83
+ packages=["runsc"],
84
+ _sudo=True,
85
+ )
86
+
87
+
88
+ def _install_gvisor_rhel() -> None:
89
+ """Install gVisor binaries manually and register as Docker's default runtime."""
90
+
91
+ runsc_path = "/usr/local/bin/runsc"
92
+ if host.get_fact(File, runsc_path):
93
+ host.noop("gVisor already installed")
94
+ return
95
+
96
+ arch = host.get_fact(Arch)
97
+ base_url = f"https://storage.googleapis.com/gvisor/releases/release/latest/{arch}"
98
+
99
+ files.download(
100
+ name="Download gVisor runsc binary",
101
+ src=f"{base_url}/runsc",
102
+ dest=runsc_path,
103
+ mode="0755",
104
+ extra_curl_args=CURL_ARGS,
105
+ _sudo=True,
106
+ _retries=3,
107
+ _retry_delay=10,
108
+ )
109
+
110
+ files.download(
111
+ name="Download gVisor containerd-shim",
112
+ src=f"{base_url}/containerd-shim-runsc-v1",
113
+ dest="/usr/local/bin/containerd-shim-runsc-v1",
114
+ mode="0755",
115
+ extra_curl_args=CURL_ARGS,
116
+ _sudo=True,
117
+ _retries=3,
118
+ _retry_delay=10,
119
+ )
120
+
121
+ register_runtime = server.shell(
122
+ name="Register gVisor as Docker's default runtime",
123
+ commands=[
124
+ # `runsc install` creates/updates /etc/docker/daemon.json with the runsc runtime entry.
125
+ f"{runsc_path} install",
126
+ # Promote runsc to default-runtime so containers use it without `--runtime=runsc`.
127
+ 'jq \'. + {"default-runtime": "runsc"}\' /etc/docker/daemon.json'
128
+ " > /etc/docker/daemon.json.tmp && mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json",
129
+ ],
130
+ _sudo=True,
131
+ )
132
+
133
+ systemd.service(
134
+ name="Reload Docker to pick up gVisor runtime",
135
+ service="docker",
136
+ reloaded=True,
137
+ _sudo=True,
138
+ _if=register_runtime.did_change,
139
+ )
140
+
141
+
142
+ def _install_docker() -> None:
143
+ """Install Docker using official installation script."""
144
+
145
+ if host.get_fact(Which, command="docker"):
146
+ return
147
+
148
+ get_docker_path = "/tmp/get-docker.sh"
149
+ files.download(
150
+ name="Download Docker installation script",
151
+ src=STATIC_URLS["docker_install"],
152
+ dest=get_docker_path,
153
+ mode="0755",
154
+ extra_curl_args=CURL_ARGS,
155
+ _retries=3,
156
+ _retry_delay=10,
157
+ )
158
+
159
+ server.shell(
160
+ name="Install Docker",
161
+ commands=[f"bash {get_docker_path}"],
162
+ _sudo=True,
163
+ )
164
+
165
+
166
+ def _add_user_to_docker_group(username: str) -> None:
167
+ """Add user to docker group."""
168
+
169
+ server.user(
170
+ name=f"Add user {username} to docker group",
171
+ user=username,
172
+ groups=["docker"],
173
+ append=True,
174
+ _sudo=True,
175
+ )
176
+
177
+
178
+ def _install_skopeo() -> None:
179
+ """Install skopeo."""
180
+
181
+ pm = get_pm()
182
+ pm.install(
183
+ name="Install skopeo",
184
+ packages=["skopeo"],
185
+ _sudo=True,
186
+ )
187
+
188
+
189
+ def _install_podman() -> None:
190
+ """Install Podman."""
191
+
192
+ pm = get_pm()
193
+ pm.install(
194
+ name="Install Podman",
195
+ packages=[
196
+ "podman",
197
+ "podman-compose",
198
+ ],
199
+ _sudo=True,
200
+ )
201
+
202
+
203
+ def _install_crun() -> None:
204
+ pm = get_pm()
205
+ pm.install(
206
+ name="Install crun",
207
+ packages=["crun"],
208
+ _sudo=True,
209
+ )
210
+
211
+
212
+ deploy_containers()
nullforge/runes/dns.py ADDED
@@ -0,0 +1,256 @@
1
+ """DNS configuration deployment module."""
2
+
3
+ from pyinfra.context import host
4
+ from pyinfra.facts.files import Link
5
+ from pyinfra.operations import files, server, systemd
6
+ from pyinfra.operations.util import any_changed
7
+
8
+ from nullforge.models.dns import (
9
+ DnsMode,
10
+ DnsProtocol,
11
+ DnsServer,
12
+ DnsServerDoH,
13
+ DnsServerDoT,
14
+ DnsServerDoU,
15
+ dns_providers,
16
+ )
17
+ from nullforge.molds import DnsMold, FeaturesMold
18
+ from nullforge.smithy.blocky import BLOCKY_GROUP, BLOCKY_USER, ensure_blocky_binary
19
+ from nullforge.smithy.network import has_ipv6
20
+ from nullforge.smithy.service import ensure_service_user
21
+ from nullforge.templates import BLOCK_TRIM_ENV, get_template_path
22
+
23
+
24
+ def deploy_dns_configuration() -> None:
25
+ """Deploy DNS configuration based on selected mode."""
26
+
27
+ ipv6_enabled = has_ipv6()
28
+ features: FeaturesMold = host.data.features
29
+ dns_opts: DnsMold = features.dns
30
+
31
+ upstream_protocol = (
32
+ DnsProtocol.DOH
33
+ if dns_opts.mode == DnsMode.BLOCKY
34
+ else DnsProtocol.DOT
35
+ if dns_opts.mode == DnsMode.DOT_RESOLVED
36
+ else None
37
+ )
38
+
39
+ upstreams: list[DnsServer] = (
40
+ dns_providers.get_upstreams(
41
+ dns_opts.upstream_provider,
42
+ upstream_protocol,
43
+ ipv6_enabled,
44
+ dns_opts.ecs,
45
+ )
46
+ if upstream_protocol
47
+ else []
48
+ )
49
+
50
+ match dns_opts.mode:
51
+ case DnsMode.DOU:
52
+ raise ValueError("DNS over UDP is not supported yet.")
53
+ case DnsMode.DOT_RESOLVED:
54
+ _deploy_dot_resolved(dns_opts, upstreams)
55
+ case DnsMode.BLOCKY:
56
+ _deploy_blocky_configuration(dns_opts, upstreams)
57
+ case DnsMode.NONE:
58
+ return
59
+ case _:
60
+ raise NotImplementedError(f"Unsupported DNS mode: {dns_opts.mode}")
61
+
62
+
63
+ def _format_resolved_upstreams(upstreams: list[DnsServer]) -> list[str]:
64
+ """Render DoT upstreams in `address[:port]#SNI` form for systemd-resolved."""
65
+
66
+ servers: list[str] = []
67
+ for srv in [u for u in upstreams if isinstance(u, DnsServerDoT)]:
68
+ addr = str(srv.host)
69
+ endpoint = f"[{addr}]:{srv.port}" if ":" in addr else f"{addr}:{srv.port}"
70
+ servers.append(f"{endpoint}#{srv.sni}" if srv.sni else endpoint)
71
+ return servers
72
+
73
+
74
+ def _deploy_dot_resolved(opts: DnsMold, upstreams: list[DnsServer]) -> None:
75
+ """Deploy DNS over TLS configuration."""
76
+
77
+ systemd.service(
78
+ name="Stop and disable blocky DNS proxy",
79
+ service="blocky",
80
+ running=False,
81
+ enabled=False,
82
+ _sudo=True,
83
+ _ignore_errors=True,
84
+ )
85
+
86
+ systemd.service(
87
+ name="Stop and disable dns-internal interface",
88
+ service="dns-internal",
89
+ running=False,
90
+ enabled=False,
91
+ _sudo=True,
92
+ _ignore_errors=True,
93
+ )
94
+
95
+ resolved_conf = files.template(
96
+ name="Configure systemd-resolved for DoT",
97
+ src=get_template_path("dns/resolved.conf.j2"),
98
+ dest="/etc/systemd/resolved.conf",
99
+ mode="0644",
100
+ DOT=True,
101
+ DOH=False,
102
+ DNS_SERVERS=_format_resolved_upstreams(upstreams),
103
+ _sudo=True,
104
+ )
105
+
106
+ stub_resolv_conf = "/run/systemd/resolve/stub-resolv.conf"
107
+ resolv_conf = "/etc/resolv.conf"
108
+ resolved_link = files.link(
109
+ name="Create symlink to resolv.conf for DoT with systemd-resolved",
110
+ path=resolv_conf,
111
+ target=stub_resolv_conf,
112
+ force=True,
113
+ _sudo=True,
114
+ )
115
+
116
+ systemd.service(
117
+ name="Restart systemd-resolved on change",
118
+ service="systemd-resolved",
119
+ restarted=True,
120
+ _sudo=True,
121
+ _if=any_changed(resolved_conf, resolved_link),
122
+ )
123
+
124
+ systemd.service(
125
+ name="Ensure systemd-resolved is running and enabled",
126
+ service="systemd-resolved",
127
+ running=True,
128
+ enabled=True,
129
+ _sudo=True,
130
+ )
131
+
132
+ server.shell(
133
+ name="Flush DNS cache",
134
+ commands=[
135
+ "resolvectl flush-caches",
136
+ ],
137
+ _sudo=True,
138
+ _if=any_changed(resolved_conf, resolved_link),
139
+ )
140
+
141
+
142
+ def _format_blocky_upstreams(upstreams: list[DnsServer]) -> list[str]:
143
+ result = []
144
+ for srv in upstreams:
145
+ if isinstance(srv, DnsServerDoH):
146
+ result.append(srv.url)
147
+ elif isinstance(srv, DnsServerDoT):
148
+ addr = f"[{srv.host}]" if ":" in str(srv.host) else str(srv.host)
149
+ result.append(f"tcp-tls:{addr}:{srv.port}")
150
+ elif isinstance(srv, DnsServerDoU):
151
+ addr = f"[{srv.host}]" if ":" in str(srv.host) else str(srv.host)
152
+ result.append(f"udp://{addr}:{srv.port}")
153
+ return result
154
+
155
+
156
+ def _deploy_blocky_configuration(opts: DnsMold, upstreams: list[DnsServer]) -> None:
157
+ """Deploy blocky DNS proxy on the internal link-local interface."""
158
+
159
+ ensure_service_user(BLOCKY_USER, BLOCKY_GROUP, "/etc/blocky")
160
+ binary_op = ensure_blocky_binary()
161
+ listen = opts.listen_address
162
+
163
+ if opts.needs_custom_interface:
164
+ iface_service_path = "/etc/systemd/system/dns-internal.service"
165
+ iface_service = files.template(
166
+ name="Configure dns-internal dummy interface service",
167
+ src=get_template_path("systemd/dns-internal.service.j2"),
168
+ dest=iface_service_path,
169
+ mode="0644",
170
+ LISTEN_ADDRESS=listen,
171
+ LISTEN_CIDR=f"{listen}/{listen.max_prefixlen}",
172
+ _sudo=True,
173
+ )
174
+
175
+ systemd.daemon_reload(
176
+ name="Reload systemd daemon for dns-internal",
177
+ _sudo=True,
178
+ _if=iface_service.did_change,
179
+ )
180
+
181
+ systemd.service(
182
+ name="Ensure dns-internal interface is running and enabled",
183
+ service="dns-internal",
184
+ running=True,
185
+ enabled=True,
186
+ _sudo=True,
187
+ )
188
+
189
+ # Deploy blocky config
190
+ config_dir = "/etc/blocky"
191
+ config_path = f"{config_dir}/config.yaml"
192
+ config_template = files.template(
193
+ name="Configure blocky DNS YAML config",
194
+ src=get_template_path("dns/blocky.yaml.j2"),
195
+ dest=config_path,
196
+ mode="0644",
197
+ user=BLOCKY_USER,
198
+ group=BLOCKY_GROUP,
199
+ jinja_env_kwargs=BLOCK_TRIM_ENV,
200
+ UPSTREAMS=_format_blocky_upstreams(upstreams),
201
+ LISTEN_ENDPOINT=f"[{listen}]:53" if listen.version == 6 else f"{listen}:53",
202
+ _sudo=True,
203
+ )
204
+
205
+ service_path = "/etc/systemd/system/blocky.service"
206
+ service_template = files.template(
207
+ name="Configure blocky DNS proxy service",
208
+ src=get_template_path("systemd/blocky.service.j2"),
209
+ dest=service_path,
210
+ mode="0644",
211
+ CONFIG_PATH=config_path,
212
+ _sudo=True,
213
+ )
214
+
215
+ systemd.daemon_reload(
216
+ name="Reload systemd daemon for blocky",
217
+ _sudo=True,
218
+ _if=service_template.did_change,
219
+ )
220
+
221
+ restart_ops = [op for op in (binary_op, config_template, service_template) if op]
222
+ systemd.service(
223
+ name="Restart blocky on change",
224
+ service="blocky",
225
+ restarted=True,
226
+ _sudo=True,
227
+ _if=any_changed(*restart_ops),
228
+ )
229
+
230
+ systemd.service(
231
+ name="Ensure blocky is running and enabled",
232
+ service="blocky",
233
+ running=True,
234
+ enabled=True,
235
+ _sudo=True,
236
+ )
237
+
238
+ if host.get_fact(Link, path="/etc/resolv.conf"):
239
+ files.link(
240
+ name="Remove /etc/resolv.conf symlink",
241
+ path="/etc/resolv.conf",
242
+ present=False,
243
+ _sudo=True,
244
+ )
245
+
246
+ files.template(
247
+ name="Configure resolv.conf for blocky",
248
+ src=get_template_path("dns/resolv.conf.j2"),
249
+ dest="/etc/resolv.conf",
250
+ mode="0644",
251
+ LISTEN_ADDRESS=opts.listen_address,
252
+ _sudo=True,
253
+ )
254
+
255
+
256
+ deploy_dns_configuration()
@@ -0,0 +1,135 @@
1
+ """HAProxy deployment module."""
2
+
3
+ from pyinfra import logger
4
+ from pyinfra.context import host
5
+ from pyinfra.facts.files import Directory
6
+ from pyinfra.operations import apt, files
7
+
8
+ from nullforge.molds import FeaturesMold, HaproxyMold
9
+ from nullforge.smithy.admin import ensure_acl_access
10
+ from nullforge.smithy.packages import get_pm
11
+ from nullforge.smithy.versions import GPG_KEYS, KEYRING_DIR
12
+
13
+
14
+ def deploy_haproxy() -> None:
15
+ """Deploy HAProxy proxy server."""
16
+
17
+ features: FeaturesMold = host.data.features
18
+ haproxy_opts: HaproxyMold = features.haproxy
19
+
20
+ _install_haproxy(haproxy_opts)
21
+ _ensure_pki_dir()
22
+ if features.users.manage:
23
+ _setup_acls(features.users.name)
24
+
25
+
26
+ def _install_haproxy(opts: HaproxyMold) -> None:
27
+ """Install HAProxy proxy server."""
28
+
29
+ haproxy_version = opts.version
30
+
31
+ pm = get_pm()
32
+
33
+ if pm.is_debian_based:
34
+ if pm.is_ubuntu:
35
+ pm.install(
36
+ name="Install software-properties-common",
37
+ packages=["software-properties-common"],
38
+ no_recommends=True,
39
+ _sudo=True,
40
+ )
41
+
42
+ apt.ppa(
43
+ name="Add HAProxy PPA",
44
+ src=f"ppa:vbernat/haproxy-{haproxy_version}",
45
+ _sudo=True,
46
+ )
47
+
48
+ pm.update(
49
+ name="Update package repositories after adding HAProxy PPA",
50
+ _sudo=True,
51
+ )
52
+
53
+ pm.install(
54
+ name="Install HAProxy",
55
+ packages=[f"haproxy={haproxy_version}.*"],
56
+ _sudo=True,
57
+ )
58
+
59
+ else:
60
+ # Debian 13 = trixie, Debian 12 = bookworm
61
+ if pm.distro_major == 13:
62
+ repo_version = f"trixie-backports-{haproxy_version}"
63
+ elif pm.distro_major == 12:
64
+ repo_version = f"bookworm-backports-{haproxy_version}"
65
+ else:
66
+ raise ValueError("Unsupported distribution version")
67
+
68
+ gpg_key_path = f"{KEYRING_DIR}/haproxy-archive-keyring.gpg"
69
+ apt.key(
70
+ name="Install HAProxy GPG key",
71
+ src=GPG_KEYS["haproxy"],
72
+ dest=gpg_key_path,
73
+ _sudo=True,
74
+ _retries=3,
75
+ _retry_delay=10,
76
+ )
77
+
78
+ apt.sources_file(
79
+ name="Write HAProxy repository source",
80
+ filename="haproxy",
81
+ uris="https://haproxy.debian.net",
82
+ suites=repo_version,
83
+ components="main",
84
+ signed_by=gpg_key_path,
85
+ _sudo=True,
86
+ )
87
+
88
+ pm.update(
89
+ name="Update package repositories after adding HAProxy repository",
90
+ _sudo=True,
91
+ )
92
+
93
+ pm.install(
94
+ name="Install HAProxy",
95
+ packages=[f"haproxy={haproxy_version}.*"],
96
+ _sudo=True,
97
+ )
98
+
99
+ elif pm.is_rhel_based:
100
+ logger.warning(
101
+ f"HAProxy version pin '{haproxy_version}' is Debian/Ubuntu-only; "
102
+ f"installing version shipped within {pm.distro_name}"
103
+ )
104
+ pm.install(
105
+ name="Install HAProxy",
106
+ packages=["haproxy"],
107
+ _sudo=True,
108
+ )
109
+
110
+
111
+ def _ensure_pki_dir() -> None:
112
+ """Ensure the HAProxy PKI directory exists for ACME certificate management."""
113
+
114
+ pki_dir = "/usr/local/etc/haproxy/pki"
115
+ if not host.get_fact(Directory, pki_dir):
116
+ files.directory(
117
+ name="Create HAProxy PKI directory",
118
+ path=pki_dir,
119
+ user="root",
120
+ group="root",
121
+ mode="0755",
122
+ _sudo=True,
123
+ )
124
+
125
+
126
+ def _setup_acls(username: str) -> None:
127
+ """Set up ACLs on HAProxy directories for ACME certificate management."""
128
+
129
+ haproxy_dir = "/etc/haproxy"
130
+ pki_dir = "/usr/local/etc/haproxy/pki"
131
+ ensure_acl_access(username, haproxy_dir, [f"{haproxy_dir}/haproxy.cfg"])
132
+ ensure_acl_access(username, pki_dir)
133
+
134
+
135
+ deploy_haproxy()
@@ -0,0 +1,21 @@
1
+ """Monitoring agent deployment module."""
2
+
3
+ from pyinfra.context import host
4
+
5
+ from nullforge.molds import FeaturesMold, MonitoringMold
6
+ from nullforge.molds.monitoring import NezhaBackend
7
+ from nullforge.smithy.monitoring.nezha.deploy import deploy_nezha
8
+
9
+
10
+ def deploy_monitoring() -> None:
11
+ """Deploy configured monitoring agent."""
12
+
13
+ features: FeaturesMold = host.data.features
14
+ monitoring: MonitoringMold = features.monitoring
15
+
16
+ match monitoring.backend:
17
+ case NezhaBackend():
18
+ deploy_nezha(monitoring.backend)
19
+
20
+
21
+ deploy_monitoring()