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,38 @@
|
|
|
1
|
+
"""Configuration molds for NullForge."""
|
|
2
|
+
|
|
3
|
+
from .base_mold import BaseMold
|
|
4
|
+
from .containers import ContainersMold
|
|
5
|
+
from .dns import DnsMold
|
|
6
|
+
from .features import FeaturesMold
|
|
7
|
+
from .haproxy import HaproxyMold
|
|
8
|
+
from .monitoring import MonitoringMold
|
|
9
|
+
from .netsec import FirewallRule, NetSecMold, SshMold
|
|
10
|
+
from .profiles import ProfilesMold
|
|
11
|
+
from .system import SystemMold
|
|
12
|
+
from .telemt import TelemtMold
|
|
13
|
+
from .tor import TorMold
|
|
14
|
+
from .user import UserMold
|
|
15
|
+
from .warp import WarpMold
|
|
16
|
+
from .xray import XrayCoreMold
|
|
17
|
+
from .zerotrust import ZeroTrustTunnelMold
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BaseMold",
|
|
22
|
+
"ContainersMold",
|
|
23
|
+
"DnsMold",
|
|
24
|
+
"FeaturesMold",
|
|
25
|
+
"FirewallRule",
|
|
26
|
+
"HaproxyMold",
|
|
27
|
+
"MonitoringMold",
|
|
28
|
+
"NetSecMold",
|
|
29
|
+
"ProfilesMold",
|
|
30
|
+
"SshMold",
|
|
31
|
+
"SystemMold",
|
|
32
|
+
"TelemtMold",
|
|
33
|
+
"TorMold",
|
|
34
|
+
"UserMold",
|
|
35
|
+
"WarpMold",
|
|
36
|
+
"XrayCoreMold",
|
|
37
|
+
"ZeroTrustTunnelMold",
|
|
38
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from typing import Any, ClassVar
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
REDACTED_VALUE = "***"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseMold(BaseModel):
|
|
10
|
+
"""Base model class with JSON serialization support for pyinfra."""
|
|
11
|
+
|
|
12
|
+
model_config = ConfigDict(extra="forbid")
|
|
13
|
+
|
|
14
|
+
_sensitive_fields: ClassVar[tuple[str, ...]] = ()
|
|
15
|
+
"""Field names whose values are redacted by `to_json`."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def is_active(self) -> bool:
|
|
19
|
+
raise NotImplementedError(
|
|
20
|
+
f"{type(self).__name__}.is_active must be implemented as @property that "
|
|
21
|
+
"returns True when rune for this mold should be deployed."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def to_json(self) -> dict[str, Any]:
|
|
25
|
+
"""JSON-serializable representation for pyinfra's debug-inventory.
|
|
26
|
+
|
|
27
|
+
Uses JSON mode so that complex internal types (e.g. IPvAnyAddress, UUID, enums)
|
|
28
|
+
are serialized to simple primitives (str etc) while keeping rich types inside the model.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
data = self.model_dump(mode="json")
|
|
32
|
+
return self._redact(self, data)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def _redact(cls, model: BaseModel, data: dict[str, Any]) -> dict[str, Any]:
|
|
36
|
+
"""Redact model's sensitive fields and recurse into nested molds."""
|
|
37
|
+
|
|
38
|
+
sensitive = getattr(type(model), "_sensitive_fields", ())
|
|
39
|
+
for key in sensitive:
|
|
40
|
+
if data.get(key):
|
|
41
|
+
data[key] = REDACTED_VALUE
|
|
42
|
+
|
|
43
|
+
for name in type(model).model_fields:
|
|
44
|
+
value = getattr(model, name)
|
|
45
|
+
if name in data:
|
|
46
|
+
data[name] = cls._redact_value(value, data[name])
|
|
47
|
+
return data
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def _redact_value(cls, value: Any, dumped: Any) -> Any:
|
|
51
|
+
"""Mirror dumped value against live counterpart, redacting nested molds."""
|
|
52
|
+
|
|
53
|
+
if isinstance(value, BaseMold) and isinstance(dumped, dict):
|
|
54
|
+
return cls._redact(value, dumped)
|
|
55
|
+
if isinstance(value, (list, tuple)) and isinstance(dumped, list):
|
|
56
|
+
return [cls._redact_value(v, d) for v, d in zip(value, dumped)]
|
|
57
|
+
if isinstance(value, dict) and isinstance(dumped, dict):
|
|
58
|
+
return {k: cls._redact_value(value[k], d) for k, d in dumped.items() if k in value}
|
|
59
|
+
return dumped
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Containers configuration mold."""
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from nullforge.models.containers import ContainersBackendType
|
|
6
|
+
|
|
7
|
+
from .base_mold import BaseMold
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ContainersMold(BaseMold):
|
|
11
|
+
install: bool = Field(
|
|
12
|
+
default=False,
|
|
13
|
+
description="Whether to install containers backend",
|
|
14
|
+
)
|
|
15
|
+
backend_type: ContainersBackendType = Field(
|
|
16
|
+
default=ContainersBackendType.DOCKER,
|
|
17
|
+
description="Which containers backend to use",
|
|
18
|
+
)
|
|
19
|
+
skopeo: bool = Field(
|
|
20
|
+
default=True,
|
|
21
|
+
description="Whether to install skopeo",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def is_active(self) -> bool:
|
|
26
|
+
return self.install
|
nullforge/molds/dns.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""DNS configuration mold."""
|
|
2
|
+
|
|
3
|
+
from ipaddress import IPv4Address, IPv6Address
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from pydantic import Field, IPvAnyAddress, field_validator
|
|
7
|
+
|
|
8
|
+
from nullforge.models.dns import DnsMode, DnsProvider
|
|
9
|
+
|
|
10
|
+
from .base_mold import BaseMold
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DnsMold(BaseMold):
|
|
14
|
+
"""Full DNS configuration mold."""
|
|
15
|
+
|
|
16
|
+
mode: DnsMode = Field(
|
|
17
|
+
default=DnsMode.BLOCKY,
|
|
18
|
+
description="How DNS resolution should be performed",
|
|
19
|
+
)
|
|
20
|
+
upstream_provider: DnsProvider = Field(
|
|
21
|
+
default=DnsProvider.CLOUDFLARE,
|
|
22
|
+
description="Provider for upstream servers.",
|
|
23
|
+
)
|
|
24
|
+
ecs: bool = Field(
|
|
25
|
+
default=False,
|
|
26
|
+
description="Enable ECS (EDNS Client Subnet) for upstream provider.",
|
|
27
|
+
)
|
|
28
|
+
listen_address: IPvAnyAddress = Field(
|
|
29
|
+
default="169.254.0.53",
|
|
30
|
+
validate_default=True,
|
|
31
|
+
description=(
|
|
32
|
+
"IP address blocky binds to. Wildcards and loopback use existing interfaces; "
|
|
33
|
+
"non-global addresses create dedicated dummy interface."
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_active(self) -> bool:
|
|
39
|
+
return self.mode != DnsMode.NONE
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
# NOTE: This stub widens `listen_address`
|
|
43
|
+
# Keep the list in sync with the fields above when they change.
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
mode: DnsMode = DnsMode.BLOCKY,
|
|
48
|
+
upstream_provider: DnsProvider = DnsProvider.CLOUDFLARE,
|
|
49
|
+
ecs: bool = False,
|
|
50
|
+
listen_address: IPvAnyAddress | str = "169.254.0.53",
|
|
51
|
+
) -> None: ...
|
|
52
|
+
|
|
53
|
+
@field_validator("listen_address")
|
|
54
|
+
@classmethod
|
|
55
|
+
def _validate_listen_address(cls, v: IPv4Address | IPv6Address) -> IPv4Address | IPv6Address:
|
|
56
|
+
if v.is_unspecified or v.is_loopback:
|
|
57
|
+
return v
|
|
58
|
+
|
|
59
|
+
if not v.is_global:
|
|
60
|
+
return v
|
|
61
|
+
|
|
62
|
+
raise ValueError(f"listen_address {v!r} must be a wildcard, loopback, or non-global address.")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def needs_custom_interface(self) -> bool:
|
|
66
|
+
"""True when blocky should bind to dedicated dummy interface."""
|
|
67
|
+
|
|
68
|
+
if self.listen_address.is_unspecified or self.listen_address.is_loopback:
|
|
69
|
+
return False
|
|
70
|
+
return True
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Features controller model."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from .base_mold import BaseMold
|
|
8
|
+
from .containers import ContainersMold
|
|
9
|
+
from .dns import DnsMold
|
|
10
|
+
from .haproxy import HaproxyMold
|
|
11
|
+
from .monitoring import MonitoringMold
|
|
12
|
+
from .netsec import NetSecMold
|
|
13
|
+
from .profiles import ProfilesMold
|
|
14
|
+
from .telemt import TelemtMold
|
|
15
|
+
from .tor import TorMold
|
|
16
|
+
from .user import UserMold
|
|
17
|
+
from .warp import WarpMold
|
|
18
|
+
from .xray import XrayCoreMold
|
|
19
|
+
from .zerotrust import ZeroTrustTunnelMold
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FeaturesMold(BaseMold):
|
|
23
|
+
"""Defines all supported molds and order of deployment."""
|
|
24
|
+
|
|
25
|
+
dns: DnsMold = Field(default_factory=DnsMold)
|
|
26
|
+
users: UserMold = Field(default_factory=UserMold)
|
|
27
|
+
netsec: NetSecMold = Field(default_factory=NetSecMold)
|
|
28
|
+
profiles: ProfilesMold = Field(default_factory=ProfilesMold)
|
|
29
|
+
warp: WarpMold = Field(default_factory=WarpMold)
|
|
30
|
+
zerotrust: ZeroTrustTunnelMold = Field(default_factory=ZeroTrustTunnelMold)
|
|
31
|
+
containers: ContainersMold = Field(default_factory=ContainersMold)
|
|
32
|
+
monitoring: MonitoringMold = Field(default_factory=MonitoringMold)
|
|
33
|
+
haproxy: HaproxyMold = Field(default_factory=HaproxyMold)
|
|
34
|
+
xray: XrayCoreMold = Field(default_factory=XrayCoreMold)
|
|
35
|
+
tor: TorMold = Field(default_factory=TorMold)
|
|
36
|
+
telemt: TelemtMold = Field(default_factory=TelemtMold)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
FEATURE_MOLD_MAPPING: dict[str, type[BaseMold]] = {}
|
|
40
|
+
"""Derived after class definition. Adding a feature field automatically updates these."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _build_mold_map() -> None:
|
|
44
|
+
global FEATURE_MOLD_MAPPING
|
|
45
|
+
mapping: dict[str, type[BaseMold]] = {}
|
|
46
|
+
for name, finfo in FeaturesMold.model_fields.items():
|
|
47
|
+
ann = finfo.annotation
|
|
48
|
+
if isinstance(ann, type) and issubclass(ann, BaseMold) and ann is not BaseMold:
|
|
49
|
+
mapping[name] = ann
|
|
50
|
+
FEATURE_MOLD_MAPPING = mapping
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_build_mold_map()
|
|
54
|
+
|
|
55
|
+
ALLOWED_FEATURES_LAYERS: tuple[type[BaseMold], ...] = tuple(FEATURE_MOLD_MAPPING.values())
|
|
56
|
+
"""Allowed molds for the FeaturesMold."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def iter_runes(features: FeaturesMold) -> Iterator[tuple[str, bool]]:
|
|
60
|
+
"""Yield every dispatchable (rune name, active) pair, in a stable order.
|
|
61
|
+
|
|
62
|
+
Rune name defaults to attribute name under FeaturesMold (e.g. "warp" -> "warp").
|
|
63
|
+
Sub-mold classes may set `_feature_rune: ClassVar[str | None]`:
|
|
64
|
+
- str value overrides the rune stem
|
|
65
|
+
- None value opts the feature out of automatic dispatch
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
yielded: set[str] = set()
|
|
69
|
+
|
|
70
|
+
for attr, mold_cls in FEATURE_MOLD_MAPPING.items():
|
|
71
|
+
rune_name = getattr(mold_cls, "_feature_rune", attr)
|
|
72
|
+
if rune_name is None or rune_name in yielded:
|
|
73
|
+
continue
|
|
74
|
+
yielded.add(rune_name)
|
|
75
|
+
yield rune_name, getattr(features, attr).is_active
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""HAProxy configuration mold."""
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from .base_mold import BaseMold
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HaproxyMold(BaseMold):
|
|
9
|
+
install: bool = Field(
|
|
10
|
+
default=False,
|
|
11
|
+
description="Whether to install HAProxy proxy server",
|
|
12
|
+
)
|
|
13
|
+
version: str = Field(
|
|
14
|
+
default="3.2",
|
|
15
|
+
description=(
|
|
16
|
+
"HAProxy version to install from `haproxy.debian.net` (Debian) or `vbernat PPA` (Ubuntu). "
|
|
17
|
+
"Ignored on RHEL-based distros, which only offer version shipped by distro."
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def is_active(self) -> bool:
|
|
23
|
+
return self.install
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Monitoring configuration mold."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, model_validator
|
|
6
|
+
|
|
7
|
+
from nullforge.models.monitoring import MonitoringBackendType
|
|
8
|
+
|
|
9
|
+
from ..base_mold import BaseMold
|
|
10
|
+
from .nezha import NezhaBackend
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
MonitoringBackend = Annotated[NezhaBackend, Field(discriminator="type")]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MonitoringMold(BaseMold):
|
|
17
|
+
install: bool = Field(
|
|
18
|
+
default=False,
|
|
19
|
+
description="Whether to deploy monitoring agent",
|
|
20
|
+
)
|
|
21
|
+
backend: MonitoringBackend = Field(
|
|
22
|
+
default_factory=NezhaBackend,
|
|
23
|
+
description="Monitoring backend and its configuration",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@model_validator(mode="after")
|
|
27
|
+
def _validate_install(self) -> "MonitoringMold":
|
|
28
|
+
if self.install:
|
|
29
|
+
self.backend.validate_enabled()
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def backend_type(self) -> MonitoringBackendType:
|
|
34
|
+
return self.backend.type
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def is_active(self) -> bool:
|
|
38
|
+
return self.install
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"MonitoringBackend",
|
|
43
|
+
"MonitoringMold",
|
|
44
|
+
"NezhaBackend",
|
|
45
|
+
]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Nezha monitoring backend configuration mold."""
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from typing import TYPE_CHECKING, ClassVar, Literal
|
|
5
|
+
from uuid import NAMESPACE_DNS, UUID, uuid5
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, field_validator
|
|
8
|
+
|
|
9
|
+
from nullforge.models.monitoring import MonitoringBackendType, MonitoringLayout, monitoring_layout_factory
|
|
10
|
+
|
|
11
|
+
from ..base_mold import BaseMold
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NezhaBackend(BaseMold):
|
|
15
|
+
"""Per-host configuration for the Nezha monitoring agent."""
|
|
16
|
+
|
|
17
|
+
_sensitive_fields: ClassVar[tuple[str, ...]] = ("client_secret", "api_token")
|
|
18
|
+
|
|
19
|
+
type: Literal[MonitoringBackendType.NEZHA] = Field(
|
|
20
|
+
default=MonitoringBackendType.NEZHA,
|
|
21
|
+
description="Discriminator selecting Nezha backend",
|
|
22
|
+
)
|
|
23
|
+
server: str = Field(
|
|
24
|
+
default="",
|
|
25
|
+
description="Agent->dashboard data endpoint as host:port (NZ_SERVER)",
|
|
26
|
+
)
|
|
27
|
+
dashboard_url: str = Field(
|
|
28
|
+
default="",
|
|
29
|
+
description="Dashboard HTTP base URL used for API calls",
|
|
30
|
+
)
|
|
31
|
+
tls: bool = Field(
|
|
32
|
+
default=True,
|
|
33
|
+
description="Use TLS for agent data channel (NZ_TLS)",
|
|
34
|
+
)
|
|
35
|
+
client_secret: str = Field(
|
|
36
|
+
default="",
|
|
37
|
+
description="Per-user connection secret (NZ_CLIENT_SECRET)",
|
|
38
|
+
)
|
|
39
|
+
disable_auto_update: bool = Field(
|
|
40
|
+
default=False,
|
|
41
|
+
description="Disable agent's self-update (NZ_DISABLE_AUTO_UPDATE)",
|
|
42
|
+
)
|
|
43
|
+
disable_command_execute: bool = Field(
|
|
44
|
+
default=True,
|
|
45
|
+
description="Disable dashboard-initiated command execution (NZ_DISABLE_COMMAND_EXECUTE)",
|
|
46
|
+
)
|
|
47
|
+
set_name_to_hostname: bool = Field(
|
|
48
|
+
default=True,
|
|
49
|
+
description="Rename auto-registered dashboard entry to host hostname",
|
|
50
|
+
)
|
|
51
|
+
api_token: str = Field(
|
|
52
|
+
default="",
|
|
53
|
+
description="Dashboard PAT (nzp_...) for API calls",
|
|
54
|
+
)
|
|
55
|
+
uuid_namespace: UUID | None = Field(
|
|
56
|
+
default=NAMESPACE_DNS,
|
|
57
|
+
description=("Namespace for deterministic agent UUIDv5 dashboard-unique generation"),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if TYPE_CHECKING:
|
|
61
|
+
# NOTE: This stub widens `uuid_namespace`
|
|
62
|
+
# Keep the list in sync with the fields above when they change.
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
type: Literal[MonitoringBackendType.NEZHA] = MonitoringBackendType.NEZHA,
|
|
67
|
+
server: str = "",
|
|
68
|
+
dashboard_url: str = "",
|
|
69
|
+
tls: bool = True,
|
|
70
|
+
client_secret: str = "",
|
|
71
|
+
disable_auto_update: bool = False,
|
|
72
|
+
disable_command_execute: bool = True,
|
|
73
|
+
set_name_to_hostname: bool = True,
|
|
74
|
+
api_token: str = "",
|
|
75
|
+
uuid_namespace: UUID | str | None = NAMESPACE_DNS,
|
|
76
|
+
) -> None: ...
|
|
77
|
+
|
|
78
|
+
@field_validator("server", "dashboard_url", "client_secret", "api_token")
|
|
79
|
+
@classmethod
|
|
80
|
+
def _strip(cls, v: str) -> str:
|
|
81
|
+
return v.strip()
|
|
82
|
+
|
|
83
|
+
@field_validator("dashboard_url")
|
|
84
|
+
@classmethod
|
|
85
|
+
def _validate_dashboard_url(cls, v: str) -> str:
|
|
86
|
+
v = v.rstrip("/")
|
|
87
|
+
if v and not v.startswith("https://"):
|
|
88
|
+
raise ValueError("dashboard_url must start with https://")
|
|
89
|
+
return v
|
|
90
|
+
|
|
91
|
+
@field_validator("uuid_namespace", mode="before")
|
|
92
|
+
@classmethod
|
|
93
|
+
def _coerce_uuid_namespace(cls, v: object) -> UUID | None:
|
|
94
|
+
if v is None or isinstance(v, UUID):
|
|
95
|
+
return v
|
|
96
|
+
if isinstance(v, str):
|
|
97
|
+
v = v.strip()
|
|
98
|
+
if not v:
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
return UUID(v)
|
|
102
|
+
except ValueError:
|
|
103
|
+
return uuid5(UUID(int=0), v)
|
|
104
|
+
raise TypeError("uuid_namespace must be UUID, string, or None")
|
|
105
|
+
|
|
106
|
+
def validate_enabled(self) -> None:
|
|
107
|
+
"""Assert config is complete enough to deploy."""
|
|
108
|
+
|
|
109
|
+
if not self.server:
|
|
110
|
+
raise ValueError("server is required when install is True")
|
|
111
|
+
if not self.client_secret:
|
|
112
|
+
raise ValueError("client_secret is required when install is True")
|
|
113
|
+
if self.set_name_to_hostname and (not self.dashboard_url or not self.api_token):
|
|
114
|
+
raise ValueError("dashboard_url and api_token are required when set_name_to_hostname is True")
|
|
115
|
+
|
|
116
|
+
@cached_property
|
|
117
|
+
def layout(self) -> MonitoringLayout:
|
|
118
|
+
return monitoring_layout_factory(self.type)
|