redundanet 2.0.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.
- redundanet/__init__.py +16 -0
- redundanet/__main__.py +6 -0
- redundanet/auth/__init__.py +9 -0
- redundanet/auth/gpg.py +323 -0
- redundanet/auth/keyserver.py +219 -0
- redundanet/cli/__init__.py +5 -0
- redundanet/cli/main.py +247 -0
- redundanet/cli/network.py +194 -0
- redundanet/cli/node.py +305 -0
- redundanet/cli/storage.py +267 -0
- redundanet/core/__init__.py +31 -0
- redundanet/core/config.py +200 -0
- redundanet/core/exceptions.py +84 -0
- redundanet/core/manifest.py +325 -0
- redundanet/core/node.py +135 -0
- redundanet/network/__init__.py +11 -0
- redundanet/network/discovery.py +218 -0
- redundanet/network/dns.py +180 -0
- redundanet/network/validation.py +279 -0
- redundanet/storage/__init__.py +13 -0
- redundanet/storage/client.py +306 -0
- redundanet/storage/furl.py +196 -0
- redundanet/storage/introducer.py +175 -0
- redundanet/storage/storage.py +195 -0
- redundanet/utils/__init__.py +15 -0
- redundanet/utils/files.py +165 -0
- redundanet/utils/logging.py +93 -0
- redundanet/utils/process.py +226 -0
- redundanet/vpn/__init__.py +12 -0
- redundanet/vpn/keys.py +173 -0
- redundanet/vpn/mesh.py +201 -0
- redundanet/vpn/tinc.py +323 -0
- redundanet-2.0.0.dist-info/LICENSE +674 -0
- redundanet-2.0.0.dist-info/METADATA +265 -0
- redundanet-2.0.0.dist-info/RECORD +37 -0
- redundanet-2.0.0.dist-info/WHEEL +4 -0
- redundanet-2.0.0.dist-info/entry_points.txt +3 -0
redundanet/core/node.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Node representation and management for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from redundanet.core.config import NodeRole
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from redundanet.core.config import NodeConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NodeStatus(str, Enum):
|
|
17
|
+
"""Runtime status of a node."""
|
|
18
|
+
|
|
19
|
+
ONLINE = "online"
|
|
20
|
+
OFFLINE = "offline"
|
|
21
|
+
CONNECTING = "connecting"
|
|
22
|
+
ERROR = "error"
|
|
23
|
+
UNKNOWN = "unknown"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class NodeHealth:
|
|
28
|
+
"""Health information for a node."""
|
|
29
|
+
|
|
30
|
+
vpn_connected: bool = False
|
|
31
|
+
storage_available: bool = False
|
|
32
|
+
introducer_reachable: bool = False
|
|
33
|
+
last_seen: datetime | None = None
|
|
34
|
+
uptime_seconds: int = 0
|
|
35
|
+
errors: list[str] = field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_healthy(self) -> bool:
|
|
39
|
+
"""Check if the node is in a healthy state."""
|
|
40
|
+
return self.vpn_connected and len(self.errors) == 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Node:
|
|
45
|
+
"""Represents a node in the RedundaNet network."""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
internal_ip: str
|
|
49
|
+
vpn_ip: str
|
|
50
|
+
roles: list[NodeRole] = field(default_factory=list)
|
|
51
|
+
public_ip: str | None = None
|
|
52
|
+
gpg_key_id: str | None = None
|
|
53
|
+
region: str | None = None
|
|
54
|
+
is_publicly_accessible: bool = False
|
|
55
|
+
storage_contribution: str | None = None
|
|
56
|
+
storage_allocation: str | None = None
|
|
57
|
+
|
|
58
|
+
# Runtime state
|
|
59
|
+
status: NodeStatus = NodeStatus.UNKNOWN
|
|
60
|
+
health: NodeHealth = field(default_factory=NodeHealth)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_config(cls, config: NodeConfig) -> Node:
|
|
64
|
+
"""Create a Node instance from NodeConfig."""
|
|
65
|
+
return cls(
|
|
66
|
+
name=config.name,
|
|
67
|
+
internal_ip=config.internal_ip,
|
|
68
|
+
vpn_ip=config.vpn_ip or config.internal_ip,
|
|
69
|
+
roles=[NodeRole(r.value) for r in config.roles],
|
|
70
|
+
public_ip=config.public_ip,
|
|
71
|
+
gpg_key_id=config.gpg_key_id,
|
|
72
|
+
region=config.region,
|
|
73
|
+
is_publicly_accessible=config.is_publicly_accessible,
|
|
74
|
+
storage_contribution=config.storage_contribution,
|
|
75
|
+
storage_allocation=config.storage_allocation,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def has_role(self, role: NodeRole) -> bool:
|
|
79
|
+
"""Check if this node has a specific role."""
|
|
80
|
+
return role in self.roles
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def is_introducer(self) -> bool:
|
|
84
|
+
"""Check if this node is a Tahoe-LAFS introducer."""
|
|
85
|
+
return self.has_role(NodeRole.TAHOE_INTRODUCER)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def is_storage(self) -> bool:
|
|
89
|
+
"""Check if this node provides storage."""
|
|
90
|
+
return self.has_role(NodeRole.TAHOE_STORAGE)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def is_client(self) -> bool:
|
|
94
|
+
"""Check if this node is a Tahoe-LAFS client."""
|
|
95
|
+
return self.has_role(NodeRole.TAHOE_CLIENT)
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def is_vpn_node(self) -> bool:
|
|
99
|
+
"""Check if this node participates in the VPN mesh."""
|
|
100
|
+
return self.has_role(NodeRole.TINC_VPN)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def fqdn(self) -> str:
|
|
104
|
+
"""Get the fully qualified domain name for this node."""
|
|
105
|
+
return f"{self.name}.redundanet.local"
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict[str, object]:
|
|
108
|
+
"""Convert node to dictionary representation."""
|
|
109
|
+
return {
|
|
110
|
+
"name": self.name,
|
|
111
|
+
"internal_ip": self.internal_ip,
|
|
112
|
+
"vpn_ip": self.vpn_ip,
|
|
113
|
+
"public_ip": self.public_ip,
|
|
114
|
+
"gpg_key_id": self.gpg_key_id,
|
|
115
|
+
"region": self.region,
|
|
116
|
+
"roles": [r.value for r in self.roles],
|
|
117
|
+
"is_publicly_accessible": self.is_publicly_accessible,
|
|
118
|
+
"storage_contribution": self.storage_contribution,
|
|
119
|
+
"storage_allocation": self.storage_allocation,
|
|
120
|
+
"status": self.status.value,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class LocalNode(Node):
|
|
126
|
+
"""Represents the local node with additional local-only information."""
|
|
127
|
+
|
|
128
|
+
config_path: str | None = None
|
|
129
|
+
data_path: str | None = None
|
|
130
|
+
private_key_path: str | None = None
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def is_configured(self) -> bool:
|
|
134
|
+
"""Check if the local node is properly configured."""
|
|
135
|
+
return bool(self.config_path and self.private_key_path)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Network utilities module for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from redundanet.network.discovery import NodeDiscovery
|
|
4
|
+
from redundanet.network.dns import DNSManager
|
|
5
|
+
from redundanet.network.validation import NetworkValidator
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"NodeDiscovery",
|
|
9
|
+
"NetworkValidator",
|
|
10
|
+
"DNSManager",
|
|
11
|
+
]
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Node discovery for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from redundanet.utils.logging import get_logger
|
|
11
|
+
from redundanet.utils.process import run_command
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from redundanet.core.config import NodeConfig
|
|
15
|
+
from redundanet.core.manifest import Manifest
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class DiscoveredNode:
|
|
22
|
+
"""Information about a discovered node."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
vpn_ip: str
|
|
26
|
+
reachable: bool = False
|
|
27
|
+
latency_ms: float | None = None
|
|
28
|
+
last_seen: datetime | None = None
|
|
29
|
+
services: list[str] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NodeDiscovery:
|
|
33
|
+
"""Discovers and monitors nodes in the network."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
manifest: Manifest,
|
|
38
|
+
local_node_name: str,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize node discovery.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
manifest: Network manifest
|
|
44
|
+
local_node_name: Name of the local node
|
|
45
|
+
"""
|
|
46
|
+
self.manifest = manifest
|
|
47
|
+
self.local_node_name = local_node_name
|
|
48
|
+
self._discovered: dict[str, DiscoveredNode] = {}
|
|
49
|
+
|
|
50
|
+
def discover_all(self) -> dict[str, DiscoveredNode]:
|
|
51
|
+
"""Discover all nodes defined in the manifest.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dictionary mapping node name to DiscoveredNode
|
|
55
|
+
"""
|
|
56
|
+
logger.info("Discovering nodes", count=len(self.manifest.nodes))
|
|
57
|
+
|
|
58
|
+
for node in self.manifest.nodes:
|
|
59
|
+
if node.name == self.local_node_name:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
discovered = self._probe_node(node)
|
|
63
|
+
self._discovered[node.name] = discovered
|
|
64
|
+
|
|
65
|
+
if discovered.reachable:
|
|
66
|
+
logger.debug(
|
|
67
|
+
"Node reachable",
|
|
68
|
+
node=node.name,
|
|
69
|
+
latency_ms=discovered.latency_ms,
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
logger.debug("Node unreachable", node=node.name)
|
|
73
|
+
|
|
74
|
+
return self._discovered
|
|
75
|
+
|
|
76
|
+
def _probe_node(self, node: NodeConfig) -> DiscoveredNode:
|
|
77
|
+
"""Probe a single node for reachability.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
node: Node configuration
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
DiscoveredNode with probe results
|
|
84
|
+
"""
|
|
85
|
+
vpn_ip = node.vpn_ip or node.internal_ip
|
|
86
|
+
|
|
87
|
+
discovered = DiscoveredNode(
|
|
88
|
+
name=node.name,
|
|
89
|
+
vpn_ip=vpn_ip,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Ping the node
|
|
93
|
+
result = run_command(f"ping -c 2 -W 2 -q {vpn_ip}", check=False)
|
|
94
|
+
|
|
95
|
+
if result.success:
|
|
96
|
+
discovered.reachable = True
|
|
97
|
+
discovered.last_seen = datetime.now()
|
|
98
|
+
|
|
99
|
+
# Parse latency from ping output
|
|
100
|
+
for line in result.stdout.split("\n"):
|
|
101
|
+
if "rtt" in line or "round-trip" in line:
|
|
102
|
+
parts = line.split("=")
|
|
103
|
+
if len(parts) >= 2:
|
|
104
|
+
times = parts[1].strip().split("/")
|
|
105
|
+
if len(times) >= 2:
|
|
106
|
+
with contextlib.suppress(ValueError):
|
|
107
|
+
discovered.latency_ms = float(times[1])
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
# Probe services
|
|
111
|
+
discovered.services = self._probe_services(vpn_ip, node)
|
|
112
|
+
|
|
113
|
+
return discovered
|
|
114
|
+
|
|
115
|
+
def _probe_services(self, ip: str, node: NodeConfig) -> list[str]:
|
|
116
|
+
"""Probe which services are available on a node.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
ip: Node IP address
|
|
120
|
+
node: Node configuration
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of available service names
|
|
124
|
+
"""
|
|
125
|
+
services = []
|
|
126
|
+
|
|
127
|
+
# Check Tahoe-LAFS ports based on roles
|
|
128
|
+
role_ports = {
|
|
129
|
+
"tahoe_introducer": node.ports.tahoe_introducer,
|
|
130
|
+
"tahoe_storage": node.ports.tahoe_storage,
|
|
131
|
+
"tahoe_client": node.ports.tahoe_client,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for role in node.roles:
|
|
135
|
+
role_str = role.value
|
|
136
|
+
if role_str in role_ports:
|
|
137
|
+
port = role_ports[role_str]
|
|
138
|
+
if self._port_open(ip, port):
|
|
139
|
+
services.append(role_str)
|
|
140
|
+
|
|
141
|
+
return services
|
|
142
|
+
|
|
143
|
+
def _port_open(self, ip: str, port: int, timeout: float = 2.0) -> bool:
|
|
144
|
+
"""Check if a TCP port is open.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
ip: IP address
|
|
148
|
+
port: Port number
|
|
149
|
+
timeout: Timeout in seconds
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if port is open
|
|
153
|
+
"""
|
|
154
|
+
result = run_command(
|
|
155
|
+
f"nc -z -w {int(timeout)} {ip} {port}",
|
|
156
|
+
check=False,
|
|
157
|
+
timeout=timeout + 1,
|
|
158
|
+
)
|
|
159
|
+
return result.success
|
|
160
|
+
|
|
161
|
+
def get_reachable_nodes(self) -> list[DiscoveredNode]:
|
|
162
|
+
"""Get all reachable nodes.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List of reachable nodes
|
|
166
|
+
"""
|
|
167
|
+
return [n for n in self._discovered.values() if n.reachable]
|
|
168
|
+
|
|
169
|
+
def get_unreachable_nodes(self) -> list[DiscoveredNode]:
|
|
170
|
+
"""Get all unreachable nodes.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of unreachable nodes
|
|
174
|
+
"""
|
|
175
|
+
return [n for n in self._discovered.values() if not n.reachable]
|
|
176
|
+
|
|
177
|
+
def get_nodes_with_service(self, service: str) -> list[DiscoveredNode]:
|
|
178
|
+
"""Get nodes that have a specific service running.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
service: Service name (e.g., 'tahoe_storage')
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
List of nodes with the service
|
|
185
|
+
"""
|
|
186
|
+
return [n for n in self._discovered.values() if service in n.services]
|
|
187
|
+
|
|
188
|
+
def refresh_node(self, node_name: str) -> DiscoveredNode | None:
|
|
189
|
+
"""Refresh the status of a single node.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
node_name: Name of the node to refresh
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Updated DiscoveredNode or None if not found
|
|
196
|
+
"""
|
|
197
|
+
node = self.manifest.get_node(node_name)
|
|
198
|
+
if not node:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
discovered = self._probe_node(node)
|
|
202
|
+
self._discovered[node_name] = discovered
|
|
203
|
+
return discovered
|
|
204
|
+
|
|
205
|
+
def get_discovery_stats(self) -> dict[str, int]:
|
|
206
|
+
"""Get discovery statistics.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Dictionary with stats
|
|
210
|
+
"""
|
|
211
|
+
total = len(self._discovered)
|
|
212
|
+
reachable = len(self.get_reachable_nodes())
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
"total_nodes": total,
|
|
216
|
+
"reachable": reachable,
|
|
217
|
+
"unreachable": total - reachable,
|
|
218
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""DNS and hosts file management for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from redundanet.utils.files import read_file, write_file
|
|
9
|
+
from redundanet.utils.logging import get_logger
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from redundanet.core.manifest import Manifest
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
HOSTS_MARKER_START = "# BEGIN REDUNDANET NODES"
|
|
17
|
+
HOSTS_MARKER_END = "# END REDUNDANET NODES"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DNSManager:
|
|
21
|
+
"""Manages local DNS resolution for network nodes."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
manifest: Manifest,
|
|
26
|
+
hosts_file: Path = Path("/etc/hosts"),
|
|
27
|
+
domain: str = "redundanet.local",
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Initialize DNS manager.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
manifest: Network manifest
|
|
33
|
+
hosts_file: Path to hosts file
|
|
34
|
+
domain: Domain name for nodes
|
|
35
|
+
"""
|
|
36
|
+
self.manifest = manifest
|
|
37
|
+
self.hosts_file = hosts_file
|
|
38
|
+
self.domain = domain
|
|
39
|
+
self._resolution_cache: dict[str, str] = {}
|
|
40
|
+
|
|
41
|
+
def update_hosts_file(self) -> None:
|
|
42
|
+
"""Update /etc/hosts with node entries."""
|
|
43
|
+
logger.info("Updating hosts file", path=str(self.hosts_file))
|
|
44
|
+
|
|
45
|
+
# Read current hosts file
|
|
46
|
+
content = read_file(self.hosts_file) if self.hosts_file.exists() else ""
|
|
47
|
+
|
|
48
|
+
# Remove existing RedundaNet entries
|
|
49
|
+
lines = content.split("\n")
|
|
50
|
+
new_lines = []
|
|
51
|
+
in_redundanet_section = False
|
|
52
|
+
|
|
53
|
+
for line in lines:
|
|
54
|
+
if HOSTS_MARKER_START in line:
|
|
55
|
+
in_redundanet_section = True
|
|
56
|
+
continue
|
|
57
|
+
if HOSTS_MARKER_END in line:
|
|
58
|
+
in_redundanet_section = False
|
|
59
|
+
continue
|
|
60
|
+
if not in_redundanet_section:
|
|
61
|
+
new_lines.append(line)
|
|
62
|
+
|
|
63
|
+
# Add new RedundaNet entries
|
|
64
|
+
new_lines.append("")
|
|
65
|
+
new_lines.append(HOSTS_MARKER_START)
|
|
66
|
+
|
|
67
|
+
for node in self.manifest.nodes:
|
|
68
|
+
ip = node.vpn_ip or node.internal_ip
|
|
69
|
+
fqdn = f"{node.name}.{self.domain}"
|
|
70
|
+
new_lines.append(f"{ip} {node.name} {fqdn}")
|
|
71
|
+
self._resolution_cache[node.name] = ip
|
|
72
|
+
self._resolution_cache[fqdn] = ip
|
|
73
|
+
|
|
74
|
+
new_lines.append(HOSTS_MARKER_END)
|
|
75
|
+
new_lines.append("")
|
|
76
|
+
|
|
77
|
+
# Write updated hosts file
|
|
78
|
+
content = "\n".join(new_lines)
|
|
79
|
+
write_file(self.hosts_file, content, mode=0o644)
|
|
80
|
+
|
|
81
|
+
logger.info("Hosts file updated", entries=len(self.manifest.nodes))
|
|
82
|
+
|
|
83
|
+
def resolve_node(self, name: str) -> str | None:
|
|
84
|
+
"""Resolve a node name to IP address.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
name: Node name or FQDN
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
IP address or None
|
|
91
|
+
"""
|
|
92
|
+
# Check cache first
|
|
93
|
+
if name in self._resolution_cache:
|
|
94
|
+
return self._resolution_cache[name]
|
|
95
|
+
|
|
96
|
+
# Check FQDN
|
|
97
|
+
fqdn = f"{name}.{self.domain}"
|
|
98
|
+
if fqdn in self._resolution_cache:
|
|
99
|
+
return self._resolution_cache[fqdn]
|
|
100
|
+
|
|
101
|
+
# Look up in manifest
|
|
102
|
+
node = self.manifest.get_node(name)
|
|
103
|
+
if node:
|
|
104
|
+
ip = node.vpn_ip or node.internal_ip
|
|
105
|
+
self._resolution_cache[name] = ip
|
|
106
|
+
return ip
|
|
107
|
+
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def get_all_resolutions(self) -> dict[str, str]:
|
|
111
|
+
"""Get all node name to IP resolutions.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Dictionary mapping names to IPs
|
|
115
|
+
"""
|
|
116
|
+
resolutions = {}
|
|
117
|
+
|
|
118
|
+
for node in self.manifest.nodes:
|
|
119
|
+
ip = node.vpn_ip or node.internal_ip
|
|
120
|
+
resolutions[node.name] = ip
|
|
121
|
+
resolutions[f"{node.name}.{self.domain}"] = ip
|
|
122
|
+
|
|
123
|
+
return resolutions
|
|
124
|
+
|
|
125
|
+
def remove_hosts_entries(self) -> None:
|
|
126
|
+
"""Remove RedundaNet entries from hosts file."""
|
|
127
|
+
if not self.hosts_file.exists():
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
content = read_file(self.hosts_file)
|
|
131
|
+
|
|
132
|
+
# Remove RedundaNet section
|
|
133
|
+
lines = content.split("\n")
|
|
134
|
+
new_lines = []
|
|
135
|
+
in_redundanet_section = False
|
|
136
|
+
|
|
137
|
+
for line in lines:
|
|
138
|
+
if HOSTS_MARKER_START in line:
|
|
139
|
+
in_redundanet_section = True
|
|
140
|
+
continue
|
|
141
|
+
if HOSTS_MARKER_END in line:
|
|
142
|
+
in_redundanet_section = False
|
|
143
|
+
continue
|
|
144
|
+
if not in_redundanet_section:
|
|
145
|
+
new_lines.append(line)
|
|
146
|
+
|
|
147
|
+
content = "\n".join(new_lines)
|
|
148
|
+
write_file(self.hosts_file, content, mode=0o644)
|
|
149
|
+
|
|
150
|
+
self._resolution_cache.clear()
|
|
151
|
+
logger.info("Removed RedundaNet entries from hosts file")
|
|
152
|
+
|
|
153
|
+
def write_resolution_file(self, path: Path) -> None:
|
|
154
|
+
"""Write a separate resolution file for other scripts.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
path: Path to write resolution file
|
|
158
|
+
"""
|
|
159
|
+
lines = []
|
|
160
|
+
|
|
161
|
+
for node in self.manifest.nodes:
|
|
162
|
+
ip = node.vpn_ip or node.internal_ip
|
|
163
|
+
fqdn = f"{node.name}.{self.domain}"
|
|
164
|
+
lines.append(f"{ip} {node.name} {fqdn}")
|
|
165
|
+
|
|
166
|
+
content = "\n".join(lines) + "\n"
|
|
167
|
+
write_file(path, content, mode=0o644)
|
|
168
|
+
|
|
169
|
+
logger.debug("Wrote resolution file", path=str(path))
|
|
170
|
+
|
|
171
|
+
def validate_resolution(self, name: str) -> bool:
|
|
172
|
+
"""Validate that a name can be resolved.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
name: Name to validate
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
True if resolvable
|
|
179
|
+
"""
|
|
180
|
+
return self.resolve_node(name) is not None
|