firecloud-devnet 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.
- fc_mlops/__init__.py +3 -0
- fc_mlops/__main__.py +5 -0
- fc_mlops/anomaly.py +112 -0
- fc_mlops/artifact_store.py +111 -0
- fc_mlops/cli.py +190 -0
- fc_mlops/simulate_failure.py +100 -0
- fc_mlops/telemetry.py +72 -0
- fc_rag/__init__.py +3 -0
- fc_rag/cli.py +51 -0
- fc_rag/config.py +24 -0
- fc_rag/embedder.py +62 -0
- fc_rag/indexer.py +121 -0
- fc_rag/query_engine.py +79 -0
- fc_rag/requirements.txt +6 -0
- fc_rag/retriever.py +46 -0
- firecloud/__init__.py +17 -0
- firecloud/chunker.py +122 -0
- firecloud/cli.py +540 -0
- firecloud/crypto.py +269 -0
- firecloud/discovery.py +164 -0
- firecloud/distributor.py +269 -0
- firecloud/exceptions.py +41 -0
- firecloud/fec.py +87 -0
- firecloud/manifest.py +263 -0
- firecloud/network.py +90 -0
- firecloud/node.py +562 -0
- firecloud/storage.py +146 -0
- firecloud/sync.py +277 -0
- firecloud/transport.py +387 -0
- firecloud_devnet-0.1.0.dist-info/METADATA +158 -0
- firecloud_devnet-0.1.0.dist-info/RECORD +34 -0
- firecloud_devnet-0.1.0.dist-info/WHEEL +4 -0
- firecloud_devnet-0.1.0.dist-info/entry_points.txt +4 -0
- firecloud_devnet-0.1.0.dist-info/licenses/LICENSE +21 -0
firecloud/cli.py
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""FireCloud CLI — click-based command-line interface.
|
|
2
|
+
|
|
3
|
+
Provides commands to initialise a network, start/stop a node,
|
|
4
|
+
upload/download/delete files, list files and peers, connect to
|
|
5
|
+
peers, and sync a folder.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import signal
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from firecloud.network import Network
|
|
16
|
+
from firecloud.exceptions import FireCloudError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Default configuration directory
|
|
20
|
+
_DEFAULT_DIR = Path.home() / ".firecloud"
|
|
21
|
+
_KEYSTORE_FILE = "network.key"
|
|
22
|
+
_PID_FILE = "firecloud.pid"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _config_dir() -> Path:
|
|
26
|
+
"""Return (and create) the config directory."""
|
|
27
|
+
d = _DEFAULT_DIR
|
|
28
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
return d
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_network(passphrase: str) -> Network:
|
|
33
|
+
"""Load the network from the default keystore."""
|
|
34
|
+
keystore = _config_dir() / _KEYSTORE_FILE
|
|
35
|
+
if not keystore.exists():
|
|
36
|
+
raise click.ClickException(
|
|
37
|
+
"Network not initialised. Run 'firecloud init' first."
|
|
38
|
+
)
|
|
39
|
+
try:
|
|
40
|
+
return Network.load(keystore, passphrase)
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
raise click.ClickException(f"Failed to load network: {exc}") from exc
|
|
43
|
+
|
|
44
|
+
def _human_size(num_bytes: int) -> str:
|
|
45
|
+
"""Convert bytes to a human-readable string."""
|
|
46
|
+
for unit in ("B", "KB", "MB", "GB", "TB"):
|
|
47
|
+
if abs(num_bytes) < 1024:
|
|
48
|
+
return f"{num_bytes:.1f} {unit}"
|
|
49
|
+
num_bytes /= 1024
|
|
50
|
+
return f"{num_bytes:.1f} PB"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ========================================================================
|
|
54
|
+
# CLI Group
|
|
55
|
+
# ========================================================================
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@click.group()
|
|
59
|
+
@click.version_option(version="0.1.0", prog_name="firecloud")
|
|
60
|
+
def cli():
|
|
61
|
+
"""FireCloud — Private, encrypted, distributed storage."""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ========================================================================
|
|
66
|
+
# init
|
|
67
|
+
# ========================================================================
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@cli.command()
|
|
71
|
+
@click.option("--join", "join_addr", default=None, help="Join an existing network via peer address (host:port).")
|
|
72
|
+
@click.option("--passphrase", prompt=True, hide_input=True, confirmation_prompt=True, help="Passphrase to protect the network key.")
|
|
73
|
+
def init(join_addr: str | None, passphrase: str):
|
|
74
|
+
"""Initialise a new FireCloud network or join an existing one."""
|
|
75
|
+
cfg = _config_dir()
|
|
76
|
+
keystore = cfg / _KEYSTORE_FILE
|
|
77
|
+
|
|
78
|
+
if join_addr:
|
|
79
|
+
# Join mode: connect to peer and get network key
|
|
80
|
+
click.echo(f"Joining network via {join_addr}...")
|
|
81
|
+
# For v1, joining requires the same passphrase — the peer shares the
|
|
82
|
+
# keystore file out-of-band. We simply create the config directory.
|
|
83
|
+
click.echo(
|
|
84
|
+
click.style("⚠ Copy the network.key file from an existing node to:", fg="yellow")
|
|
85
|
+
)
|
|
86
|
+
click.echo(f" {keystore}")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
if keystore.exists():
|
|
90
|
+
click.echo(
|
|
91
|
+
click.style("Network already initialised.", fg="yellow")
|
|
92
|
+
+ f" Keystore at {keystore}"
|
|
93
|
+
)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
net = Network.create(passphrase)
|
|
97
|
+
net.save(keystore, passphrase)
|
|
98
|
+
click.echo(click.style("✓ Network initialised.", fg="green"))
|
|
99
|
+
click.echo(f" Network ID : {net.network_id}")
|
|
100
|
+
click.echo(f" Keystore : {keystore}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ========================================================================
|
|
104
|
+
# start
|
|
105
|
+
# ========================================================================
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@cli.command()
|
|
109
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
110
|
+
@click.option("--port", default=7474, type=int, help="TCP port to listen on.")
|
|
111
|
+
@click.option("--daemon", is_flag=True, help="Run in the background (Unix only).")
|
|
112
|
+
@click.option("--storage", default=None, type=click.Path(), help="Storage directory.")
|
|
113
|
+
def start(passphrase: str, port: int, daemon: bool, storage: str | None):
|
|
114
|
+
"""Start the FireCloud node."""
|
|
115
|
+
net = _load_network(passphrase)
|
|
116
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
117
|
+
|
|
118
|
+
if daemon:
|
|
119
|
+
_start_daemon(net, storage_path, port)
|
|
120
|
+
else:
|
|
121
|
+
_start_foreground(net, storage_path, port)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _start_foreground(net: Network, storage_path: Path, port: int):
|
|
125
|
+
"""Run the node in the foreground."""
|
|
126
|
+
from firecloud.node import Node
|
|
127
|
+
|
|
128
|
+
async def _run():
|
|
129
|
+
node = Node(network=net, storage_path=storage_path, port=port)
|
|
130
|
+
await node.start()
|
|
131
|
+
click.echo(click.style(f"✓ Node {node.node_id} running on port {port}", fg="green"))
|
|
132
|
+
click.echo(" Press Ctrl+C to stop.")
|
|
133
|
+
|
|
134
|
+
stop_event = asyncio.Event()
|
|
135
|
+
loop = asyncio.get_running_loop()
|
|
136
|
+
|
|
137
|
+
def _signal_handler():
|
|
138
|
+
stop_event.set()
|
|
139
|
+
|
|
140
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
141
|
+
try:
|
|
142
|
+
loop.add_signal_handler(sig, _signal_handler)
|
|
143
|
+
except NotImplementedError:
|
|
144
|
+
pass # Windows
|
|
145
|
+
|
|
146
|
+
await stop_event.wait()
|
|
147
|
+
click.echo("\nStopping node...")
|
|
148
|
+
await node.stop()
|
|
149
|
+
click.echo(click.style("✓ Node stopped.", fg="green"))
|
|
150
|
+
|
|
151
|
+
asyncio.run(_run())
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _start_daemon(net: Network, storage_path: Path, port: int):
|
|
155
|
+
"""Fork to background and write a PID file (Unix only)."""
|
|
156
|
+
pid_file = _config_dir() / _PID_FILE
|
|
157
|
+
|
|
158
|
+
if pid_file.exists():
|
|
159
|
+
pid = int(pid_file.read_text().strip())
|
|
160
|
+
try:
|
|
161
|
+
os.kill(pid, 0)
|
|
162
|
+
click.echo(click.style(f"Node already running (PID {pid}).", fg="yellow"))
|
|
163
|
+
return
|
|
164
|
+
except OSError:
|
|
165
|
+
pid_file.unlink(missing_ok=True)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
child_pid = os.fork()
|
|
169
|
+
except AttributeError:
|
|
170
|
+
raise click.ClickException("--daemon is not supported on this platform (no os.fork).")
|
|
171
|
+
|
|
172
|
+
if child_pid > 0:
|
|
173
|
+
# Parent — write PID and exit
|
|
174
|
+
pid_file.write_text(str(child_pid))
|
|
175
|
+
click.echo(click.style(f"✓ Node daemonised (PID {child_pid}).", fg="green"))
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Child — detach and run
|
|
179
|
+
os.setsid()
|
|
180
|
+
_start_foreground(net, storage_path, port)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ========================================================================
|
|
184
|
+
# stop
|
|
185
|
+
# ========================================================================
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@cli.command()
|
|
189
|
+
def stop():
|
|
190
|
+
"""Stop a running FireCloud daemon."""
|
|
191
|
+
pid_file = _config_dir() / _PID_FILE
|
|
192
|
+
if not pid_file.exists():
|
|
193
|
+
click.echo(click.style("No running daemon found.", fg="yellow"))
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
pid = int(pid_file.read_text().strip())
|
|
197
|
+
try:
|
|
198
|
+
os.kill(pid, signal.SIGTERM)
|
|
199
|
+
click.echo(click.style(f"✓ Sent SIGTERM to PID {pid}.", fg="green"))
|
|
200
|
+
except OSError as exc:
|
|
201
|
+
click.echo(click.style(f"Failed to stop PID {pid}: {exc}", fg="red"))
|
|
202
|
+
finally:
|
|
203
|
+
pid_file.unlink(missing_ok=True)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ========================================================================
|
|
207
|
+
# status
|
|
208
|
+
# ========================================================================
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@cli.command()
|
|
212
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
213
|
+
@click.option("--port", default=7474, type=int)
|
|
214
|
+
@click.option("--storage", default=None, type=click.Path())
|
|
215
|
+
def status(passphrase: str, port: int, storage: str | None):
|
|
216
|
+
"""Show node and network status."""
|
|
217
|
+
net = _load_network(passphrase)
|
|
218
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
219
|
+
|
|
220
|
+
from firecloud.node import Node
|
|
221
|
+
|
|
222
|
+
async def _run():
|
|
223
|
+
node = Node(network=net, storage_path=storage_path, port=port,
|
|
224
|
+
enable_discovery=False)
|
|
225
|
+
s = node.status()
|
|
226
|
+
click.echo(click.style("FireCloud Status", fg="cyan", bold=True))
|
|
227
|
+
click.echo(f" Node ID : {s['node_id']}")
|
|
228
|
+
click.echo(f" Network ID : {s['network_id']}")
|
|
229
|
+
click.echo(f" Port : {s['port']}")
|
|
230
|
+
click.echo(f" Running : {s['running']}")
|
|
231
|
+
click.echo(f" Peers connected : {s['peers_connected']}")
|
|
232
|
+
click.echo(f" Files stored : {s['files_stored']}")
|
|
233
|
+
click.echo(f" Chunks stored : {s['chunks_stored']}")
|
|
234
|
+
click.echo(f" Storage used : {_human_size(s['storage_used'])}")
|
|
235
|
+
click.echo(f" Storage available: {_human_size(s['storage_available'])}")
|
|
236
|
+
|
|
237
|
+
asyncio.run(_run())
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ========================================================================
|
|
241
|
+
# upload
|
|
242
|
+
# ========================================================================
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@cli.command()
|
|
246
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
247
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
248
|
+
@click.option("--port", default=7474, type=int)
|
|
249
|
+
@click.option("--storage", default=None, type=click.Path())
|
|
250
|
+
def upload(path: str, passphrase: str, port: int, storage: str | None):
|
|
251
|
+
"""Upload a file to the FireCloud network."""
|
|
252
|
+
net = _load_network(passphrase)
|
|
253
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
254
|
+
|
|
255
|
+
from firecloud.node import Node
|
|
256
|
+
|
|
257
|
+
async def _run():
|
|
258
|
+
node = Node(network=net, storage_path=storage_path, port=port,
|
|
259
|
+
enable_discovery=False)
|
|
260
|
+
await node.start()
|
|
261
|
+
try:
|
|
262
|
+
file_id = await node.upload(path)
|
|
263
|
+
click.echo(click.style("✓ Uploaded successfully.", fg="green"))
|
|
264
|
+
click.echo(f" File ID: {file_id}")
|
|
265
|
+
except FireCloudError as exc:
|
|
266
|
+
raise click.ClickException(str(exc))
|
|
267
|
+
finally:
|
|
268
|
+
await node.stop()
|
|
269
|
+
|
|
270
|
+
asyncio.run(_run())
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ========================================================================
|
|
274
|
+
# download
|
|
275
|
+
# ========================================================================
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@cli.command()
|
|
279
|
+
@click.argument("file_id")
|
|
280
|
+
@click.argument("output", type=click.Path())
|
|
281
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
282
|
+
@click.option("--port", default=7474, type=int)
|
|
283
|
+
@click.option("--storage", default=None, type=click.Path())
|
|
284
|
+
def download(file_id: str, output: str, passphrase: str, port: int, storage: str | None):
|
|
285
|
+
"""Download a file from the FireCloud network."""
|
|
286
|
+
net = _load_network(passphrase)
|
|
287
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
288
|
+
|
|
289
|
+
from firecloud.node import Node
|
|
290
|
+
|
|
291
|
+
async def _run():
|
|
292
|
+
node = Node(network=net, storage_path=storage_path, port=port,
|
|
293
|
+
enable_discovery=False)
|
|
294
|
+
await node.start()
|
|
295
|
+
try:
|
|
296
|
+
await node.download(file_id, output)
|
|
297
|
+
click.echo(click.style(f"✓ Downloaded to {output}", fg="green"))
|
|
298
|
+
except FireCloudError as exc:
|
|
299
|
+
raise click.ClickException(str(exc))
|
|
300
|
+
finally:
|
|
301
|
+
await node.stop()
|
|
302
|
+
|
|
303
|
+
asyncio.run(_run())
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ========================================================================
|
|
307
|
+
# delete
|
|
308
|
+
# ========================================================================
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@cli.command("delete")
|
|
312
|
+
@click.argument("file_id")
|
|
313
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
314
|
+
@click.option("--port", default=7474, type=int)
|
|
315
|
+
@click.option("--storage", default=None, type=click.Path())
|
|
316
|
+
def delete_file(file_id: str, passphrase: str, port: int, storage: str | None):
|
|
317
|
+
"""Delete a file from the FireCloud network."""
|
|
318
|
+
net = _load_network(passphrase)
|
|
319
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
320
|
+
|
|
321
|
+
from firecloud.node import Node
|
|
322
|
+
|
|
323
|
+
async def _run():
|
|
324
|
+
node = Node(network=net, storage_path=storage_path, port=port,
|
|
325
|
+
enable_discovery=False)
|
|
326
|
+
await node.start()
|
|
327
|
+
try:
|
|
328
|
+
await node.delete(file_id)
|
|
329
|
+
click.echo(click.style(f"✓ File {file_id[:16]}... deleted.", fg="green"))
|
|
330
|
+
except FireCloudError as exc:
|
|
331
|
+
raise click.ClickException(str(exc))
|
|
332
|
+
finally:
|
|
333
|
+
await node.stop()
|
|
334
|
+
|
|
335
|
+
asyncio.run(_run())
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ========================================================================
|
|
339
|
+
# list
|
|
340
|
+
# ========================================================================
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@cli.command("list")
|
|
344
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
345
|
+
@click.option("--port", default=7474, type=int)
|
|
346
|
+
@click.option("--storage", default=None, type=click.Path())
|
|
347
|
+
def list_files(passphrase: str, port: int, storage: str | None):
|
|
348
|
+
"""List all files stored in the FireCloud network."""
|
|
349
|
+
net = _load_network(passphrase)
|
|
350
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
351
|
+
|
|
352
|
+
from firecloud.node import Node
|
|
353
|
+
|
|
354
|
+
async def _run():
|
|
355
|
+
node = Node(network=net, storage_path=storage_path, port=port,
|
|
356
|
+
enable_discovery=False)
|
|
357
|
+
files = node.list_files()
|
|
358
|
+
if not files:
|
|
359
|
+
click.echo("No files stored.")
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
# Table header
|
|
363
|
+
click.echo(
|
|
364
|
+
click.style(
|
|
365
|
+
f"{'Name':<30} {'Size':>10} {'Chunks':>7} {'FEC':>5} {'Uploaded At':<26} {'File ID':<20}",
|
|
366
|
+
bold=True,
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
click.echo("─" * 105)
|
|
370
|
+
for f in files:
|
|
371
|
+
click.echo(
|
|
372
|
+
f"{f['name']:<30} "
|
|
373
|
+
f"{_human_size(f['size']):>10} "
|
|
374
|
+
f"{f['chunk_count']:>7} "
|
|
375
|
+
f"{'yes' if f['fec_enabled'] else 'no':>5} "
|
|
376
|
+
f"{f['uploaded_at']:<26} "
|
|
377
|
+
f"{f['file_id'][:20]}"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
asyncio.run(_run())
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# ========================================================================
|
|
384
|
+
# peers
|
|
385
|
+
# ========================================================================
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@cli.command()
|
|
389
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
390
|
+
@click.option("--port", default=7474, type=int)
|
|
391
|
+
@click.option("--storage", default=None, type=click.Path())
|
|
392
|
+
def peers(passphrase: str, port: int, storage: str | None):
|
|
393
|
+
"""List known peers."""
|
|
394
|
+
net = _load_network(passphrase)
|
|
395
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
396
|
+
|
|
397
|
+
from firecloud.node import Node
|
|
398
|
+
|
|
399
|
+
async def _run():
|
|
400
|
+
node = Node(network=net, storage_path=storage_path, port=port,
|
|
401
|
+
enable_discovery=False)
|
|
402
|
+
peer_list = node.peers()
|
|
403
|
+
if not peer_list:
|
|
404
|
+
click.echo("No known peers.")
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
click.echo(
|
|
408
|
+
click.style(f"{'Node ID':<20} {'Host':<16} {'Port':>6} {'Connected':>10}", bold=True)
|
|
409
|
+
)
|
|
410
|
+
click.echo("─" * 55)
|
|
411
|
+
for p in peer_list:
|
|
412
|
+
click.echo(
|
|
413
|
+
f"{p['node_id']:<20} "
|
|
414
|
+
f"{p['host']:<16} "
|
|
415
|
+
f"{p['port']:>6} "
|
|
416
|
+
f"{'yes' if p['connected'] else 'no':>10}"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
asyncio.run(_run())
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ========================================================================
|
|
423
|
+
# connect
|
|
424
|
+
# ========================================================================
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@cli.command()
|
|
428
|
+
@click.argument("address")
|
|
429
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
430
|
+
@click.option("--port", default=7474, type=int)
|
|
431
|
+
@click.option("--storage", default=None, type=click.Path())
|
|
432
|
+
def connect(address: str, passphrase: str, port: int, storage: str | None):
|
|
433
|
+
"""Connect to a peer at ADDRESS (host:port)."""
|
|
434
|
+
net = _load_network(passphrase)
|
|
435
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
436
|
+
|
|
437
|
+
from firecloud.node import Node
|
|
438
|
+
|
|
439
|
+
async def _run():
|
|
440
|
+
node = Node(network=net, storage_path=storage_path, port=port,
|
|
441
|
+
enable_discovery=False)
|
|
442
|
+
await node.start()
|
|
443
|
+
try:
|
|
444
|
+
await node.connect(address)
|
|
445
|
+
click.echo(click.style(f"✓ Connected to {address}", fg="green"))
|
|
446
|
+
except FireCloudError as exc:
|
|
447
|
+
raise click.ClickException(str(exc))
|
|
448
|
+
finally:
|
|
449
|
+
await node.stop()
|
|
450
|
+
|
|
451
|
+
asyncio.run(_run())
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# ========================================================================
|
|
455
|
+
# remove-node
|
|
456
|
+
# ========================================================================
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@cli.command("remove-node")
|
|
460
|
+
@click.argument("node_id")
|
|
461
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
462
|
+
@click.option("--port", default=7474, type=int)
|
|
463
|
+
@click.option("--storage", default=None, type=click.Path())
|
|
464
|
+
def remove_node(node_id: str, passphrase: str, port: int, storage: str | None):
|
|
465
|
+
"""Remove a node from the network and trigger re-replication."""
|
|
466
|
+
net = _load_network(passphrase)
|
|
467
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
468
|
+
|
|
469
|
+
from firecloud.node import Node
|
|
470
|
+
|
|
471
|
+
async def _run():
|
|
472
|
+
node = Node(network=net, storage_path=storage_path, port=port,
|
|
473
|
+
enable_discovery=False)
|
|
474
|
+
await node.start()
|
|
475
|
+
try:
|
|
476
|
+
await node.remove_node(node_id)
|
|
477
|
+
click.echo(click.style(f"✓ Removed node {node_id[:16]}... and triggered re-replication", fg="green"))
|
|
478
|
+
except FireCloudError as exc:
|
|
479
|
+
raise click.ClickException(str(exc))
|
|
480
|
+
finally:
|
|
481
|
+
await node.stop()
|
|
482
|
+
|
|
483
|
+
asyncio.run(_run())
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# ========================================================================
|
|
487
|
+
# sync
|
|
488
|
+
# ========================================================================
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@cli.command()
|
|
492
|
+
@click.argument("folder", type=click.Path())
|
|
493
|
+
@click.option("--passphrase", prompt=True, hide_input=True, help="Network passphrase.")
|
|
494
|
+
@click.option("--port", default=7474, type=int)
|
|
495
|
+
@click.option("--storage", default=None, type=click.Path())
|
|
496
|
+
@click.option("--daemon", is_flag=True, help="Run in the background (Unix only).")
|
|
497
|
+
def sync(folder: str, passphrase: str, port: int, storage: str | None, daemon: bool):
|
|
498
|
+
"""Sync a local folder with the FireCloud network."""
|
|
499
|
+
net = _load_network(passphrase)
|
|
500
|
+
storage_path = Path(storage) if storage else _config_dir() / "storage"
|
|
501
|
+
|
|
502
|
+
from firecloud.node import Node
|
|
503
|
+
from firecloud.sync import FolderSync
|
|
504
|
+
|
|
505
|
+
async def _run():
|
|
506
|
+
node = Node(network=net, storage_path=storage_path, port=port)
|
|
507
|
+
await node.start()
|
|
508
|
+
fs = FolderSync(node, Path(folder))
|
|
509
|
+
await fs.start()
|
|
510
|
+
click.echo(click.style(f"✓ Syncing {folder}", fg="green"))
|
|
511
|
+
click.echo(" Press Ctrl+C to stop.")
|
|
512
|
+
|
|
513
|
+
stop_event = asyncio.Event()
|
|
514
|
+
loop = asyncio.get_running_loop()
|
|
515
|
+
|
|
516
|
+
def _signal_handler():
|
|
517
|
+
stop_event.set()
|
|
518
|
+
|
|
519
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
520
|
+
try:
|
|
521
|
+
loop.add_signal_handler(sig, _signal_handler)
|
|
522
|
+
except NotImplementedError:
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
await stop_event.wait()
|
|
526
|
+
click.echo("\nStopping sync...")
|
|
527
|
+
await fs.stop()
|
|
528
|
+
await node.stop()
|
|
529
|
+
click.echo(click.style("✓ Sync stopped.", fg="green"))
|
|
530
|
+
|
|
531
|
+
asyncio.run(_run())
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
# ========================================================================
|
|
535
|
+
# Entry point
|
|
536
|
+
# ========================================================================
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
if __name__ == "__main__":
|
|
540
|
+
cli()
|