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.
- nullforge/__init__.py +1 -0
- nullforge/foundry/README.md +0 -0
- nullforge/foundry/__init__.py +7 -0
- nullforge/foundry/full_cast.py +22 -0
- nullforge/inventories/README.md +0 -0
- nullforge/inventories/example.py +87 -0
- nullforge/models/__init__.py +1 -0
- nullforge/models/containers.py +60 -0
- nullforge/models/dns.py +222 -0
- nullforge/models/monitoring/__init__.py +29 -0
- nullforge/models/monitoring/base.py +19 -0
- nullforge/models/monitoring/nezha.py +13 -0
- nullforge/models/netsec.py +43 -0
- nullforge/models/profiles.py +33 -0
- nullforge/models/system.py +15 -0
- nullforge/models/users.py +9 -0
- nullforge/models/warp.py +65 -0
- nullforge/models/zerotrust.py +9 -0
- nullforge/molds/__init__.py +38 -0
- nullforge/molds/base_mold.py +59 -0
- nullforge/molds/containers.py +26 -0
- nullforge/molds/defaults.py +10 -0
- nullforge/molds/dns.py +70 -0
- nullforge/molds/features.py +75 -0
- nullforge/molds/haproxy.py +23 -0
- nullforge/molds/monitoring/__init__.py +45 -0
- nullforge/molds/monitoring/nezha.py +118 -0
- nullforge/molds/netsec.py +237 -0
- nullforge/molds/profiles.py +30 -0
- nullforge/molds/system.py +140 -0
- nullforge/molds/telemt.py +140 -0
- nullforge/molds/tor.py +28 -0
- nullforge/molds/user.py +77 -0
- nullforge/molds/utils.py +115 -0
- nullforge/molds/warp.py +54 -0
- nullforge/molds/xray.py +16 -0
- nullforge/molds/zerotrust.py +59 -0
- nullforge/runes/__init__.py +46 -0
- nullforge/runes/base.py +327 -0
- nullforge/runes/containers.py +212 -0
- nullforge/runes/dns.py +256 -0
- nullforge/runes/haproxy.py +135 -0
- nullforge/runes/monitoring.py +21 -0
- nullforge/runes/netsec.py +547 -0
- nullforge/runes/prepare.py +30 -0
- nullforge/runes/profiles.py +583 -0
- nullforge/runes/telemt.py +252 -0
- nullforge/runes/tor.py +57 -0
- nullforge/runes/users.py +159 -0
- nullforge/runes/warp.py +273 -0
- nullforge/runes/xray.py +81 -0
- nullforge/runes/zerotrust.py +91 -0
- nullforge/smithy/__init__.py +1 -0
- nullforge/smithy/admin.py +40 -0
- nullforge/smithy/arch.py +36 -0
- nullforge/smithy/blocky.py +37 -0
- nullforge/smithy/cloudflare.py +32 -0
- nullforge/smithy/github.py +256 -0
- nullforge/smithy/http.py +85 -0
- nullforge/smithy/install.py +149 -0
- nullforge/smithy/monitoring/__init__.py +1 -0
- nullforge/smithy/monitoring/nezha/__init__.py +0 -0
- nullforge/smithy/monitoring/nezha/agent.py +54 -0
- nullforge/smithy/monitoring/nezha/dashboard.py +137 -0
- nullforge/smithy/monitoring/nezha/deploy.py +81 -0
- nullforge/smithy/network.py +95 -0
- nullforge/smithy/packages.py +176 -0
- nullforge/smithy/service.py +37 -0
- nullforge/smithy/sni.py +358 -0
- nullforge/smithy/swap.py +132 -0
- nullforge/smithy/system.py +86 -0
- nullforge/smithy/versions.py +282 -0
- nullforge/templates/__init__.py +32 -0
- nullforge/templates/cloudflared/tunnel.yml.j2 +6 -0
- nullforge/templates/dns/blocky.yaml.j2 +11 -0
- nullforge/templates/dns/dns.yaml.j2 +6 -0
- nullforge/templates/dns/resolv.conf.j2 +1 -0
- nullforge/templates/dns/resolved.conf.j2 +13 -0
- nullforge/templates/etc/default/zramswap.j2 +13 -0
- nullforge/templates/nvim/nvim_patch.lua.j2 +5 -0
- nullforge/templates/profiles/direnv.toml +2 -0
- nullforge/templates/profiles/starship.toml +186 -0
- nullforge/templates/profiles/tmux.conf +32 -0
- nullforge/templates/profiles/zshrc.j2 +46 -0
- nullforge/templates/scripts/telemt-synfix.sh +89 -0
- nullforge/templates/scripts/teleproxy-warp.sh +127 -0
- nullforge/templates/scripts/warp-v6-policy.sh +118 -0
- nullforge/templates/scripts/zt-tunnel-warp.sh +28 -0
- nullforge/templates/systemd/blocky.service.j2 +19 -0
- nullforge/templates/systemd/cloudflare-tunnel.service.j2 +27 -0
- nullforge/templates/systemd/cloudflare-warp.service.j2 +21 -0
- nullforge/templates/systemd/dns-internal.service.j2 +15 -0
- nullforge/templates/systemd/telemt.service.j2 +36 -0
- nullforge/templates/telemt/telemt.toml.j2 +49 -0
- nullforge/templates/tor/torrc.j2 +5 -0
- nullforge-0.1.0.dist-info/METADATA +39 -0
- nullforge-0.1.0.dist-info/RECORD +99 -0
- nullforge-0.1.0.dist-info/WHEEL +4 -0
- 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()
|