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,273 @@
1
+ """Cloudflare WARP deployment module."""
2
+
3
+ from pyinfra.api.operation import OperationMeta
4
+ from pyinfra.context import host
5
+ from pyinfra.facts.files import File
6
+ from pyinfra.operations import files, server, systemd
7
+ from pyinfra.operations.util import any_changed
8
+
9
+ from nullforge.models.warp import WarpEngineType
10
+ from nullforge.molds import FeaturesMold, WarpMold
11
+ from nullforge.smithy.cloudflare import CLOUDFLARE_GROUP, CLOUDFLARE_USER
12
+ from nullforge.smithy.install import install_release_binary
13
+ from nullforge.smithy.network import has_ipv6
14
+ from nullforge.smithy.packages import get_pm
15
+ from nullforge.smithy.service import ensure_service_user
16
+ from nullforge.smithy.versions import get_versions, is_pinned_version_installed, record_installed_version
17
+ from nullforge.templates import get_template_path
18
+
19
+
20
+ def deploy_warp() -> None:
21
+ """Deploy Cloudflare WARP configuration."""
22
+
23
+ features: FeaturesMold = host.data.features
24
+ warp_opts: WarpMold = features.warp
25
+
26
+ ensure_service_user(CLOUDFLARE_USER, CLOUDFLARE_GROUP, "/etc/cloudflare")
27
+
28
+ files.directory(
29
+ name="Ensure WARP engine configuration directory exists",
30
+ path=warp_opts.engine.config_dir,
31
+ user=CLOUDFLARE_USER,
32
+ group=CLOUDFLARE_GROUP,
33
+ mode="0755",
34
+ _sudo=True,
35
+ )
36
+
37
+ match warp_opts.engine_type:
38
+ case WarpEngineType.WIREGUARD:
39
+ _deploy_wireguard_warp(warp_opts)
40
+ case WarpEngineType.MASQUE:
41
+ _deploy_masque_warp(warp_opts)
42
+
43
+
44
+ def _install_wgcf(opts: WarpMold) -> None:
45
+ """Install wgcf binary."""
46
+
47
+ if is_pinned_version_installed("wgcf", opts.engine.binary_path):
48
+ return
49
+
50
+ install_release_binary(
51
+ name="Install wgcf binary",
52
+ url=get_versions().wgcf_url(),
53
+ dest=opts.engine.binary_path,
54
+ )
55
+
56
+ record_installed_version("wgcf")
57
+
58
+
59
+ def _deploy_wireguard_warp(opts: WarpMold) -> None:
60
+ """Deploy WARP using WireGuard."""
61
+
62
+ pm = get_pm()
63
+ pm.install(
64
+ name="Install WireGuard packages",
65
+ packages=["wireguard", "wireguard-tools"],
66
+ _sudo=True,
67
+ )
68
+
69
+ _install_wgcf(opts)
70
+
71
+ wgcf_account_path = opts.engine.account_path
72
+ wgcf_profile_path = opts.engine.profile_path
73
+ wgcf_bin = opts.engine.binary_path
74
+
75
+ register_op = None
76
+ if not host.get_fact(File, wgcf_account_path):
77
+ register_op = server.shell(
78
+ name="Register wgcf account",
79
+ commands=f"{wgcf_bin} register --accept-tos --config {wgcf_account_path}",
80
+ _retries=3,
81
+ _retry_delay=10,
82
+ _ignore_errors=True,
83
+ )
84
+
85
+ register_condition = register_op.did_succeed if register_op else (lambda: True)
86
+
87
+ generate_op = None
88
+ if not host.get_fact(File, wgcf_profile_path):
89
+ generate_op = server.shell(
90
+ name="Generate WireGuard profile",
91
+ commands=f"{wgcf_bin} generate --config {wgcf_account_path} --profile {wgcf_profile_path}",
92
+ _retries=3,
93
+ _retry_delay=10,
94
+ _ignore_errors=True,
95
+ _if=register_condition,
96
+ )
97
+
98
+ server.shell(
99
+ name="Post-process WireGuard profile",
100
+ commands=f"sed -i '/^DNS = /d' {wgcf_profile_path} "
101
+ rf"&& sed -i '/^\[Interface\]/a Table = off' {wgcf_profile_path}",
102
+ _if=generate_op.did_change,
103
+ )
104
+
105
+ def _wgcf_ok() -> bool:
106
+ """True iff every declared wgcf step really succeeded (or none were declared)."""
107
+
108
+ if register_op is not None and not register_op.did_succeed():
109
+ return False
110
+ if generate_op is not None and not generate_op.did_succeed():
111
+ return False
112
+ return True
113
+
114
+ profile_link = files.link(
115
+ name="Link WireGuard profile to /etc/wireguard/warp.conf",
116
+ path="/etc/wireguard/warp.conf",
117
+ target=wgcf_profile_path,
118
+ _sudo=True,
119
+ _if=_wgcf_ok,
120
+ )
121
+
122
+ restart_ops = [op for op in (register_op, generate_op, profile_link) if op]
123
+ systemd.service(
124
+ name="Restart WireGuard WARP on change",
125
+ service=opts.engine.systemd_service_name,
126
+ restarted=True,
127
+ _sudo=True,
128
+ _if=[_wgcf_ok, any_changed(*restart_ops)],
129
+ )
130
+
131
+ systemd.service(
132
+ name="Ensure WireGuard WARP is running and enabled",
133
+ service=opts.engine.systemd_service_name,
134
+ running=True,
135
+ enabled=True,
136
+ _sudo=True,
137
+ _if=_wgcf_ok,
138
+ )
139
+
140
+
141
+ def _install_usque(opts: WarpMold) -> OperationMeta | None:
142
+ """Install usque binary."""
143
+
144
+ if is_pinned_version_installed("usque", opts.engine.binary_path):
145
+ return None
146
+
147
+ install_op = install_release_binary(
148
+ name="Extract and install usque binary",
149
+ url=get_versions().usque_zip(),
150
+ dest=opts.engine.binary_path,
151
+ binary_name="usque",
152
+ )
153
+
154
+ files.file(
155
+ name="Set usque binary as executable",
156
+ path=opts.engine.binary_path,
157
+ mode="0755",
158
+ user="root",
159
+ group=CLOUDFLARE_GROUP,
160
+ _sudo=True,
161
+ )
162
+
163
+ return install_op
164
+
165
+
166
+ def _deploy_masque_warp(opts: WarpMold) -> None:
167
+ """Deploy WARP using Masque."""
168
+
169
+ binary_op = _install_usque(opts)
170
+
171
+ usque_config_path = opts.engine.config_path
172
+ usque_bin = opts.engine.binary_path
173
+ if not host.get_fact(File, usque_config_path):
174
+ server.shell(
175
+ name="Enroll device in Warp",
176
+ commands=f"{usque_bin} enroll -c {usque_config_path}",
177
+ _sudo=True,
178
+ )
179
+
180
+ register_op = server.shell(
181
+ name="Register device in Warp",
182
+ commands=f"{usque_bin} register -c {usque_config_path} --accept-tos",
183
+ _sudo=True,
184
+ _retries=3,
185
+ _retry_delay=10,
186
+ _ignore_errors=True,
187
+ )
188
+ config_condition = register_op.did_succeed
189
+ else:
190
+ config_condition = lambda: True # noqa: E731 # config already exists, ops run unconditionally
191
+
192
+ files.file(
193
+ name="Ensure WARP config file ownership to cloudflare",
194
+ path=usque_config_path,
195
+ user=CLOUDFLARE_USER,
196
+ group=CLOUDFLARE_GROUP,
197
+ mode="0640",
198
+ _sudo=True,
199
+ _if=config_condition,
200
+ )
201
+
202
+ rt_tables_dir = "/etc/iproute2"
203
+ rt_tables_path = f"{rt_tables_dir}/rt_tables"
204
+ files.directory(
205
+ name="Create /etc/iproute2 directory",
206
+ path=rt_tables_dir,
207
+ user="root",
208
+ group="root",
209
+ mode="0755",
210
+ _sudo=True,
211
+ _if=config_condition,
212
+ )
213
+
214
+ files.file(
215
+ name=f"Set {rt_tables_path} group to cloudflare",
216
+ path=rt_tables_path,
217
+ group=CLOUDFLARE_GROUP,
218
+ mode="0664",
219
+ _sudo=True,
220
+ _if=config_condition,
221
+ )
222
+
223
+ policy_script = files.put(
224
+ name="Deploy WARP v6 policy script",
225
+ src=get_template_path("scripts/warp-v6-policy.sh"),
226
+ dest=opts.engine.policy_script,
227
+ user=CLOUDFLARE_USER,
228
+ group=CLOUDFLARE_GROUP,
229
+ mode="0755",
230
+ _sudo=True,
231
+ _if=config_condition,
232
+ )
233
+
234
+ service_template = files.template(
235
+ name="Deploy Masque WARP service configuration",
236
+ src=get_template_path("systemd/cloudflare-warp.service.j2"),
237
+ dest=f"/etc/systemd/system/{opts.engine.systemd_service_name}.service",
238
+ mode="0644",
239
+ WORKDIR=opts.engine.config_dir,
240
+ CONFIG_PATH=opts.engine.config_path,
241
+ MTU=opts.mtu,
242
+ INET_NAME=opts.iface,
243
+ USE_OUTBOUND_IPV6=has_ipv6(exclude_iface=opts.iface),
244
+ _sudo=True,
245
+ _if=config_condition,
246
+ )
247
+
248
+ systemd.daemon_reload(
249
+ name="Reload systemd daemon for Masque WARP",
250
+ _sudo=True,
251
+ _if=any_changed(policy_script, service_template),
252
+ )
253
+
254
+ restart_ops = [op for op in (binary_op, policy_script, service_template) if op]
255
+ systemd.service(
256
+ name="Restart Masque WARP on change",
257
+ service=opts.engine.systemd_service_name,
258
+ restarted=True,
259
+ _sudo=True,
260
+ _if=any_changed(*restart_ops),
261
+ )
262
+
263
+ systemd.service(
264
+ name="Ensure Masque WARP is running and enabled",
265
+ service=opts.engine.systemd_service_name,
266
+ running=True,
267
+ enabled=True,
268
+ _sudo=True,
269
+ _if=config_condition,
270
+ )
271
+
272
+
273
+ deploy_warp()
@@ -0,0 +1,81 @@
1
+ """Xray proxy deployment module."""
2
+
3
+ from pyinfra.context import host
4
+ from pyinfra.facts.files import File
5
+ from pyinfra.operations import files, server, systemd
6
+
7
+ from nullforge.molds import FeaturesMold, XrayCoreMold
8
+ from nullforge.smithy.admin import ensure_acl_access
9
+ from nullforge.smithy.http import CURL_ARGS
10
+ from nullforge.smithy.versions import STATIC_URLS
11
+
12
+
13
+ GEOIP_DAT_URL = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
14
+ GEOSITE_DAT_URL = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
15
+
16
+ GEO_DATS = {
17
+ "geoip.dat": GEOIP_DAT_URL,
18
+ "geosite.dat": GEOSITE_DAT_URL,
19
+ }
20
+
21
+ BASE_DIR = "/usr/local/share/xray"
22
+ CONFIG_DIR = "/usr/local/etc/xray"
23
+
24
+
25
+ def deploy_xray() -> None:
26
+ """Deploy Xray proxy configuration."""
27
+
28
+ features: FeaturesMold = host.data.features
29
+ xray_opts: XrayCoreMold = features.xray
30
+
31
+ _install_xray(xray_opts)
32
+ _download_geo_data()
33
+
34
+ systemd.service(
35
+ name="Ensure Xray is running and enabled",
36
+ service="xray",
37
+ running=True,
38
+ enabled=True,
39
+ _sudo=True,
40
+ )
41
+
42
+ if features.users.manage:
43
+ _setup_acls(features.users.name)
44
+
45
+
46
+ def _install_xray(opts: XrayCoreMold) -> None:
47
+ """Install Xray using official installation script."""
48
+
49
+ if host.get_fact(File, "/usr/local/bin/xray"):
50
+ return
51
+
52
+ server.shell(
53
+ name="Install Xray proxy",
54
+ commands=f'bash -lc "$(curl -L {STATIC_URLS["xray_install"]})" @ install --beta',
55
+ _sudo=True,
56
+ )
57
+
58
+
59
+ def _download_geo_data() -> None:
60
+ """Download GeoIP and GeoSite data files."""
61
+
62
+ for file, url in host.loop(GEO_DATS.items()):
63
+ files.download(
64
+ name=f"Download {file}",
65
+ src=url,
66
+ dest=f"{BASE_DIR}/{file}",
67
+ extra_curl_args=CURL_ARGS,
68
+ _sudo=True,
69
+ _retries=3,
70
+ _retry_delay=10,
71
+ )
72
+
73
+
74
+ def _setup_acls(username: str) -> None:
75
+ """Set up ACLs on Xray directories for config management."""
76
+
77
+ ensure_acl_access(username, BASE_DIR)
78
+ ensure_acl_access(username, CONFIG_DIR, [f"{CONFIG_DIR}/config.json"])
79
+
80
+
81
+ deploy_xray()
@@ -0,0 +1,91 @@
1
+ """Cloudflare Zero Trust Tunnel deployment module."""
2
+
3
+ from pyinfra.context import host
4
+ from pyinfra.operations import files, systemd
5
+ from pyinfra.operations.util import any_changed
6
+
7
+ from nullforge.molds import FeaturesMold, WarpMold, ZeroTrustTunnelMold
8
+ from nullforge.smithy.cloudflare import CLOUDFLARE_GROUP, CLOUDFLARE_USER, ensure_cloudflared_binary
9
+ from nullforge.smithy.service import ensure_service_user
10
+ from nullforge.templates import BLOCK_TRIM_ENV, get_template_path
11
+
12
+
13
+ def deploy_zerotrust_tunnel() -> None:
14
+ features: FeaturesMold = host.data.features
15
+ tunnel_opts: ZeroTrustTunnelMold = features.zerotrust
16
+
17
+ _deploy_tunnel(tunnel_opts, features.warp)
18
+
19
+
20
+ def _deploy_tunnel(opts: ZeroTrustTunnelMold, warp_opts: WarpMold) -> None:
21
+ ensure_service_user(CLOUDFLARE_USER, CLOUDFLARE_GROUP, "/etc/cloudflare")
22
+ binary_op = ensure_cloudflared_binary()
23
+
24
+ config_dir = "/etc/cloudflare"
25
+ config_path = f"{config_dir}/tunnel.yml"
26
+ config_template = files.template(
27
+ name="Deploy Zero Trust Tunnel configuration",
28
+ src=get_template_path("cloudflared/tunnel.yml.j2"),
29
+ dest=config_path,
30
+ mode="0600",
31
+ user=CLOUDFLARE_USER,
32
+ group=CLOUDFLARE_GROUP,
33
+ jinja_env_kwargs=BLOCK_TRIM_ENV,
34
+ TOKEN=opts.token,
35
+ PROTOCOL=opts.protocol,
36
+ HA_CONNECTIONS=opts.ha_connections,
37
+ POST_QUANTUM=opts.post_quantum,
38
+ _sudo=True,
39
+ )
40
+
41
+ warp_route_script = None
42
+ if opts.route_through_warp:
43
+ warp_route_script = files.put(
44
+ name="Deploy Zero Trust tunnel WARP routing script",
45
+ src=get_template_path("scripts/zt-tunnel-warp.sh"),
46
+ dest=f"{config_dir}/zt-tunnel-warp.sh",
47
+ user=CLOUDFLARE_USER,
48
+ group=CLOUDFLARE_GROUP,
49
+ mode="0755",
50
+ _sudo=True,
51
+ )
52
+
53
+ service_template = files.template(
54
+ name="Deploy Zero Trust Tunnel systemd service",
55
+ src=get_template_path("systemd/cloudflare-tunnel.service.j2"),
56
+ dest="/etc/systemd/system/cloudflare-tunnel.service",
57
+ mode="0644",
58
+ jinja_env_kwargs=BLOCK_TRIM_ENV,
59
+ CONFIG_PATH=config_path,
60
+ WORKDIR=config_dir,
61
+ ROUTE_THROUGH_WARP=opts.route_through_warp,
62
+ WARP_IFACE=warp_opts.iface,
63
+ WARP_SERVICE=warp_opts.engine.systemd_service_name,
64
+ _sudo=True,
65
+ )
66
+
67
+ systemd.daemon_reload(
68
+ name="Reload systemd daemon for Zero Trust Tunnel",
69
+ _sudo=True,
70
+ _if=service_template.did_change,
71
+ )
72
+
73
+ restart_ops = [op for op in (binary_op, config_template, service_template, warp_route_script) if op]
74
+ systemd.service(
75
+ name="Restart Zero Trust Tunnel on change",
76
+ service="cloudflare-tunnel",
77
+ restarted=True,
78
+ _sudo=True,
79
+ _if=any_changed(*restart_ops),
80
+ )
81
+
82
+ systemd.service(
83
+ name="Ensure Zero Trust Tunnel is running and enabled",
84
+ service="cloudflare-tunnel",
85
+ running=True,
86
+ enabled=True,
87
+ _sudo=True,
88
+ )
89
+
90
+
91
+ deploy_zerotrust_tunnel()
@@ -0,0 +1 @@
1
+ """Helpers for NullForge."""
@@ -0,0 +1,40 @@
1
+ """Admin utilities for NullForge."""
2
+
3
+ from pyinfra.context import host
4
+ from pyinfra.facts.server import Command, User
5
+ from pyinfra.operations import server
6
+
7
+
8
+ def is_root() -> bool:
9
+ cache_key = "_nullforge_is_root"
10
+ if hasattr(host.data, cache_key):
11
+ return getattr(host.data, cache_key)
12
+
13
+ result = host.get_fact(User) == "root"
14
+
15
+ setattr(host.data, cache_key, result)
16
+ return result
17
+
18
+
19
+ def _acl_entries(path: str) -> str:
20
+ return host.get_fact(Command, f"getfacl {path} 2>/dev/null || true", _sudo=True) or ""
21
+
22
+
23
+ def ensure_acl_access(username: str, directory: str, rw_files: list[str] | None = None) -> None:
24
+ """Grant a user rwx ACL access to a directory, and optional rw to specific files."""
25
+
26
+ cmds: list[str] = []
27
+
28
+ if f"user:{username}:rwx" not in _acl_entries(directory):
29
+ cmds += [
30
+ f"setfacl -m u:{username}:rwx {directory}",
31
+ f"setfacl -d -m u:{username}:rwx {directory}",
32
+ ]
33
+
34
+ for f in rw_files or []:
35
+ entries = _acl_entries(f)
36
+ if entries and f"user:{username}:rw" not in entries:
37
+ cmds.append(f"setfacl -m u:{username}:rw {f}")
38
+
39
+ if cmds:
40
+ server.shell(name=f"Set ACLs on {directory}", commands=cmds, _sudo=True)
@@ -0,0 +1,36 @@
1
+ """Architecture utilities for NullForge."""
2
+
3
+ import platform
4
+
5
+ from pyinfra.context import host
6
+ from pyinfra.facts.server import Arch
7
+
8
+
9
+ def _normalize(a: str | None) -> str:
10
+ return {
11
+ "x86_64": "x86_64",
12
+ "amd64": "x86_64",
13
+ "aarch64": "arm64",
14
+ "arm64": "arm64",
15
+ }.get(a or "", a or "x86_64")
16
+
17
+
18
+ def arch_id() -> str:
19
+ """Normalize arch id where vendors differ."""
20
+
21
+ cache_key = "_nullforge_arch_id"
22
+ try:
23
+ if hasattr(host.data, cache_key):
24
+ return getattr(host.data, cache_key)
25
+ result = _normalize(host.get_fact(Arch))
26
+ setattr(host.data, cache_key, result)
27
+ return result
28
+ except AttributeError:
29
+ return _normalize(platform.machine())
30
+
31
+
32
+ def deb_arch() -> str:
33
+ return {
34
+ "x86_64": "amd64",
35
+ "arm64": "arm64",
36
+ }.get(arch_id(), "amd64")
@@ -0,0 +1,37 @@
1
+ """Blocky DNS proxy setup module."""
2
+
3
+ from pyinfra.api.operation import OperationMeta
4
+ from pyinfra.context import host
5
+ from pyinfra.operations import files
6
+
7
+ from nullforge.smithy.install import install_release_binary
8
+ from nullforge.smithy.versions import get_versions, is_pinned_version_installed
9
+
10
+
11
+ BLOCKY_GROUP = "blocky"
12
+ BLOCKY_USER = "blocky"
13
+ BLOCKY_BINARY = "/usr/bin/blocky"
14
+
15
+
16
+ def ensure_blocky_binary() -> OperationMeta | None:
17
+ if is_pinned_version_installed("blocky", BLOCKY_BINARY):
18
+ host.noop("blocky binary is already installed")
19
+ return None
20
+
21
+ install_op = install_release_binary(
22
+ name="Install blocky binary",
23
+ url=get_versions().blocky_tar(),
24
+ dest=BLOCKY_BINARY,
25
+ binary_name="blocky",
26
+ )
27
+
28
+ files.file(
29
+ name="Set blocky binary permissions",
30
+ path=BLOCKY_BINARY,
31
+ mode="0755",
32
+ user="root",
33
+ group=BLOCKY_GROUP,
34
+ _sudo=True,
35
+ )
36
+
37
+ return install_op
@@ -0,0 +1,32 @@
1
+ """Cloudflare service management module."""
2
+
3
+ from pyinfra.api.operation import OperationMeta
4
+ from pyinfra.context import host
5
+ from pyinfra.operations import files
6
+
7
+ from nullforge.smithy.versions import get_versions, is_pinned_version_installed
8
+
9
+
10
+ CLOUDFLARE_GROUP = "cloudflare"
11
+ CLOUDFLARE_USER = "cloudflare"
12
+ CLOUDFLARED_BINARY = "/usr/bin/cloudflared"
13
+
14
+
15
+ def ensure_cloudflared_binary() -> OperationMeta | None:
16
+ if is_pinned_version_installed("cloudflared", CLOUDFLARED_BINARY):
17
+ host.noop("cloudflared binary is already installed")
18
+ return None
19
+
20
+ return files.download(
21
+ name="Install cloudflared binary",
22
+ src=get_versions().cloudflared_url(),
23
+ dest=CLOUDFLARED_BINARY,
24
+ mode="0755",
25
+ user=CLOUDFLARE_USER,
26
+ group=CLOUDFLARE_GROUP,
27
+ force=True,
28
+ extra_curl_args=get_versions().release_curl_args(),
29
+ _sudo=True,
30
+ _retries=3,
31
+ _retry_delay=10,
32
+ )