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,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
@@ -0,0 +1,10 @@
1
+ """Base configuration presets."""
2
+
3
+ from nullforge.molds import FeaturesMold, SystemMold
4
+
5
+
6
+ BASE_FEATURES = FeaturesMold()
7
+ """Default features configuration."""
8
+
9
+ BASE_SYSTEM = SystemMold()
10
+ """Default system configuration."""
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)