blitzwing 0.1.8 → 0.1.9
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.
- package/package.json +1 -1
- package/src/install.js +12 -2
- package/src/net.js +53 -0
- package/src/wizard.js +93 -20
package/package.json
CHANGED
package/src/install.js
CHANGED
|
@@ -220,6 +220,7 @@ from pydantic import BaseModel, Field
|
|
|
220
220
|
|
|
221
221
|
class ReloadRequest(BaseModel):
|
|
222
222
|
block_indices: str = Field(..., pattern=r"^\\d+:\\d+$")
|
|
223
|
+
initial_peers: Optional[List[str]] = None
|
|
223
224
|
|
|
224
225
|
class StatusResponse(BaseModel):
|
|
225
226
|
running: bool
|
|
@@ -259,6 +260,13 @@ class Mgr:
|
|
|
259
260
|
c += ["--public_ip", self.public_ip]
|
|
260
261
|
if self.initial_peers:
|
|
261
262
|
c += ["--initial_peers", *self.initial_peers]
|
|
263
|
+
use_auto_relay = os.environ.get("PETALS_USE_AUTO_RELAY", "1") not in ("0", "false", "False")
|
|
264
|
+
if not self.announce_maddrs and use_auto_relay:
|
|
265
|
+
pass
|
|
266
|
+
elif not use_auto_relay:
|
|
267
|
+
c += ["--no_auto_relay"]
|
|
268
|
+
if os.environ.get("PETALS_SKIP_REACHABILITY_CHECK", "1") in ("1", "true", "True"):
|
|
269
|
+
c += ["--skip_reachability_check"]
|
|
262
270
|
return c
|
|
263
271
|
|
|
264
272
|
def start(self, bi=None):
|
|
@@ -296,7 +304,9 @@ class Mgr:
|
|
|
296
304
|
self.last_exit_code = p.returncode
|
|
297
305
|
self._proc = None
|
|
298
306
|
|
|
299
|
-
def reload(self, bi):
|
|
307
|
+
def reload(self, bi, initial_peers=None):
|
|
308
|
+
if initial_peers is not None:
|
|
309
|
+
self.initial_peers = initial_peers
|
|
300
310
|
self.stop(); time.sleep(1); self.start(bi)
|
|
301
311
|
|
|
302
312
|
def status(self):
|
|
@@ -333,7 +343,7 @@ def status():
|
|
|
333
343
|
def reload(body: ReloadRequest):
|
|
334
344
|
a,b = map(int, body.block_indices.split(":"))
|
|
335
345
|
if b <= a: raise HTTPException(400, "bad range")
|
|
336
|
-
mgr.reload(body.block_indices)
|
|
346
|
+
mgr.reload(body.block_indices, initial_peers=body.initial_peers)
|
|
337
347
|
time.sleep(0.5)
|
|
338
348
|
return mgr.status()
|
|
339
349
|
|
package/src/net.js
CHANGED
|
@@ -24,6 +24,59 @@ export async function detectPublicIp() {
|
|
|
24
24
|
return "";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** Local LAN/WSL IP for shard_manager_url metadata (not Petals announce). */
|
|
28
|
+
export async function detectLocalIp() {
|
|
29
|
+
if (process.env.BLITZWING_LOCAL_IP) {
|
|
30
|
+
return process.env.BLITZWING_LOCAL_IP.trim();
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const { networkInterfaces } = await import("node:os");
|
|
34
|
+
const nets = networkInterfaces();
|
|
35
|
+
for (const name of Object.keys(nets)) {
|
|
36
|
+
for (const net of nets[name] || []) {
|
|
37
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
38
|
+
return net.address;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
return "127.0.0.1";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Optional ngrok TCP tunnel for dev; production should use VM public IP or auto-relay. */
|
|
49
|
+
export async function discoverNgrokTcpAnnounce(petalsPort) {
|
|
50
|
+
const override = process.env.BLITZWING_ANNOUNCE_MADDRS?.trim();
|
|
51
|
+
if (override) return override;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch("http://127.0.0.1:4040/api/tunnels", {
|
|
55
|
+
signal: AbortSignal.timeout(2000),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) return null;
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
const tunnel = (data.tunnels || []).find((t) => t.proto === "tcp");
|
|
60
|
+
if (!tunnel?.public_url) return null;
|
|
61
|
+
const url = new URL(tunnel.public_url);
|
|
62
|
+
return `/dns4/${url.hostname}/tcp/${url.port || petalsPort}`;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function extractPeerMultiaddrFromLog(logPath, fs) {
|
|
69
|
+
if (!fs.existsSync(logPath)) return null;
|
|
70
|
+
const text = fs.readFileSync(logPath, "utf8");
|
|
71
|
+
const line = text
|
|
72
|
+
.split("\n")
|
|
73
|
+
.reverse()
|
|
74
|
+
.find((l) => l.includes("Running a server on"));
|
|
75
|
+
if (!line) return null;
|
|
76
|
+
const m = line.match(/Running a server on \['([^']+)'\]/);
|
|
77
|
+
return m ? m[1] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
27
80
|
export function which(cmd) {
|
|
28
81
|
try {
|
|
29
82
|
const out = execFileSync(process.platform === "win32" ? "where" : "which", [cmd], {
|
package/src/wizard.js
CHANGED
|
@@ -5,7 +5,12 @@ import path from "node:path";
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { DEFAULT_DISCOVERY_URL, HOME_DIR, SHARD_PORT, PETALS_PORT } from "./config.js";
|
|
7
7
|
import { listMothers, motherHosts, joinHost, readyHost, leaveHost } from "./api.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
detectPublicIp,
|
|
10
|
+
detectLocalIp,
|
|
11
|
+
discoverNgrokTcpAnnounce,
|
|
12
|
+
extractPeerMultiaddrFromLog,
|
|
13
|
+
} from "./net.js";
|
|
9
14
|
import {
|
|
10
15
|
ensureVenvAndPetals,
|
|
11
16
|
startShardManagerProcess,
|
|
@@ -55,6 +60,9 @@ ${color.bold("blitzwing")} — join a Blitzwing mother swarm as a compute node
|
|
|
55
60
|
|
|
56
61
|
Env:
|
|
57
62
|
BLITZWING_DISCOVERY_URL Override discovery service (default ${DEFAULT_DISCOVERY_URL})
|
|
63
|
+
BLITZWING_ANNOUNCE_MADDRS Explicit Petals announce multiaddr (VM with public IP)
|
|
64
|
+
BLITZWING_LOCAL_IP Local IP for shard manager metadata
|
|
65
|
+
PETALS_USE_AUTO_RELAY=0 Disable libp2p auto-relay (use with public IP / port-forward)
|
|
58
66
|
`);
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -130,18 +138,70 @@ async function wizard(args) {
|
|
|
130
138
|
const layersN = Number(layers);
|
|
131
139
|
|
|
132
140
|
const detected = await detectPublicIp();
|
|
133
|
-
const
|
|
134
|
-
message: "
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
const networkMode = await p.select({
|
|
142
|
+
message: "How will other peers reach your Petals node?",
|
|
143
|
+
options: [
|
|
144
|
+
{
|
|
145
|
+
value: "relay",
|
|
146
|
+
label: "Auto (recommended for home / NAT)",
|
|
147
|
+
hint: "Uses Petals libp2p relay — no port forwarding",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
value: "public",
|
|
151
|
+
label: "Public IP / cloud VM",
|
|
152
|
+
hint: "TCP 31337 (and 8001) open on the internet",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
value: "ngrok",
|
|
156
|
+
label: "Dev tunnel (ngrok TCP on :4040)",
|
|
157
|
+
hint: "Only if ngrok is already running",
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
initialValue: "relay",
|
|
139
161
|
});
|
|
140
|
-
if (p.isCancel(
|
|
162
|
+
if (p.isCancel(networkMode)) {
|
|
141
163
|
p.cancel("Setup cancelled");
|
|
142
164
|
process.exit(0);
|
|
143
165
|
}
|
|
144
166
|
|
|
167
|
+
let publicIp = "";
|
|
168
|
+
let announceMaddrs = "";
|
|
169
|
+
let useAutoRelay = "1";
|
|
170
|
+
|
|
171
|
+
if (networkMode === "public") {
|
|
172
|
+
publicIp = await p.text({
|
|
173
|
+
message: "Public IPv4 (must be reachable on TCP " + PETALS_PORT + ")",
|
|
174
|
+
initialValue: detected || "",
|
|
175
|
+
validate(v) {
|
|
176
|
+
if (!v || !/^\d+\.\d+\.\d+\.\d+$/.test(String(v).trim())) {
|
|
177
|
+
return "Enter a valid public IPv4 address";
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
if (p.isCancel(publicIp)) {
|
|
182
|
+
p.cancel("Setup cancelled");
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
publicIp = String(publicIp).trim();
|
|
186
|
+
announceMaddrs = `/ip4/${publicIp}/tcp/${PETALS_PORT}`;
|
|
187
|
+
useAutoRelay = "0";
|
|
188
|
+
} else if (networkMode === "ngrok") {
|
|
189
|
+
const ngrok = await discoverNgrokTcpAnnounce(PETALS_PORT);
|
|
190
|
+
if (!ngrok) {
|
|
191
|
+
p.cancel("No ngrok TCP tunnel on http://127.0.0.1:4040. Start: ngrok tcp " + PETALS_PORT);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
announceMaddrs = ngrok;
|
|
195
|
+
publicIp = ngrok.match(/dns4\/([^/]+)/)?.[1] || detected || "relay";
|
|
196
|
+
useAutoRelay = "0";
|
|
197
|
+
p.log.info(`Using ngrok announce ${announceMaddrs}`);
|
|
198
|
+
} else {
|
|
199
|
+
publicIp = detected || "relay";
|
|
200
|
+
p.log.info("Using Petals libp2p auto-relay (no inbound port forward required).");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const localIp = await detectLocalIp();
|
|
204
|
+
|
|
145
205
|
const hederaAccount = await p.text({
|
|
146
206
|
message: "Hedera account ID for layer payouts (e.g. 0.0.123456)",
|
|
147
207
|
initialValue: process.env.BLITZWING_HEDERA_ACCOUNT_ID || "",
|
|
@@ -157,7 +217,7 @@ async function wizard(args) {
|
|
|
157
217
|
const hederaAccountId = String(hederaAccount).trim();
|
|
158
218
|
|
|
159
219
|
const confirm = await p.confirm({
|
|
160
|
-
message: `Join ${selected.model} hosting ${layersN} layers
|
|
220
|
+
message: `Join ${selected.model} hosting ${layersN} layers (${networkMode}) paying to ${hederaAccountId}?`,
|
|
161
221
|
initialValue: true,
|
|
162
222
|
});
|
|
163
223
|
if (p.isCancel(confirm) || !confirm) {
|
|
@@ -180,7 +240,8 @@ async function wizard(args) {
|
|
|
180
240
|
const python = venvPython();
|
|
181
241
|
spin.stop("Environment ready");
|
|
182
242
|
|
|
183
|
-
const shardManagerUrl = `http://${
|
|
243
|
+
const shardManagerUrl = `http://${localIp}:${SHARD_PORT}`;
|
|
244
|
+
const petalsLogPath = path.join(HOME_DIR, "petals.log");
|
|
184
245
|
|
|
185
246
|
spin.start("Requesting layer assignment from mother…");
|
|
186
247
|
let assignment;
|
|
@@ -202,22 +263,28 @@ async function wizard(args) {
|
|
|
202
263
|
|
|
203
264
|
const logPath = path.join(HOME_DIR, "shard_manager.log");
|
|
204
265
|
spin.start("Starting local Petals server…");
|
|
205
|
-
const
|
|
206
|
-
python,
|
|
207
|
-
logPath,
|
|
208
|
-
env: {
|
|
266
|
+
const shardEnv = {
|
|
209
267
|
MODEL_NAME: selected.model,
|
|
210
|
-
PUBLIC_IP: String(publicIp).trim(),
|
|
268
|
+
PUBLIC_IP: announceMaddrs ? "" : String(publicIp).trim(),
|
|
211
269
|
BLOCK_INDICES: assignment.block_indices,
|
|
212
270
|
INITIAL_PEERS: (assignment.initial_peers || []).join(","),
|
|
213
271
|
PETALS_PORT: String(PETALS_PORT),
|
|
214
272
|
IDENTITY_PATH: path.join(HOME_DIR, "petals-identity"),
|
|
215
273
|
SHARD_AUTO_START: "1",
|
|
216
|
-
|
|
274
|
+
PETALS_USE_AUTO_RELAY: useAutoRelay,
|
|
275
|
+
PETALS_SKIP_REACHABILITY_CHECK: "1",
|
|
276
|
+
};
|
|
277
|
+
if (announceMaddrs) {
|
|
278
|
+
shardEnv.ANNOUNCE_MADDRS = announceMaddrs;
|
|
279
|
+
}
|
|
280
|
+
const { pid } = startShardManagerProcess({
|
|
281
|
+
python,
|
|
282
|
+
logPath,
|
|
283
|
+
env: shardEnv,
|
|
217
284
|
});
|
|
218
285
|
|
|
219
286
|
try {
|
|
220
|
-
await waitForShardRunning({ timeoutMs: 600000, statusHost:
|
|
287
|
+
await waitForShardRunning({ timeoutMs: 600000, statusHost: "127.0.0.1" });
|
|
221
288
|
} catch (err) {
|
|
222
289
|
spin.stop("Petals did not become ready");
|
|
223
290
|
p.cancel(`${err.message}. See ${logPath}`);
|
|
@@ -226,8 +293,12 @@ async function wizard(args) {
|
|
|
226
293
|
spin.stop("Petals is serving your layers");
|
|
227
294
|
|
|
228
295
|
spin.start("Finalizing handoff with mother…");
|
|
296
|
+
const peerMultiaddr = extractPeerMultiaddrFromLog(petalsLogPath, fs);
|
|
229
297
|
try {
|
|
230
|
-
await readyHost(selected.mother_url, {
|
|
298
|
+
await readyHost(selected.mother_url, {
|
|
299
|
+
host_id: assignment.host_id,
|
|
300
|
+
peer_multiaddr: peerMultiaddr || undefined,
|
|
301
|
+
});
|
|
231
302
|
} catch (err) {
|
|
232
303
|
spin.stop("Handoff failed");
|
|
233
304
|
p.cancel(err.message);
|
|
@@ -242,6 +313,8 @@ async function wizard(args) {
|
|
|
242
313
|
block_indices: assignment.block_indices,
|
|
243
314
|
layers_hosted: assignment.layers_hosted,
|
|
244
315
|
public_ip: String(publicIp).trim(),
|
|
316
|
+
network_mode: networkMode,
|
|
317
|
+
announce_maddrs: announceMaddrs || null,
|
|
245
318
|
shard_manager_url: shardManagerUrl,
|
|
246
319
|
shard_pid: pid,
|
|
247
320
|
discovery_url: args.discoveryUrl,
|
|
@@ -297,7 +370,7 @@ async function showStatus() {
|
|
|
297
370
|
console.log(` mother: ${state.mother_url}`);
|
|
298
371
|
console.log(` public_ip: ${state.public_ip}`);
|
|
299
372
|
try {
|
|
300
|
-
const statusHost =
|
|
373
|
+
const statusHost = "127.0.0.1";
|
|
301
374
|
const res = await fetch(`http://${statusHost}:${SHARD_PORT}/status`);
|
|
302
375
|
if (res.ok) {
|
|
303
376
|
const st = await res.json();
|
|
@@ -325,7 +398,7 @@ async function doLeave() {
|
|
|
325
398
|
p.log.warn(`Mother leave call failed: ${err.message}`);
|
|
326
399
|
}
|
|
327
400
|
try {
|
|
328
|
-
const statusHost =
|
|
401
|
+
const statusHost = "127.0.0.1";
|
|
329
402
|
await fetch(`http://${statusHost}:${SHARD_PORT}/stop`, { method: "POST" });
|
|
330
403
|
} catch {
|
|
331
404
|
/* ignore */
|