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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""FURL management for Tahoe-LAFS introducers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from redundanet.core.exceptions import StorageError
|
|
9
|
+
from redundanet.utils.files import read_file, read_yaml, write_file, write_yaml
|
|
10
|
+
from redundanet.utils.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
# FURL format: pb://<tubid>@<location>/<swissnum>
|
|
15
|
+
FURL_PATTERN = re.compile(r"^pb://([a-z2-7]+)@([^/]+)/([a-z2-7]+)$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_furl(furl: str) -> dict[str, str]:
|
|
19
|
+
"""Parse a FURL into its components.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
furl: The FURL string
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Dictionary with tubid, location, and swissnum
|
|
26
|
+
"""
|
|
27
|
+
match = FURL_PATTERN.match(furl.strip())
|
|
28
|
+
if not match:
|
|
29
|
+
raise StorageError(f"Invalid FURL format: {furl}")
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
"tubid": match.group(1),
|
|
33
|
+
"location": match.group(2),
|
|
34
|
+
"swissnum": match.group(3),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate_furl(furl: str) -> bool:
|
|
39
|
+
"""Validate a FURL format.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
furl: The FURL string
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
True if valid
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
parse_furl(furl)
|
|
49
|
+
return True
|
|
50
|
+
except StorageError:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FURLManager:
|
|
55
|
+
"""Manages FURL distribution and synchronization."""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
manifest_path: Path,
|
|
60
|
+
cache_path: Path | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Initialize FURL manager.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
manifest_path: Path to the network manifest
|
|
66
|
+
cache_path: Path to cache the FURL locally
|
|
67
|
+
"""
|
|
68
|
+
self.manifest_path = manifest_path
|
|
69
|
+
self.cache_path = cache_path or Path("/var/lib/redundanet/introducer_furl.cache")
|
|
70
|
+
|
|
71
|
+
def get_cached_furl(self) -> str | None:
|
|
72
|
+
"""Get the cached FURL if available.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Cached FURL or None
|
|
76
|
+
"""
|
|
77
|
+
if self.cache_path.exists():
|
|
78
|
+
try:
|
|
79
|
+
return read_file(self.cache_path).strip()
|
|
80
|
+
except Exception:
|
|
81
|
+
return None
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def cache_furl(self, furl: str) -> None:
|
|
85
|
+
"""Cache a FURL locally.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
furl: FURL to cache
|
|
89
|
+
"""
|
|
90
|
+
if not validate_furl(furl):
|
|
91
|
+
raise StorageError(f"Cannot cache invalid FURL: {furl}")
|
|
92
|
+
|
|
93
|
+
self.cache_path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
write_file(self.cache_path, furl, mode=0o600)
|
|
95
|
+
logger.debug("Cached FURL", path=str(self.cache_path))
|
|
96
|
+
|
|
97
|
+
def get_furl_from_manifest(self) -> str | None:
|
|
98
|
+
"""Get the introducer FURL from the manifest.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
FURL from manifest or None
|
|
102
|
+
"""
|
|
103
|
+
if not self.manifest_path.exists():
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
data = read_yaml(self.manifest_path)
|
|
108
|
+
furl = data.get("introducer_furl")
|
|
109
|
+
if furl and furl != "null":
|
|
110
|
+
return str(furl)
|
|
111
|
+
return None
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error("Failed to read FURL from manifest", error=str(e))
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def update_manifest_furl(self, furl: str) -> None:
|
|
117
|
+
"""Update the FURL in the manifest.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
furl: New FURL to set
|
|
121
|
+
"""
|
|
122
|
+
if not validate_furl(furl):
|
|
123
|
+
raise StorageError(f"Cannot update manifest with invalid FURL: {furl}")
|
|
124
|
+
|
|
125
|
+
if not self.manifest_path.exists():
|
|
126
|
+
raise StorageError(f"Manifest not found: {self.manifest_path}")
|
|
127
|
+
|
|
128
|
+
data = read_yaml(self.manifest_path)
|
|
129
|
+
data["introducer_furl"] = furl
|
|
130
|
+
write_yaml(self.manifest_path, data)
|
|
131
|
+
|
|
132
|
+
logger.info("Updated manifest with FURL", furl=furl[:50] + "...")
|
|
133
|
+
|
|
134
|
+
def get_effective_furl(self) -> str | None:
|
|
135
|
+
"""Get the effective FURL to use.
|
|
136
|
+
|
|
137
|
+
Checks manifest first, then cache.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
FURL to use or None
|
|
141
|
+
"""
|
|
142
|
+
# First try manifest
|
|
143
|
+
furl = self.get_furl_from_manifest()
|
|
144
|
+
if furl:
|
|
145
|
+
# Update cache
|
|
146
|
+
self.cache_furl(furl)
|
|
147
|
+
return furl
|
|
148
|
+
|
|
149
|
+
# Fall back to cache
|
|
150
|
+
return self.get_cached_furl()
|
|
151
|
+
|
|
152
|
+
def sync_furl(self, local_furl: str | None = None) -> str | None:
|
|
153
|
+
"""Synchronize FURL between local and manifest.
|
|
154
|
+
|
|
155
|
+
If local_furl is provided (this is an introducer), update the manifest.
|
|
156
|
+
Otherwise, fetch FURL from manifest and cache it.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
local_furl: Local FURL if this is an introducer
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
The synchronized FURL
|
|
163
|
+
"""
|
|
164
|
+
if local_furl:
|
|
165
|
+
# This is an introducer - publish FURL to manifest
|
|
166
|
+
if validate_furl(local_furl):
|
|
167
|
+
self.update_manifest_furl(local_furl)
|
|
168
|
+
self.cache_furl(local_furl)
|
|
169
|
+
return local_furl
|
|
170
|
+
else:
|
|
171
|
+
raise StorageError(f"Invalid local FURL: {local_furl}")
|
|
172
|
+
else:
|
|
173
|
+
# This is a client/storage - get FURL from manifest
|
|
174
|
+
furl = self.get_effective_furl()
|
|
175
|
+
if furl:
|
|
176
|
+
self.cache_furl(furl)
|
|
177
|
+
return furl
|
|
178
|
+
|
|
179
|
+
def extract_introducer_ip(self) -> str | None:
|
|
180
|
+
"""Extract the introducer IP from the FURL.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
IP address of the introducer or None
|
|
184
|
+
"""
|
|
185
|
+
furl = self.get_effective_furl()
|
|
186
|
+
if not furl:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
parsed = parse_furl(furl)
|
|
191
|
+
location = parsed["location"]
|
|
192
|
+
# Location format: ip:port or hostname:port
|
|
193
|
+
host = location.split(":")[0]
|
|
194
|
+
return host
|
|
195
|
+
except Exception:
|
|
196
|
+
return None
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Tahoe-LAFS introducer operations for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from jinja2 import Template
|
|
9
|
+
|
|
10
|
+
from redundanet.core.exceptions import StorageError
|
|
11
|
+
from redundanet.utils.files import ensure_dir, read_file, write_file
|
|
12
|
+
from redundanet.utils.logging import get_logger
|
|
13
|
+
from redundanet.utils.process import is_command_available, run_command
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
TAHOE_INTRODUCER_CFG_TEMPLATE = """# Tahoe-LAFS introducer configuration
|
|
18
|
+
# Generated by RedundaNet
|
|
19
|
+
|
|
20
|
+
[node]
|
|
21
|
+
nickname = {{ nickname }}
|
|
22
|
+
web.port = tcp:{{ web_port }}:interface=0.0.0.0
|
|
23
|
+
tub.port = tcp:{{ tub_port }}
|
|
24
|
+
tub.location = {{ tub_location }}
|
|
25
|
+
|
|
26
|
+
[introducer]
|
|
27
|
+
# This node is an introducer
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class TahoeIntroducerConfig:
|
|
33
|
+
"""Configuration for Tahoe-LAFS introducer."""
|
|
34
|
+
|
|
35
|
+
nickname: str
|
|
36
|
+
node_dir: Path
|
|
37
|
+
vpn_ip: str
|
|
38
|
+
web_port: int = 3458
|
|
39
|
+
tub_port: int = 3458
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def tub_location(self) -> str:
|
|
43
|
+
"""Get the tub location for this introducer."""
|
|
44
|
+
return f"tcp:{self.vpn_ip}:{self.tub_port}"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TahoeIntroducer:
|
|
48
|
+
"""Manages Tahoe-LAFS introducer node."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, config: TahoeIntroducerConfig) -> None:
|
|
51
|
+
self.config = config
|
|
52
|
+
self._tahoe_cfg = config.node_dir / "tahoe.cfg"
|
|
53
|
+
self._furl_file = config.node_dir / "private" / "introducer.furl"
|
|
54
|
+
|
|
55
|
+
def ensure_tahoe_installed(self) -> bool:
|
|
56
|
+
"""Check if Tahoe-LAFS is installed."""
|
|
57
|
+
if not is_command_available("tahoe"):
|
|
58
|
+
raise StorageError("Tahoe-LAFS is not installed.")
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
def create_node(self) -> str:
|
|
62
|
+
"""Create a new Tahoe-LAFS introducer node.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The introducer FURL
|
|
66
|
+
"""
|
|
67
|
+
logger.info("Creating Tahoe introducer", nickname=self.config.nickname)
|
|
68
|
+
|
|
69
|
+
self.ensure_tahoe_installed()
|
|
70
|
+
ensure_dir(self.config.node_dir, mode=0o700)
|
|
71
|
+
|
|
72
|
+
# Create the introducer (nickname is set in tahoe.cfg, not command line)
|
|
73
|
+
# Use --listen=none initially, we'll configure the proper settings in tahoe.cfg
|
|
74
|
+
result = run_command(
|
|
75
|
+
f"tahoe create-introducer --listen=none {self.config.node_dir}",
|
|
76
|
+
check=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not result.success and "already exists" not in result.stderr:
|
|
80
|
+
raise StorageError(f"Failed to create introducer: {result.stderr}")
|
|
81
|
+
|
|
82
|
+
# Write custom configuration
|
|
83
|
+
self._write_config()
|
|
84
|
+
|
|
85
|
+
# Start the introducer to generate the FURL
|
|
86
|
+
self.start()
|
|
87
|
+
|
|
88
|
+
# Wait for FURL to be generated
|
|
89
|
+
import time
|
|
90
|
+
|
|
91
|
+
for _ in range(30):
|
|
92
|
+
if self._furl_file.exists():
|
|
93
|
+
break
|
|
94
|
+
time.sleep(1)
|
|
95
|
+
|
|
96
|
+
if not self._furl_file.exists():
|
|
97
|
+
raise StorageError("FURL file not generated after starting introducer")
|
|
98
|
+
|
|
99
|
+
furl = self.get_furl()
|
|
100
|
+
logger.info("Tahoe introducer created", furl=furl[:50] + "...")
|
|
101
|
+
|
|
102
|
+
return furl
|
|
103
|
+
|
|
104
|
+
def _write_config(self) -> None:
|
|
105
|
+
"""Write tahoe.cfg configuration file."""
|
|
106
|
+
template = Template(TAHOE_INTRODUCER_CFG_TEMPLATE)
|
|
107
|
+
content = template.render(
|
|
108
|
+
nickname=self.config.nickname,
|
|
109
|
+
web_port=self.config.web_port,
|
|
110
|
+
tub_port=self.config.tub_port,
|
|
111
|
+
tub_location=self.config.tub_location,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
write_file(self._tahoe_cfg, content, mode=0o600)
|
|
115
|
+
logger.debug("Wrote tahoe.cfg", path=str(self._tahoe_cfg))
|
|
116
|
+
|
|
117
|
+
def start(self) -> bool:
|
|
118
|
+
"""Start the Tahoe introducer."""
|
|
119
|
+
logger.info("Starting Tahoe introducer", nickname=self.config.nickname)
|
|
120
|
+
|
|
121
|
+
result = run_command(f"tahoe start {self.config.node_dir}", check=False)
|
|
122
|
+
|
|
123
|
+
if not result.success:
|
|
124
|
+
logger.error("Failed to start Tahoe introducer", error=result.stderr)
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
logger.info("Tahoe introducer started")
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def stop(self) -> bool:
|
|
131
|
+
"""Stop the Tahoe introducer."""
|
|
132
|
+
logger.info("Stopping Tahoe introducer", nickname=self.config.nickname)
|
|
133
|
+
|
|
134
|
+
result = run_command(f"tahoe stop {self.config.node_dir}", check=False)
|
|
135
|
+
|
|
136
|
+
if not result.success:
|
|
137
|
+
logger.error("Failed to stop Tahoe introducer", error=result.stderr)
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
logger.info("Tahoe introducer stopped")
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
def restart(self) -> bool:
|
|
144
|
+
"""Restart the Tahoe introducer."""
|
|
145
|
+
result = run_command(f"tahoe restart {self.config.node_dir}", check=False)
|
|
146
|
+
return result.success
|
|
147
|
+
|
|
148
|
+
def is_running(self) -> bool:
|
|
149
|
+
"""Check if the introducer is running."""
|
|
150
|
+
result = run_command(f"tahoe status {self.config.node_dir}", check=False)
|
|
151
|
+
return result.success and "RUNNING" in result.stdout
|
|
152
|
+
|
|
153
|
+
def get_furl(self) -> str:
|
|
154
|
+
"""Get the introducer FURL.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
The introducer FURL string
|
|
158
|
+
"""
|
|
159
|
+
if not self._furl_file.exists():
|
|
160
|
+
raise StorageError(f"FURL file not found: {self._furl_file}")
|
|
161
|
+
|
|
162
|
+
return read_file(self._furl_file).strip()
|
|
163
|
+
|
|
164
|
+
def get_status(self) -> dict[str, object]:
|
|
165
|
+
"""Get introducer status."""
|
|
166
|
+
status: dict[str, object] = {
|
|
167
|
+
"running": self.is_running(),
|
|
168
|
+
"node_dir": str(self.config.node_dir),
|
|
169
|
+
"web_url": f"http://{self.config.vpn_ip}:{self.config.web_port}",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if self._furl_file.exists():
|
|
173
|
+
status["furl"] = self.get_furl()
|
|
174
|
+
|
|
175
|
+
return status
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Tahoe-LAFS storage node operations for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from jinja2 import Template
|
|
9
|
+
|
|
10
|
+
from redundanet.core.config import TahoeConfig
|
|
11
|
+
from redundanet.core.exceptions import StorageError
|
|
12
|
+
from redundanet.utils.files import ensure_dir, write_file
|
|
13
|
+
from redundanet.utils.logging import get_logger
|
|
14
|
+
from redundanet.utils.process import is_command_available, run_command
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
TAHOE_STORAGE_CFG_TEMPLATE = """# Tahoe-LAFS storage node configuration
|
|
19
|
+
# Generated by RedundaNet
|
|
20
|
+
|
|
21
|
+
[node]
|
|
22
|
+
nickname = {{ nickname }}
|
|
23
|
+
web.port = tcp:{{ web_port }}:interface=127.0.0.1
|
|
24
|
+
tub.port = tcp:{{ tub_port }}
|
|
25
|
+
tub.location = {{ tub_location }}
|
|
26
|
+
|
|
27
|
+
[client]
|
|
28
|
+
introducer.furl = {{ introducer_furl }}
|
|
29
|
+
shares.needed = {{ shares_needed }}
|
|
30
|
+
shares.happy = {{ shares_happy }}
|
|
31
|
+
shares.total = {{ shares_total }}
|
|
32
|
+
|
|
33
|
+
[storage]
|
|
34
|
+
enabled = true
|
|
35
|
+
reserved_space = {{ reserved_space }}
|
|
36
|
+
{% if storage_dir %}storage_dir = {{ storage_dir }}{% endif %}
|
|
37
|
+
|
|
38
|
+
[helper]
|
|
39
|
+
enabled = false
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class TahoeStorageConfig:
|
|
45
|
+
"""Configuration for Tahoe-LAFS storage node."""
|
|
46
|
+
|
|
47
|
+
nickname: str
|
|
48
|
+
node_dir: Path
|
|
49
|
+
introducer_furl: str
|
|
50
|
+
reserved_space: str = "50G"
|
|
51
|
+
storage_dir: Path | None = None
|
|
52
|
+
web_port: int = 3457
|
|
53
|
+
tub_port: int = 34570
|
|
54
|
+
tub_location: str = "AUTO"
|
|
55
|
+
shares_needed: int = 3
|
|
56
|
+
shares_happy: int = 7
|
|
57
|
+
shares_total: int = 10
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_tahoe_config(
|
|
61
|
+
cls,
|
|
62
|
+
nickname: str,
|
|
63
|
+
introducer_furl: str,
|
|
64
|
+
tahoe_config: TahoeConfig,
|
|
65
|
+
node_dir: Path | None = None,
|
|
66
|
+
storage_dir: Path | None = None,
|
|
67
|
+
) -> TahoeStorageConfig:
|
|
68
|
+
"""Create storage config from TahoeConfig."""
|
|
69
|
+
return cls(
|
|
70
|
+
nickname=nickname,
|
|
71
|
+
node_dir=node_dir or Path("/var/lib/tahoe-storage"),
|
|
72
|
+
introducer_furl=introducer_furl,
|
|
73
|
+
reserved_space=tahoe_config.reserved_space,
|
|
74
|
+
storage_dir=storage_dir,
|
|
75
|
+
shares_needed=tahoe_config.shares_needed,
|
|
76
|
+
shares_happy=tahoe_config.shares_happy,
|
|
77
|
+
shares_total=tahoe_config.shares_total,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TahoeStorage:
|
|
82
|
+
"""Manages Tahoe-LAFS storage node."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, config: TahoeStorageConfig) -> None:
|
|
85
|
+
self.config = config
|
|
86
|
+
self._tahoe_cfg = config.node_dir / "tahoe.cfg"
|
|
87
|
+
|
|
88
|
+
def ensure_tahoe_installed(self) -> bool:
|
|
89
|
+
"""Check if Tahoe-LAFS is installed."""
|
|
90
|
+
if not is_command_available("tahoe"):
|
|
91
|
+
raise StorageError("Tahoe-LAFS is not installed.")
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def create_node(self) -> None:
|
|
95
|
+
"""Create a new Tahoe-LAFS storage node."""
|
|
96
|
+
logger.info("Creating Tahoe storage node", nickname=self.config.nickname)
|
|
97
|
+
|
|
98
|
+
self.ensure_tahoe_installed()
|
|
99
|
+
ensure_dir(self.config.node_dir, mode=0o700)
|
|
100
|
+
|
|
101
|
+
if self.config.storage_dir:
|
|
102
|
+
ensure_dir(self.config.storage_dir, mode=0o700)
|
|
103
|
+
|
|
104
|
+
# Create the node
|
|
105
|
+
result = run_command(
|
|
106
|
+
f"tahoe create-node --basedir={self.config.node_dir} "
|
|
107
|
+
f"--nickname={self.config.nickname}",
|
|
108
|
+
check=False,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not result.success and "already exists" not in result.stderr:
|
|
112
|
+
raise StorageError(f"Failed to create storage node: {result.stderr}")
|
|
113
|
+
|
|
114
|
+
# Write custom configuration
|
|
115
|
+
self._write_config()
|
|
116
|
+
|
|
117
|
+
logger.info("Tahoe storage node created", node_dir=str(self.config.node_dir))
|
|
118
|
+
|
|
119
|
+
def _write_config(self) -> None:
|
|
120
|
+
"""Write tahoe.cfg configuration file."""
|
|
121
|
+
template = Template(TAHOE_STORAGE_CFG_TEMPLATE)
|
|
122
|
+
content = template.render(
|
|
123
|
+
nickname=self.config.nickname,
|
|
124
|
+
web_port=self.config.web_port,
|
|
125
|
+
tub_port=self.config.tub_port,
|
|
126
|
+
tub_location=self.config.tub_location,
|
|
127
|
+
introducer_furl=self.config.introducer_furl,
|
|
128
|
+
reserved_space=self.config.reserved_space,
|
|
129
|
+
storage_dir=str(self.config.storage_dir) if self.config.storage_dir else None,
|
|
130
|
+
shares_needed=self.config.shares_needed,
|
|
131
|
+
shares_happy=self.config.shares_happy,
|
|
132
|
+
shares_total=self.config.shares_total,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
write_file(self._tahoe_cfg, content, mode=0o600)
|
|
136
|
+
logger.debug("Wrote tahoe.cfg", path=str(self._tahoe_cfg))
|
|
137
|
+
|
|
138
|
+
def start(self) -> bool:
|
|
139
|
+
"""Start the Tahoe storage node."""
|
|
140
|
+
logger.info("Starting Tahoe storage", nickname=self.config.nickname)
|
|
141
|
+
|
|
142
|
+
result = run_command(f"tahoe start {self.config.node_dir}", check=False)
|
|
143
|
+
|
|
144
|
+
if not result.success:
|
|
145
|
+
logger.error("Failed to start Tahoe storage", error=result.stderr)
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
logger.info("Tahoe storage started")
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
def stop(self) -> bool:
|
|
152
|
+
"""Stop the Tahoe storage node."""
|
|
153
|
+
logger.info("Stopping Tahoe storage", nickname=self.config.nickname)
|
|
154
|
+
|
|
155
|
+
result = run_command(f"tahoe stop {self.config.node_dir}", check=False)
|
|
156
|
+
|
|
157
|
+
if not result.success:
|
|
158
|
+
logger.error("Failed to stop Tahoe storage", error=result.stderr)
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
logger.info("Tahoe storage stopped")
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def restart(self) -> bool:
|
|
165
|
+
"""Restart the Tahoe storage node."""
|
|
166
|
+
result = run_command(f"tahoe restart {self.config.node_dir}", check=False)
|
|
167
|
+
return result.success
|
|
168
|
+
|
|
169
|
+
def is_running(self) -> bool:
|
|
170
|
+
"""Check if the Tahoe storage node is running."""
|
|
171
|
+
result = run_command(f"tahoe status {self.config.node_dir}", check=False)
|
|
172
|
+
return result.success and "RUNNING" in result.stdout
|
|
173
|
+
|
|
174
|
+
def get_stats(self) -> dict[str, object]:
|
|
175
|
+
"""Get storage statistics."""
|
|
176
|
+
stats: dict[str, object] = {
|
|
177
|
+
"running": self.is_running(),
|
|
178
|
+
"node_dir": str(self.config.node_dir),
|
|
179
|
+
"reserved_space": self.config.reserved_space,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Try to get disk usage
|
|
183
|
+
storage_path = self.config.storage_dir or (self.config.node_dir / "storage" / "shares")
|
|
184
|
+
if storage_path.exists():
|
|
185
|
+
result = run_command(f"du -sh {storage_path}", check=False)
|
|
186
|
+
if result.success:
|
|
187
|
+
stats["used_space"] = result.stdout.split()[0]
|
|
188
|
+
|
|
189
|
+
return stats
|
|
190
|
+
|
|
191
|
+
def update_introducer_furl(self, furl: str) -> None:
|
|
192
|
+
"""Update the introducer FURL in the configuration."""
|
|
193
|
+
self.config.introducer_furl = furl
|
|
194
|
+
self._write_config()
|
|
195
|
+
logger.info("Updated introducer FURL")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Utility functions for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from redundanet.utils.files import ensure_dir, read_yaml, write_yaml
|
|
4
|
+
from redundanet.utils.logging import get_logger, setup_logging
|
|
5
|
+
from redundanet.utils.process import run_command, run_command_async
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ensure_dir",
|
|
9
|
+
"get_logger",
|
|
10
|
+
"read_yaml",
|
|
11
|
+
"run_command",
|
|
12
|
+
"run_command_async",
|
|
13
|
+
"setup_logging",
|
|
14
|
+
"write_yaml",
|
|
15
|
+
]
|