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/cli/node.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Node management CLI commands for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from redundanet.core.config import NodeRole, NodeStatus
|
|
13
|
+
from redundanet.core.manifest import Manifest
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Node management commands")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("list")
|
|
20
|
+
def list_nodes(
|
|
21
|
+
manifest_path: Annotated[
|
|
22
|
+
Optional[Path],
|
|
23
|
+
typer.Option("--manifest", "-m", help="Path to manifest file"),
|
|
24
|
+
] = None,
|
|
25
|
+
role: Annotated[
|
|
26
|
+
Optional[str],
|
|
27
|
+
typer.Option("--role", "-r", help="Filter by role"),
|
|
28
|
+
] = None,
|
|
29
|
+
status: Annotated[
|
|
30
|
+
Optional[str],
|
|
31
|
+
typer.Option("--status", "-s", help="Filter by status"),
|
|
32
|
+
] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""List all nodes in the network."""
|
|
35
|
+
if manifest_path is None:
|
|
36
|
+
from redundanet.core.config import get_default_manifest_path
|
|
37
|
+
|
|
38
|
+
manifest_path = get_default_manifest_path()
|
|
39
|
+
|
|
40
|
+
if not manifest_path.exists():
|
|
41
|
+
console.print(f"[red]Error:[/red] Manifest not found: {manifest_path}")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
|
|
44
|
+
manifest = Manifest.from_file(manifest_path)
|
|
45
|
+
nodes = manifest.nodes
|
|
46
|
+
|
|
47
|
+
# Apply filters
|
|
48
|
+
if role:
|
|
49
|
+
nodes = [n for n in nodes if role in [r.value for r in n.roles]]
|
|
50
|
+
if status:
|
|
51
|
+
nodes = [n for n in nodes if n.status.value == status]
|
|
52
|
+
|
|
53
|
+
# Create table
|
|
54
|
+
table = Table(title=f"RedundaNet Nodes ({len(nodes)})")
|
|
55
|
+
table.add_column("Name", style="cyan")
|
|
56
|
+
table.add_column("VPN IP", style="green")
|
|
57
|
+
table.add_column("Status")
|
|
58
|
+
table.add_column("Roles")
|
|
59
|
+
table.add_column("Region")
|
|
60
|
+
|
|
61
|
+
for node in nodes:
|
|
62
|
+
status_style = {
|
|
63
|
+
"active": "[green]active[/green]",
|
|
64
|
+
"pending": "[yellow]pending[/yellow]",
|
|
65
|
+
"inactive": "[red]inactive[/red]",
|
|
66
|
+
}.get(node.status.value, node.status.value)
|
|
67
|
+
|
|
68
|
+
roles_str = ", ".join(r.value for r in node.roles)
|
|
69
|
+
|
|
70
|
+
table.add_row(
|
|
71
|
+
node.name,
|
|
72
|
+
node.vpn_ip or node.internal_ip,
|
|
73
|
+
status_style,
|
|
74
|
+
roles_str or "[dim]none[/dim]",
|
|
75
|
+
node.region or "[dim]unknown[/dim]",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
console.print(table)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command("info")
|
|
82
|
+
def node_info(
|
|
83
|
+
node_name: Annotated[str, typer.Argument(help="Name of the node")],
|
|
84
|
+
manifest_path: Annotated[
|
|
85
|
+
Optional[Path],
|
|
86
|
+
typer.Option("--manifest", "-m", help="Path to manifest file"),
|
|
87
|
+
] = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Show detailed information about a node."""
|
|
90
|
+
if manifest_path is None:
|
|
91
|
+
from redundanet.core.config import get_default_manifest_path
|
|
92
|
+
|
|
93
|
+
manifest_path = get_default_manifest_path()
|
|
94
|
+
|
|
95
|
+
if not manifest_path.exists():
|
|
96
|
+
console.print(f"[red]Error:[/red] Manifest not found: {manifest_path}")
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
|
|
99
|
+
manifest = Manifest.from_file(manifest_path)
|
|
100
|
+
node = manifest.get_node(node_name)
|
|
101
|
+
|
|
102
|
+
if node is None:
|
|
103
|
+
console.print(f"[red]Error:[/red] Node '{node_name}' not found")
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
|
|
106
|
+
# Display node info
|
|
107
|
+
table = Table(title=f"Node: {node.name}", show_header=False, box=None)
|
|
108
|
+
table.add_column("Property", style="cyan")
|
|
109
|
+
table.add_column("Value")
|
|
110
|
+
|
|
111
|
+
table.add_row("Name", node.name)
|
|
112
|
+
table.add_row("Internal IP", node.internal_ip)
|
|
113
|
+
table.add_row("VPN IP", node.vpn_ip or "[dim]same as internal[/dim]")
|
|
114
|
+
table.add_row("Public IP", node.public_ip or "[dim]not set[/dim]")
|
|
115
|
+
table.add_row("Status", node.status.value)
|
|
116
|
+
table.add_row("Region", node.region or "[dim]unknown[/dim]")
|
|
117
|
+
table.add_row("GPG Key ID", node.gpg_key_id or "[dim]not set[/dim]")
|
|
118
|
+
table.add_row("Publicly Accessible", "Yes" if node.is_publicly_accessible else "No")
|
|
119
|
+
table.add_row("Roles", ", ".join(r.value for r in node.roles) or "[dim]none[/dim]")
|
|
120
|
+
table.add_row("Storage Contribution", node.storage_contribution or "[dim]none[/dim]")
|
|
121
|
+
table.add_row("Storage Allocation", node.storage_allocation or "[dim]none[/dim]")
|
|
122
|
+
|
|
123
|
+
# Ports
|
|
124
|
+
table.add_row("", "")
|
|
125
|
+
table.add_row("[bold]Ports[/bold]", "")
|
|
126
|
+
table.add_row(" Tinc", str(node.ports.tinc))
|
|
127
|
+
table.add_row(" Tahoe Storage", str(node.ports.tahoe_storage))
|
|
128
|
+
table.add_row(" Tahoe Client", str(node.ports.tahoe_client))
|
|
129
|
+
table.add_row(" Tahoe Introducer", str(node.ports.tahoe_introducer))
|
|
130
|
+
|
|
131
|
+
console.print(table)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.command("add")
|
|
135
|
+
def add_node(
|
|
136
|
+
name: Annotated[str, typer.Option("--name", "-n", prompt=True, help="Node name")],
|
|
137
|
+
internal_ip: Annotated[
|
|
138
|
+
str,
|
|
139
|
+
typer.Option("--ip", "-i", prompt=True, help="Internal/VPN IP address"),
|
|
140
|
+
],
|
|
141
|
+
public_ip: Annotated[
|
|
142
|
+
Optional[str],
|
|
143
|
+
typer.Option("--public-ip", "-p", help="Public IP address"),
|
|
144
|
+
] = None,
|
|
145
|
+
gpg_key_id: Annotated[
|
|
146
|
+
Optional[str],
|
|
147
|
+
typer.Option("--gpg-key", "-g", help="GPG key ID"),
|
|
148
|
+
] = None,
|
|
149
|
+
region: Annotated[
|
|
150
|
+
Optional[str],
|
|
151
|
+
typer.Option("--region", "-r", help="Geographic region"),
|
|
152
|
+
] = None,
|
|
153
|
+
roles: Annotated[
|
|
154
|
+
Optional[list[str]],
|
|
155
|
+
typer.Option("--role", help="Node roles (can be specified multiple times)"),
|
|
156
|
+
] = None,
|
|
157
|
+
storage: Annotated[
|
|
158
|
+
Optional[str],
|
|
159
|
+
typer.Option("--storage", "-s", help="Storage contribution"),
|
|
160
|
+
] = None,
|
|
161
|
+
publicly_accessible: Annotated[
|
|
162
|
+
bool,
|
|
163
|
+
typer.Option("--public", help="Node is publicly accessible"),
|
|
164
|
+
] = False,
|
|
165
|
+
manifest_path: Annotated[
|
|
166
|
+
Optional[Path],
|
|
167
|
+
typer.Option("--manifest", "-m", help="Path to manifest file"),
|
|
168
|
+
] = None,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Add a new node to the manifest."""
|
|
171
|
+
from redundanet.core.config import NodeConfig, PortConfig
|
|
172
|
+
|
|
173
|
+
if manifest_path is None:
|
|
174
|
+
from redundanet.core.config import get_default_manifest_path
|
|
175
|
+
|
|
176
|
+
manifest_path = get_default_manifest_path()
|
|
177
|
+
|
|
178
|
+
if not manifest_path.exists():
|
|
179
|
+
console.print(f"[red]Error:[/red] Manifest not found: {manifest_path}")
|
|
180
|
+
console.print("Create a new manifest first or specify a valid path.")
|
|
181
|
+
raise typer.Exit(1)
|
|
182
|
+
|
|
183
|
+
manifest = Manifest.from_file(manifest_path)
|
|
184
|
+
|
|
185
|
+
# Check if node already exists
|
|
186
|
+
if manifest.get_node(name):
|
|
187
|
+
console.print(f"[red]Error:[/red] Node '{name}' already exists")
|
|
188
|
+
raise typer.Exit(1)
|
|
189
|
+
|
|
190
|
+
# Parse roles
|
|
191
|
+
node_roles = []
|
|
192
|
+
if roles:
|
|
193
|
+
for role in roles:
|
|
194
|
+
try:
|
|
195
|
+
node_roles.append(NodeRole(role))
|
|
196
|
+
except ValueError:
|
|
197
|
+
valid_roles = [r.value for r in NodeRole]
|
|
198
|
+
console.print(f"[red]Error:[/red] Invalid role '{role}'")
|
|
199
|
+
console.print(f"Valid roles: {', '.join(valid_roles)}")
|
|
200
|
+
raise typer.Exit(1) from None
|
|
201
|
+
|
|
202
|
+
# Create node config
|
|
203
|
+
node = NodeConfig(
|
|
204
|
+
name=name,
|
|
205
|
+
internal_ip=internal_ip,
|
|
206
|
+
vpn_ip=internal_ip,
|
|
207
|
+
public_ip=public_ip,
|
|
208
|
+
gpg_key_id=gpg_key_id,
|
|
209
|
+
region=region,
|
|
210
|
+
status=NodeStatus.PENDING,
|
|
211
|
+
roles=node_roles,
|
|
212
|
+
ports=PortConfig(),
|
|
213
|
+
storage_contribution=storage,
|
|
214
|
+
is_publicly_accessible=publicly_accessible,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Add to manifest
|
|
218
|
+
manifest.add_node(node)
|
|
219
|
+
manifest.save(manifest_path)
|
|
220
|
+
|
|
221
|
+
console.print(f"[green]Added node '{name}' to manifest[/green]")
|
|
222
|
+
console.print(f"Manifest saved to: {manifest_path}")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@app.command("remove")
|
|
226
|
+
def remove_node(
|
|
227
|
+
node_name: Annotated[str, typer.Argument(help="Name of the node to remove")],
|
|
228
|
+
manifest_path: Annotated[
|
|
229
|
+
Optional[Path],
|
|
230
|
+
typer.Option("--manifest", "-m", help="Path to manifest file"),
|
|
231
|
+
] = None,
|
|
232
|
+
force: Annotated[
|
|
233
|
+
bool,
|
|
234
|
+
typer.Option("--force", "-f", help="Skip confirmation"),
|
|
235
|
+
] = False,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Remove a node from the manifest."""
|
|
238
|
+
if manifest_path is None:
|
|
239
|
+
from redundanet.core.config import get_default_manifest_path
|
|
240
|
+
|
|
241
|
+
manifest_path = get_default_manifest_path()
|
|
242
|
+
|
|
243
|
+
if not manifest_path.exists():
|
|
244
|
+
console.print(f"[red]Error:[/red] Manifest not found: {manifest_path}")
|
|
245
|
+
raise typer.Exit(1)
|
|
246
|
+
|
|
247
|
+
manifest = Manifest.from_file(manifest_path)
|
|
248
|
+
|
|
249
|
+
if not manifest.get_node(node_name):
|
|
250
|
+
console.print(f"[red]Error:[/red] Node '{node_name}' not found")
|
|
251
|
+
raise typer.Exit(1)
|
|
252
|
+
|
|
253
|
+
if not force:
|
|
254
|
+
confirm = typer.confirm(f"Are you sure you want to remove node '{node_name}'?")
|
|
255
|
+
if not confirm:
|
|
256
|
+
console.print("Aborted.")
|
|
257
|
+
raise typer.Exit(0)
|
|
258
|
+
|
|
259
|
+
manifest.remove_node(node_name)
|
|
260
|
+
manifest.save(manifest_path)
|
|
261
|
+
|
|
262
|
+
console.print(f"[green]Removed node '{node_name}' from manifest[/green]")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@app.command("keys")
|
|
266
|
+
def manage_keys(
|
|
267
|
+
action: Annotated[
|
|
268
|
+
str,
|
|
269
|
+
typer.Argument(help="Action: generate, export, import"),
|
|
270
|
+
],
|
|
271
|
+
node_name: Annotated[
|
|
272
|
+
Optional[str],
|
|
273
|
+
typer.Option("--name", "-n", help="Node name"),
|
|
274
|
+
] = None,
|
|
275
|
+
) -> None:
|
|
276
|
+
"""Manage GPG keys for node authentication."""
|
|
277
|
+
from redundanet.core.config import load_settings
|
|
278
|
+
|
|
279
|
+
settings = load_settings()
|
|
280
|
+
node_name = node_name or settings.node_name
|
|
281
|
+
|
|
282
|
+
if not node_name:
|
|
283
|
+
console.print("[red]Error:[/red] No node name specified")
|
|
284
|
+
console.print("Use --name or set REDUNDANET_NODE_NAME")
|
|
285
|
+
raise typer.Exit(1)
|
|
286
|
+
|
|
287
|
+
if action == "generate":
|
|
288
|
+
console.print(f"[bold]Generating GPG key for node: {node_name}[/bold]")
|
|
289
|
+
console.print("[yellow]Note:[/yellow] This will create a new GPG keypair.")
|
|
290
|
+
console.print("Use 'redundanet node keys export' to share your public key.")
|
|
291
|
+
# In a real implementation, we'd call the GPG module
|
|
292
|
+
console.print("[dim]GPG key generation not yet implemented[/dim]")
|
|
293
|
+
|
|
294
|
+
elif action == "export":
|
|
295
|
+
console.print(f"[bold]Exporting public key for node: {node_name}[/bold]")
|
|
296
|
+
console.print("[dim]Key export not yet implemented[/dim]")
|
|
297
|
+
|
|
298
|
+
elif action == "import":
|
|
299
|
+
console.print("[bold]Importing public key[/bold]")
|
|
300
|
+
console.print("[dim]Key import not yet implemented[/dim]")
|
|
301
|
+
|
|
302
|
+
else:
|
|
303
|
+
console.print(f"[red]Error:[/red] Unknown action '{action}'")
|
|
304
|
+
console.print("Valid actions: generate, export, import")
|
|
305
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Storage management CLI commands for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Storage management commands")
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("status")
|
|
19
|
+
def storage_status(
|
|
20
|
+
verbose: Annotated[
|
|
21
|
+
bool,
|
|
22
|
+
typer.Option("--verbose", "-v", help="Show detailed status"),
|
|
23
|
+
] = False,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Show storage status and statistics."""
|
|
26
|
+
console.print(Panel("[bold]Storage Status[/bold]", expand=False))
|
|
27
|
+
|
|
28
|
+
# Storage overview
|
|
29
|
+
table = Table(title="Storage Overview", show_header=True)
|
|
30
|
+
table.add_column("Metric", style="cyan")
|
|
31
|
+
table.add_column("Value", style="green")
|
|
32
|
+
|
|
33
|
+
table.add_row("Contribution", "[dim]Not configured[/dim]")
|
|
34
|
+
table.add_row("Allocation", "[dim]Not configured[/dim]")
|
|
35
|
+
table.add_row("Used", "[dim]Unknown[/dim]")
|
|
36
|
+
table.add_row("Available", "[dim]Unknown[/dim]")
|
|
37
|
+
|
|
38
|
+
console.print(table)
|
|
39
|
+
|
|
40
|
+
if verbose:
|
|
41
|
+
# Tahoe-LAFS status
|
|
42
|
+
console.print("\n[bold]Tahoe-LAFS Configuration:[/bold]")
|
|
43
|
+
tahoe_table = Table(show_header=False)
|
|
44
|
+
tahoe_table.add_column("Property", style="cyan")
|
|
45
|
+
tahoe_table.add_column("Value")
|
|
46
|
+
|
|
47
|
+
tahoe_table.add_row("Shares Needed (k)", "[dim]3[/dim]")
|
|
48
|
+
tahoe_table.add_row("Shares Happy", "[dim]7[/dim]")
|
|
49
|
+
tahoe_table.add_row("Shares Total (n)", "[dim]10[/dim]")
|
|
50
|
+
tahoe_table.add_row("Introducer FURL", "[dim]Not set[/dim]")
|
|
51
|
+
|
|
52
|
+
console.print(tahoe_table)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command("start")
|
|
56
|
+
def storage_start(
|
|
57
|
+
client: Annotated[
|
|
58
|
+
bool,
|
|
59
|
+
typer.Option("--client", "-c", help="Start client service"),
|
|
60
|
+
] = True,
|
|
61
|
+
storage: Annotated[
|
|
62
|
+
bool,
|
|
63
|
+
typer.Option("--storage", "-s", help="Start storage service"),
|
|
64
|
+
] = True,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Start storage services."""
|
|
67
|
+
services = []
|
|
68
|
+
if client:
|
|
69
|
+
services.append("client")
|
|
70
|
+
if storage:
|
|
71
|
+
services.append("storage")
|
|
72
|
+
|
|
73
|
+
if not services:
|
|
74
|
+
console.print("[yellow]No services specified[/yellow]")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
with Progress(
|
|
78
|
+
SpinnerColumn(),
|
|
79
|
+
TextColumn("[progress.description]{task.description}"),
|
|
80
|
+
console=console,
|
|
81
|
+
) as progress:
|
|
82
|
+
for service in services:
|
|
83
|
+
task = progress.add_task(f"Starting {service}...", total=None)
|
|
84
|
+
# In a real implementation, we'd start the service
|
|
85
|
+
progress.update(task, completed=True)
|
|
86
|
+
|
|
87
|
+
console.print(f"[green]Started services: {', '.join(services)}[/green]")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command("stop")
|
|
91
|
+
def storage_stop(
|
|
92
|
+
client: Annotated[
|
|
93
|
+
bool,
|
|
94
|
+
typer.Option("--client", "-c", help="Stop client service"),
|
|
95
|
+
] = True,
|
|
96
|
+
storage: Annotated[
|
|
97
|
+
bool,
|
|
98
|
+
typer.Option("--storage", "-s", help="Stop storage service"),
|
|
99
|
+
] = True,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Stop storage services."""
|
|
102
|
+
services = []
|
|
103
|
+
if client:
|
|
104
|
+
services.append("client")
|
|
105
|
+
if storage:
|
|
106
|
+
services.append("storage")
|
|
107
|
+
|
|
108
|
+
if not services:
|
|
109
|
+
console.print("[yellow]No services specified[/yellow]")
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
with console.status("[bold yellow]Stopping services..."):
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
console.print(f"[yellow]Stopped services: {', '.join(services)}[/yellow]")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command("mount")
|
|
119
|
+
def mount_storage(
|
|
120
|
+
mountpoint: Annotated[
|
|
121
|
+
Path,
|
|
122
|
+
typer.Argument(help="Directory to mount Tahoe filesystem"),
|
|
123
|
+
] = Path("/mnt/redundanet"),
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Mount the Tahoe-LAFS filesystem."""
|
|
126
|
+
if not mountpoint.exists():
|
|
127
|
+
console.print(f"[yellow]Creating mountpoint: {mountpoint}[/yellow]")
|
|
128
|
+
mountpoint.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
|
|
130
|
+
with console.status(f"[bold green]Mounting at {mountpoint}..."):
|
|
131
|
+
# In a real implementation, we'd mount the FUSE filesystem
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
console.print(f"[green]Mounted at: {mountpoint}[/green]")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.command("unmount")
|
|
138
|
+
def unmount_storage(
|
|
139
|
+
mountpoint: Annotated[
|
|
140
|
+
Path,
|
|
141
|
+
typer.Argument(help="Mountpoint to unmount"),
|
|
142
|
+
] = Path("/mnt/redundanet"),
|
|
143
|
+
force: Annotated[
|
|
144
|
+
bool,
|
|
145
|
+
typer.Option("--force", "-f", help="Force unmount"),
|
|
146
|
+
] = False,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Unmount the Tahoe-LAFS filesystem."""
|
|
149
|
+
with console.status(f"[bold yellow]Unmounting {mountpoint}..."):
|
|
150
|
+
# In a real implementation, we'd unmount
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
console.print(f"[yellow]Unmounted: {mountpoint}[/yellow]")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command("upload")
|
|
157
|
+
def upload_file(
|
|
158
|
+
source: Annotated[Path, typer.Argument(help="File or directory to upload")],
|
|
159
|
+
destination: Annotated[
|
|
160
|
+
Optional[str],
|
|
161
|
+
typer.Argument(help="Destination path in storage"),
|
|
162
|
+
] = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Upload a file or directory to the storage grid."""
|
|
165
|
+
if not source.exists():
|
|
166
|
+
console.print(f"[red]Error:[/red] Source not found: {source}")
|
|
167
|
+
raise typer.Exit(1)
|
|
168
|
+
|
|
169
|
+
dest = destination or source.name
|
|
170
|
+
|
|
171
|
+
with Progress(
|
|
172
|
+
SpinnerColumn(),
|
|
173
|
+
TextColumn("[progress.description]{task.description}"),
|
|
174
|
+
console=console,
|
|
175
|
+
) as progress:
|
|
176
|
+
task = progress.add_task(f"Uploading {source.name}...", total=None)
|
|
177
|
+
# In a real implementation, we'd upload using Tahoe-LAFS
|
|
178
|
+
progress.update(task, completed=True)
|
|
179
|
+
|
|
180
|
+
console.print(f"[green]Uploaded:[/green] {source} -> {dest}")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command("download")
|
|
184
|
+
def download_file(
|
|
185
|
+
source: Annotated[str, typer.Argument(help="Source path in storage")],
|
|
186
|
+
destination: Annotated[
|
|
187
|
+
Optional[Path],
|
|
188
|
+
typer.Argument(help="Local destination path"),
|
|
189
|
+
] = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Download a file from the storage grid."""
|
|
192
|
+
dest = destination or Path(source).name
|
|
193
|
+
|
|
194
|
+
with Progress(
|
|
195
|
+
SpinnerColumn(),
|
|
196
|
+
TextColumn("[progress.description]{task.description}"),
|
|
197
|
+
console=console,
|
|
198
|
+
) as progress:
|
|
199
|
+
task = progress.add_task(f"Downloading {source}...", total=None)
|
|
200
|
+
# In a real implementation, we'd download using Tahoe-LAFS
|
|
201
|
+
progress.update(task, completed=True)
|
|
202
|
+
|
|
203
|
+
console.print(f"[green]Downloaded:[/green] {source} -> {dest}")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.command("ls")
|
|
207
|
+
def list_files(
|
|
208
|
+
path: Annotated[str, typer.Argument(help="Path to list")] = "/",
|
|
209
|
+
long: Annotated[
|
|
210
|
+
bool,
|
|
211
|
+
typer.Option("--long", "-l", help="Show detailed listing"),
|
|
212
|
+
] = False,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""List files in the storage grid."""
|
|
215
|
+
console.print(f"[bold]Listing: {path}[/bold]")
|
|
216
|
+
|
|
217
|
+
# In a real implementation, we'd query Tahoe-LAFS
|
|
218
|
+
table = Table(show_header=True)
|
|
219
|
+
table.add_column("Name")
|
|
220
|
+
if long:
|
|
221
|
+
table.add_column("Size")
|
|
222
|
+
table.add_column("Modified")
|
|
223
|
+
table.add_column("Cap")
|
|
224
|
+
|
|
225
|
+
console.print("[dim]No files found (storage not connected)[/dim]")
|
|
226
|
+
console.print(table)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@app.command("info")
|
|
230
|
+
def file_info(
|
|
231
|
+
path: Annotated[str, typer.Argument(help="File path to inspect")],
|
|
232
|
+
) -> None:
|
|
233
|
+
"""Show detailed information about a file."""
|
|
234
|
+
console.print(f"[bold]File Info: {path}[/bold]")
|
|
235
|
+
|
|
236
|
+
table = Table(show_header=False)
|
|
237
|
+
table.add_column("Property", style="cyan")
|
|
238
|
+
table.add_column("Value")
|
|
239
|
+
|
|
240
|
+
table.add_row("Path", path)
|
|
241
|
+
table.add_row("Size", "[dim]Unknown[/dim]")
|
|
242
|
+
table.add_row("Type", "[dim]Unknown[/dim]")
|
|
243
|
+
table.add_row("Shares", "[dim]Unknown[/dim]")
|
|
244
|
+
table.add_row("Health", "[dim]Unknown[/dim]")
|
|
245
|
+
|
|
246
|
+
console.print(table)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@app.command("repair")
|
|
250
|
+
def repair_file(
|
|
251
|
+
path: Annotated[str, typer.Argument(help="File path to repair")],
|
|
252
|
+
check_only: Annotated[
|
|
253
|
+
bool,
|
|
254
|
+
typer.Option("--check", "-c", help="Only check, don't repair"),
|
|
255
|
+
] = False,
|
|
256
|
+
) -> None:
|
|
257
|
+
"""Check and repair file redundancy."""
|
|
258
|
+
action = "Checking" if check_only else "Repairing"
|
|
259
|
+
|
|
260
|
+
with console.status(f"[bold green]{action} {path}..."):
|
|
261
|
+
# In a real implementation, we'd run tahoe check/repair
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
if check_only:
|
|
265
|
+
console.print("[green]File health: OK[/green]")
|
|
266
|
+
else:
|
|
267
|
+
console.print("[green]Repair complete[/green]")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Core business logic for RedundaNet."""
|
|
2
|
+
|
|
3
|
+
from redundanet.core.config import NetworkConfig, NodeConfig, NodeRole, TahoeConfig
|
|
4
|
+
from redundanet.core.exceptions import (
|
|
5
|
+
ConfigurationError,
|
|
6
|
+
ManifestError,
|
|
7
|
+
NetworkError,
|
|
8
|
+
NodeError,
|
|
9
|
+
RedundaNetError,
|
|
10
|
+
StorageError,
|
|
11
|
+
VPNError,
|
|
12
|
+
)
|
|
13
|
+
from redundanet.core.manifest import Manifest
|
|
14
|
+
from redundanet.core.node import Node, NodeStatus
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ConfigurationError",
|
|
18
|
+
"Manifest",
|
|
19
|
+
"ManifestError",
|
|
20
|
+
"NetworkConfig",
|
|
21
|
+
"NetworkError",
|
|
22
|
+
"Node",
|
|
23
|
+
"NodeConfig",
|
|
24
|
+
"NodeError",
|
|
25
|
+
"NodeRole",
|
|
26
|
+
"NodeStatus",
|
|
27
|
+
"RedundaNetError",
|
|
28
|
+
"StorageError",
|
|
29
|
+
"TahoeConfig",
|
|
30
|
+
"VPNError",
|
|
31
|
+
]
|