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.
@@ -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
+ ]