golem-vm-provider 0.1.55__py3-none-any.whl → 0.1.56__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.
- {golem_vm_provider-0.1.55.dist-info → golem_vm_provider-0.1.56.dist-info}/METADATA +74 -8
- {golem_vm_provider-0.1.55.dist-info → golem_vm_provider-0.1.56.dist-info}/RECORD +6 -6
- provider/main.py +534 -0
- provider/network/port_verifier.py +32 -34
- {golem_vm_provider-0.1.55.dist-info → golem_vm_provider-0.1.56.dist-info}/WHEEL +0 -0
- {golem_vm_provider-0.1.55.dist-info → golem_vm_provider-0.1.56.dist-info}/entry_points.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: golem-vm-provider
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.56
|
4
4
|
Summary: VM on Golem Provider Node - Run your own provider node to offer VMs on the Golem Network
|
5
5
|
Keywords: golem,vm,provider,cloud,decentralized
|
6
6
|
Author: Phillip Jensen
|
@@ -61,6 +61,69 @@ pip install golem-vm-provider
|
|
61
61
|
golem-provider start --network testnet
|
62
62
|
```
|
63
63
|
|
64
|
+
Verify your environment and connectivity anytime:
|
65
|
+
|
66
|
+
```bash
|
67
|
+
golem-provider status
|
68
|
+
```
|
69
|
+
This checks Multipass availability, local/external port reachability, and whether an update is available on PyPI.
|
70
|
+
|
71
|
+
### Status Command (TTY and JSON)
|
72
|
+
|
73
|
+
Use `golem-provider status` to quickly assess health.
|
74
|
+
|
75
|
+
TTY output highlights
|
76
|
+
|
77
|
+
```
|
78
|
+
Overall Error | Issues detected | Healthy
|
79
|
+
|
80
|
+
Multipass ✅ OK | ❌ Missing
|
81
|
+
|
82
|
+
Provider Port 0.0.0.0:7466
|
83
|
+
Local ✅ service is listening | ❌ port unavailable
|
84
|
+
External ✅ reachable | ❌ unreachable — <reason>
|
85
|
+
|
86
|
+
SSH Ports <start>-<end> — OK | limited — N issue(s) | blocked
|
87
|
+
Usable free <count> # free AND externally reachable
|
88
|
+
In use <count>
|
89
|
+
Issues e.g. "100 not reachable externally" or "3 unreachable, 1 not listening"
|
90
|
+
```
|
91
|
+
|
92
|
+
Severity rules
|
93
|
+
|
94
|
+
- Overall is Error when any critical prerequisite fails:
|
95
|
+
- Provider API port not externally reachable (or external check fails).
|
96
|
+
- No externally reachable SSH ports in the configured range.
|
97
|
+
- Multipass missing or provider local port not ready.
|
98
|
+
- Otherwise it shows Issues detected or Healthy.
|
99
|
+
|
100
|
+
Machine‑readable JSON
|
101
|
+
|
102
|
+
```bash
|
103
|
+
golem-provider status --json
|
104
|
+
```
|
105
|
+
|
106
|
+
Key fields:
|
107
|
+
|
108
|
+
- `overall.status`: "healthy" | "issues" | "error"
|
109
|
+
- `overall.issues`: list of concise issue strings
|
110
|
+
- `ports.provider`:
|
111
|
+
- `port`: int, `host`: string
|
112
|
+
- `status`: "reachable" | "unreachable" (external check failures are treated as "unreachable")
|
113
|
+
- `ports.ssh`:
|
114
|
+
- `range`: [start, end)
|
115
|
+
- `status`: "ok" | "limited" | "blocked"
|
116
|
+
- `usable_free`: integer — free AND externally reachable
|
117
|
+
- `in_use`: integer
|
118
|
+
- `issues`: `{ unreachable: int, not_listening: int }`
|
119
|
+
- `ports`: array of per‑port summaries:
|
120
|
+
- `{ port: int, status: "reachable" | "unreachable" | "unknown", listening: bool }`
|
121
|
+
|
122
|
+
Notes
|
123
|
+
|
124
|
+
- The concept of "free" in JSON is replaced by `usable_free` (free + externally reachable) to avoid misleading counts when ports are blocked.
|
125
|
+
- When the external checker is unavailable, per‑port `status` is `"unknown"` and `listening` still reflects local state.
|
126
|
+
|
64
127
|
3) Set pricing in USD (GLM rates auto‑compute):
|
65
128
|
|
66
129
|
```bash
|
@@ -592,13 +655,16 @@ The provider includes real-time port verification status:
|
|
592
655
|
|
593
656
|
Example status output:
|
594
657
|
|
595
|
-
```
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
658
|
+
```
|
659
|
+
Overall Healthy
|
660
|
+
|
661
|
+
Provider Port {host}:{provider_port}
|
662
|
+
Local ✅ service is listening
|
663
|
+
External ✅ reachable
|
664
|
+
|
665
|
+
SSH Ports {start_port}-{end_port_minus_one} — OK
|
666
|
+
Usable free {usable_free}
|
667
|
+
In use {in_use}
|
602
668
|
```
|
603
669
|
|
604
670
|
### Resource Allocation Issues
|
@@ -13,8 +13,8 @@ provider/discovery/multi_advertiser.py,sha256=_J79wA1-XQ4GsLzt9KrKpWigGSGBqtut7D
|
|
13
13
|
provider/discovery/resource_monitor.py,sha256=AmiEc7yBGEGXCunQ-QKmVgosDX3gOhK1Y58LJZXrwAs,949
|
14
14
|
provider/discovery/resource_tracker.py,sha256=MP7IXd3aIMsjB4xz5Oj9zFDTEnvrnw-Cyxpl33xcJcc,6006
|
15
15
|
provider/discovery/service.py,sha256=vX_mVSxvn3arnb2cKDM_SeJp1ZgPdImP2aUubeXgdRg,915
|
16
|
-
provider/main.py,sha256=
|
17
|
-
provider/network/port_verifier.py,sha256=
|
16
|
+
provider/main.py,sha256=ozyvQc-1oNLznwr09gYdbGBkYncw-1ejdOdNhxOPcTo,52788
|
17
|
+
provider/network/port_verifier.py,sha256=mlSzr9Z-W5Z5mL3EYg4zemgGoi8Z5ebNoeFgLGRaoH4,13253
|
18
18
|
provider/payments/blockchain_service.py,sha256=4GrzDKwCSUVoENqjD4RLyJ0qwBOJKMyVk5Li-XNsyTc,3567
|
19
19
|
provider/payments/monitor.py,sha256=seo8vE622IdbcRE3x69IpvHn2mel_tlMNGt_DxOIoww,5386
|
20
20
|
provider/payments/stream_map.py,sha256=qk6Y8hS72DplAifZ0ZMWPHBAyc_3IWIQyWUBuCU3_To,1191
|
@@ -39,7 +39,7 @@ provider/vm/port_manager.py,sha256=iYSwjTjD_ziOhG8aI7juKHw1OwwRUTJQyQoRUNQvz9w,1
|
|
39
39
|
provider/vm/provider.py,sha256=A7QN89EJjcSS40_SmKeinG1Jp_NGffJaLse-XdKciAs,1164
|
40
40
|
provider/vm/proxy_manager.py,sha256=n4NTsyz2rtrvjtf_ceKBk-g2q_mzqPwruB1q7UlQVBc,14928
|
41
41
|
provider/vm/service.py,sha256=Ki4SGNIZUq3XmaPMwAOoNzdZzKQsmFXid374wgjFPes,4636
|
42
|
-
golem_vm_provider-0.1.
|
43
|
-
golem_vm_provider-0.1.
|
44
|
-
golem_vm_provider-0.1.
|
45
|
-
golem_vm_provider-0.1.
|
42
|
+
golem_vm_provider-0.1.56.dist-info/METADATA,sha256=D83fVjgeQlLJN2XzW_XGGBWK0g0PAe0nOp87hzjgLjU,20932
|
43
|
+
golem_vm_provider-0.1.56.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
44
|
+
golem_vm_provider-0.1.56.dist-info/entry_points.txt,sha256=5Jiie1dIXygmxmDW66bKKxQpmBLJ7leSKRrb8bkQALw,52
|
45
|
+
golem_vm_provider-0.1.56.dist-info/RECORD,,
|
provider/main.py
CHANGED
@@ -143,6 +143,540 @@ def main():
|
|
143
143
|
return
|
144
144
|
|
145
145
|
|
146
|
+
def _get_installed_version(pkg_name: str) -> str:
|
147
|
+
try:
|
148
|
+
return metadata.version(pkg_name)
|
149
|
+
except Exception:
|
150
|
+
return "unknown"
|
151
|
+
|
152
|
+
|
153
|
+
def _get_latest_version_from_pypi(pkg_name: str) -> Optional[str]:
|
154
|
+
# Avoid network in pytest runs
|
155
|
+
if os.environ.get("PYTEST_CURRENT_TEST"):
|
156
|
+
return None
|
157
|
+
try:
|
158
|
+
import json as _json
|
159
|
+
from urllib.request import urlopen
|
160
|
+
with urlopen(f"https://pypi.org/pypi/{pkg_name}/json", timeout=5) as resp:
|
161
|
+
data = _json.loads(resp.read().decode("utf-8"))
|
162
|
+
return data.get("info", {}).get("version")
|
163
|
+
except Exception:
|
164
|
+
return None
|
165
|
+
|
166
|
+
|
167
|
+
@cli.command("status")
|
168
|
+
def status(json_out: bool = typer.Option(False, "--json", help="Output machine-readable JSON")):
|
169
|
+
"""Show provider environment status and update info (pretty or JSON)."""
|
170
|
+
from .utils.logging import logger as _logger
|
171
|
+
from rich.console import Console
|
172
|
+
from rich.table import Table
|
173
|
+
from rich.panel import Panel
|
174
|
+
from rich import box
|
175
|
+
|
176
|
+
# Temporarily quiet info logs during checks for cleaner UI
|
177
|
+
prev_level = _logger.level
|
178
|
+
try:
|
179
|
+
_logger.setLevel("WARNING")
|
180
|
+
except Exception:
|
181
|
+
pass
|
182
|
+
|
183
|
+
# Silence port_verifier warnings during status checks for clean UI
|
184
|
+
import logging as _logging
|
185
|
+
try:
|
186
|
+
_pv_logger = _logging.getLogger("provider.network.port_verifier")
|
187
|
+
_prev_pv_level = _pv_logger.level
|
188
|
+
_pv_logger.setLevel(_logging.CRITICAL)
|
189
|
+
except Exception:
|
190
|
+
_pv_logger = None
|
191
|
+
_prev_pv_level = None
|
192
|
+
# Also quiet config auto-detection logs (e.g., multipass path) for clean JSON/TTY
|
193
|
+
try:
|
194
|
+
_cfg_logger = _logging.getLogger("provider.config")
|
195
|
+
_prev_cfg_level = _cfg_logger.level
|
196
|
+
_cfg_logger.setLevel(_logging.WARNING)
|
197
|
+
except Exception:
|
198
|
+
_cfg_logger = None
|
199
|
+
_prev_cfg_level = None
|
200
|
+
|
201
|
+
# Defer config-heavy imports until after log levels are adjusted
|
202
|
+
from .config import settings as _settings
|
203
|
+
from .network.port_verifier import PortVerifier
|
204
|
+
|
205
|
+
# Versions
|
206
|
+
pkg = "golem-vm-provider"
|
207
|
+
current = _get_installed_version(pkg)
|
208
|
+
latest = _get_latest_version_from_pypi(pkg)
|
209
|
+
update_available = bool(latest and current != latest)
|
210
|
+
|
211
|
+
# Environment
|
212
|
+
env = os.environ.get("GOLEM_PROVIDER_ENVIRONMENT", _settings.ENVIRONMENT)
|
213
|
+
net = getattr(_settings, "NETWORK", None)
|
214
|
+
dev_mode = env == "development" or bool(getattr(_settings, "DEV_MODE", False))
|
215
|
+
|
216
|
+
# Multipass
|
217
|
+
mp = {"ok": False, "path": None, "version": None, "error": None}
|
218
|
+
try:
|
219
|
+
mp_path = _settings.MULTIPASS_BINARY_PATH
|
220
|
+
mp["path"] = mp_path or None
|
221
|
+
if mp_path:
|
222
|
+
import subprocess
|
223
|
+
r = subprocess.run([mp_path, "version"], capture_output=True, text=True, timeout=5)
|
224
|
+
if r.returncode == 0:
|
225
|
+
mp["ok"] = True
|
226
|
+
mp["version"] = (r.stdout or r.stderr).strip()
|
227
|
+
else:
|
228
|
+
mp["error"] = (r.stderr or r.stdout or "failed").strip()
|
229
|
+
else:
|
230
|
+
mp["error"] = "not configured"
|
231
|
+
except Exception as e:
|
232
|
+
mp["ok"] = False
|
233
|
+
mp["error"] = str(e)
|
234
|
+
|
235
|
+
# Provider port (local)
|
236
|
+
port = int(_settings.PORT)
|
237
|
+
host = getattr(_settings, "HOST", "0.0.0.0")
|
238
|
+
local = {"ok": False, "detail": None}
|
239
|
+
try:
|
240
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
241
|
+
s.settimeout(1)
|
242
|
+
local_conn = s.connect_ex(("127.0.0.1", port)) == 0
|
243
|
+
s.close()
|
244
|
+
if local_conn:
|
245
|
+
local["ok"] = True
|
246
|
+
local["detail"] = "service is listening"
|
247
|
+
else:
|
248
|
+
# Check that we can bind (port free)
|
249
|
+
if asyncio.run(verify_provider_port(port)):
|
250
|
+
local["ok"] = True
|
251
|
+
local["detail"] = "port is free (bindable)"
|
252
|
+
else:
|
253
|
+
local["ok"] = False
|
254
|
+
local["detail"] = "port unavailable"
|
255
|
+
except Exception as e:
|
256
|
+
local["ok"] = False
|
257
|
+
local["detail"] = str(e)
|
258
|
+
|
259
|
+
# Always use shared external port-checker for public reachability
|
260
|
+
servers = ["http://195.201.39.101:9000"]
|
261
|
+
|
262
|
+
external = {"status": "unknown", "verified_by": None, "error": None}
|
263
|
+
try:
|
264
|
+
verifier = PortVerifier(servers, discovery_port=port)
|
265
|
+
results = asyncio.run(verifier.verify_external_access({port}))
|
266
|
+
r = results.get(port)
|
267
|
+
if r and r.accessible:
|
268
|
+
external["status"] = "reachable"
|
269
|
+
external["verified_by"] = r.verified_by
|
270
|
+
elif r:
|
271
|
+
external["status"] = "unreachable"
|
272
|
+
external["error"] = r.error
|
273
|
+
else:
|
274
|
+
external["status"] = "not_verified"
|
275
|
+
except Exception as e:
|
276
|
+
external["status"] = "check_failed"
|
277
|
+
external["error"] = str(e).splitlines()[0]
|
278
|
+
|
279
|
+
# Base data structure
|
280
|
+
data = {
|
281
|
+
"version": {
|
282
|
+
"installed": current,
|
283
|
+
"latest": latest,
|
284
|
+
"update_available": update_available,
|
285
|
+
},
|
286
|
+
"environment": {
|
287
|
+
"environment": env,
|
288
|
+
"network": net,
|
289
|
+
"dev_mode": dev_mode,
|
290
|
+
},
|
291
|
+
"multipass": mp,
|
292
|
+
"ports": {
|
293
|
+
"provider": {
|
294
|
+
"port": port,
|
295
|
+
"host": host,
|
296
|
+
"local_ok": local["ok"],
|
297
|
+
"local_detail": local["detail"],
|
298
|
+
"external": external,
|
299
|
+
}
|
300
|
+
},
|
301
|
+
}
|
302
|
+
|
303
|
+
# SSH port usage summary from state file + external reachability for full range
|
304
|
+
try:
|
305
|
+
from pathlib import Path as _Path
|
306
|
+
import json as _json
|
307
|
+
state_path = _Path(_settings.PROXY_STATE_DIR) / "ports.json"
|
308
|
+
ports_in_use = []
|
309
|
+
if state_path.exists():
|
310
|
+
with open(state_path, "r") as fh:
|
311
|
+
st = _json.load(fh)
|
312
|
+
for _req_name, pinfo in (st.get("proxies", {}) or {}).items():
|
313
|
+
prt = pinfo.get("port")
|
314
|
+
if isinstance(prt, int):
|
315
|
+
ports_in_use.append(prt)
|
316
|
+
start = int(getattr(_settings, "PORT_RANGE_START", 50800))
|
317
|
+
end = int(getattr(_settings, "PORT_RANGE_END", 50900))
|
318
|
+
total = max(0, end - start)
|
319
|
+
used = sorted(set(prt for prt in ports_in_use if start <= prt < end))
|
320
|
+
# Check if used ports are actually listening
|
321
|
+
used_listening = []
|
322
|
+
used_not_listening = []
|
323
|
+
for prt in used:
|
324
|
+
try:
|
325
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
326
|
+
s.settimeout(0.5)
|
327
|
+
ok = s.connect_ex(("127.0.0.1", prt)) == 0
|
328
|
+
s.close()
|
329
|
+
(used_listening if ok else used_not_listening).append(prt)
|
330
|
+
except Exception:
|
331
|
+
used_not_listening.append(prt)
|
332
|
+
free_count = total - len(used)
|
333
|
+
# External reachability across entire range
|
334
|
+
external_ok: set[int] = set()
|
335
|
+
ext_results_map: dict[int, bool] = {}
|
336
|
+
external_batch_ok = False
|
337
|
+
external_batch_error: str | None = None
|
338
|
+
try:
|
339
|
+
verifier_all = PortVerifier(servers, discovery_port=port)
|
340
|
+
_ext_results = asyncio.run(verifier_all.verify_external_access(set(range(start, end))))
|
341
|
+
for prt, res in _ext_results.items():
|
342
|
+
p = int(prt)
|
343
|
+
ok = bool(getattr(res, "accessible", False))
|
344
|
+
ext_results_map[p] = ok
|
345
|
+
if ok:
|
346
|
+
external_ok.add(p)
|
347
|
+
external_batch_ok = True
|
348
|
+
except Exception:
|
349
|
+
# Leave external_ok empty on failure
|
350
|
+
external_batch_ok = False
|
351
|
+
try:
|
352
|
+
external_batch_error = str(_) # type: ignore[name-defined]
|
353
|
+
except Exception:
|
354
|
+
external_batch_error = None
|
355
|
+
|
356
|
+
firewall_issues = [p for p in used_listening if p not in external_ok]
|
357
|
+
|
358
|
+
# Build per-port details for JSON consumers
|
359
|
+
details = []
|
360
|
+
for p in range(start, end):
|
361
|
+
details.append({
|
362
|
+
"port": p,
|
363
|
+
"in_use": p in used,
|
364
|
+
"local_listening": p in used_listening,
|
365
|
+
"external_reachable": bool(ext_results_map.get(p, False)) if external_batch_ok else False,
|
366
|
+
})
|
367
|
+
|
368
|
+
# Compute number of free ports that are actually externally reachable (usable)
|
369
|
+
usable_free_count = None
|
370
|
+
try:
|
371
|
+
if external_batch_ok:
|
372
|
+
usable_free_count = len([
|
373
|
+
p for p in range(start, end)
|
374
|
+
if (p not in used) and bool(ext_results_map.get(p, False))
|
375
|
+
])
|
376
|
+
except Exception:
|
377
|
+
usable_free_count = None
|
378
|
+
|
379
|
+
# Legacy detailed metrics (retained under ssh_legacy)
|
380
|
+
_ssh_legacy = {
|
381
|
+
"range": [start, end],
|
382
|
+
"total": total,
|
383
|
+
"in_use": used,
|
384
|
+
"listening_ok": used_listening,
|
385
|
+
"listening_issues": used_not_listening,
|
386
|
+
"free_count": free_count,
|
387
|
+
"external_reachable_count": len([p for p in range(start, end) if p in external_ok]),
|
388
|
+
"firewall_issues_count": len(firewall_issues),
|
389
|
+
"external_checked": external_batch_ok,
|
390
|
+
"external_error": external_batch_error,
|
391
|
+
"usable_free_count": usable_free_count,
|
392
|
+
"details": details,
|
393
|
+
}
|
394
|
+
|
395
|
+
# Concise status fields for programmatic checks (mirrors TTY)
|
396
|
+
_ext_reach = int(len(external_ok))
|
397
|
+
_issues_count = len(used_not_listening) + len(firewall_issues)
|
398
|
+
if (not external_batch_ok) or _ext_reach == 0:
|
399
|
+
ssh_status = "blocked"
|
400
|
+
elif _issues_count > 0:
|
401
|
+
ssh_status = "limited"
|
402
|
+
else:
|
403
|
+
ssh_status = "ok"
|
404
|
+
|
405
|
+
# Usable free: default to 0 when external check failed
|
406
|
+
if usable_free_count is None:
|
407
|
+
usable_free_out = 0
|
408
|
+
else:
|
409
|
+
usable_free_out = int(usable_free_count)
|
410
|
+
|
411
|
+
# Issues breakdown matching TTY wording
|
412
|
+
unreachable_count = total if (not external_batch_ok or _ext_reach == 0) else len(firewall_issues)
|
413
|
+
not_listening_count = len(used_not_listening)
|
414
|
+
|
415
|
+
# Build minimal per-port status list for JSON consumers
|
416
|
+
# Consistent definition:
|
417
|
+
# - status: reachable | unreachable | unknown (unknown only if external check failed)
|
418
|
+
# - listening: true | false
|
419
|
+
ports_detail: list[dict] = []
|
420
|
+
for p in range(start, end):
|
421
|
+
listening = p in used_listening
|
422
|
+
if external_batch_ok:
|
423
|
+
status_val = "reachable" if bool(ext_results_map.get(p, False)) else "unreachable"
|
424
|
+
else:
|
425
|
+
status_val = "unknown"
|
426
|
+
ports_detail.append({
|
427
|
+
"port": p,
|
428
|
+
"status": status_val,
|
429
|
+
"listening": bool(listening),
|
430
|
+
})
|
431
|
+
|
432
|
+
data["ports"]["ssh"] = {
|
433
|
+
"range": [start, end],
|
434
|
+
"status": ssh_status,
|
435
|
+
"usable_free": usable_free_out,
|
436
|
+
"in_use": len(used),
|
437
|
+
"issues": {
|
438
|
+
"unreachable": int(unreachable_count),
|
439
|
+
"not_listening": int(not_listening_count),
|
440
|
+
},
|
441
|
+
"ports": ports_detail,
|
442
|
+
}
|
443
|
+
|
444
|
+
except Exception:
|
445
|
+
# Non-fatal; omit ssh summary if state not available
|
446
|
+
pass
|
447
|
+
|
448
|
+
# Provider concise status: reachable | unreachable (treat check failures as unreachable)
|
449
|
+
prov_status = external.get("status")
|
450
|
+
provider_status = "reachable" if prov_status == "reachable" else "unreachable"
|
451
|
+
data["ports"]["provider"]["status"] = provider_status
|
452
|
+
|
453
|
+
# Compute overall and issues for JSON output (mirrors condensed model)
|
454
|
+
json_issues = []
|
455
|
+
json_ssh_blocked = False
|
456
|
+
json_critical_no_ssh = False
|
457
|
+
# Multipass
|
458
|
+
if not mp["ok"]:
|
459
|
+
json_issues.append("Multipass not available")
|
460
|
+
# Provider local port
|
461
|
+
if not local["ok"]:
|
462
|
+
json_issues.append(f"Provider port {port} not ready")
|
463
|
+
# SSH ports
|
464
|
+
if data["ports"].get("ssh"):
|
465
|
+
_ssh = data["ports"]["ssh"]
|
466
|
+
_status = str(_ssh.get("status") or "blocked").lower()
|
467
|
+
_issues = _ssh.get("issues") or {}
|
468
|
+
_not_listening = int(_issues.get("not_listening", 0) or 0)
|
469
|
+
_unreachable = int(_issues.get("unreachable", 0) or 0)
|
470
|
+
if _status == "blocked":
|
471
|
+
json_ssh_blocked = True
|
472
|
+
json_critical_no_ssh = True
|
473
|
+
json_issues.append("No externally reachable SSH ports")
|
474
|
+
else:
|
475
|
+
if _unreachable > 0:
|
476
|
+
json_issues.append(f"{_unreachable} SSH port(s) unreachable externally")
|
477
|
+
if _not_listening > 0:
|
478
|
+
json_issues.append(f"{_not_listening} SSH port(s) not listening")
|
479
|
+
# Provider external
|
480
|
+
json_critical_provider_external = False
|
481
|
+
if external.get("status") in ("unreachable", "check_failed"):
|
482
|
+
json_issues.append("Provider API port not reachable externally")
|
483
|
+
json_critical_provider_external = True
|
484
|
+
|
485
|
+
if json_critical_no_ssh or (not local["ok"]) or (not mp["ok"]) or json_critical_provider_external:
|
486
|
+
overall_status = "error"
|
487
|
+
else:
|
488
|
+
overall_status = "healthy" if (not json_issues and not json_ssh_blocked) else "issues"
|
489
|
+
|
490
|
+
data["overall"] = {"status": overall_status, "issues": json_issues}
|
491
|
+
|
492
|
+
if json_out:
|
493
|
+
import json as _json
|
494
|
+
print(_json.dumps(data, indent=2))
|
495
|
+
# Restore logger level
|
496
|
+
try:
|
497
|
+
_logger.setLevel(prev_level)
|
498
|
+
except Exception:
|
499
|
+
pass
|
500
|
+
if _pv_logger and _prev_pv_level is not None:
|
501
|
+
try:
|
502
|
+
_pv_logger.setLevel(_prev_pv_level)
|
503
|
+
except Exception:
|
504
|
+
pass
|
505
|
+
if _cfg_logger and _prev_cfg_level is not None:
|
506
|
+
try:
|
507
|
+
_cfg_logger.setLevel(_prev_cfg_level)
|
508
|
+
except Exception:
|
509
|
+
pass
|
510
|
+
return
|
511
|
+
|
512
|
+
console = Console()
|
513
|
+
|
514
|
+
# Overall status
|
515
|
+
issues = []
|
516
|
+
if not mp["ok"]:
|
517
|
+
issues.append("Multipass not available")
|
518
|
+
if not local["ok"]:
|
519
|
+
issues.append(f"Provider port {port} not ready")
|
520
|
+
ssh_blocked = False
|
521
|
+
critical_no_ssh = False
|
522
|
+
if data["ports"].get("ssh"):
|
523
|
+
ssh = data["ports"]["ssh"]
|
524
|
+
if ssh.get("listening_issues"):
|
525
|
+
issues.append(f"{len(ssh['listening_issues'])} SSH port(s) not listening")
|
526
|
+
if ssh.get("free_count", 0) == 0:
|
527
|
+
issues.append("No free SSH ports available")
|
528
|
+
if ssh.get("external_checked"):
|
529
|
+
if int(ssh.get("external_reachable_count", 0) or 0) == 0:
|
530
|
+
ssh_blocked = True
|
531
|
+
critical_no_ssh = True # No externally reachable SSH ports is critical
|
532
|
+
issues.append("No externally reachable SSH ports")
|
533
|
+
else:
|
534
|
+
# If we couldn't check, mark as issue but not critical
|
535
|
+
issues.append("SSH external reachability check failed")
|
536
|
+
critical_provider_external = False
|
537
|
+
if external["status"] in ("unreachable", "check_failed"):
|
538
|
+
issues.append("Provider API port not reachable externally")
|
539
|
+
critical_provider_external = True
|
540
|
+
|
541
|
+
# Severity: Error when critical conditions are met; else Issues/Healthy
|
542
|
+
if critical_no_ssh or (not local["ok"]) or (not mp["ok"]) or critical_provider_external:
|
543
|
+
overall = "Error"
|
544
|
+
else:
|
545
|
+
overall = "Healthy" if (not issues and not ssh_blocked) else "Issues detected"
|
546
|
+
|
547
|
+
# Build a single compact table
|
548
|
+
tbl = Table(box=box.SIMPLE_HEAVY, show_header=False, pad_edge=False)
|
549
|
+
tbl.add_column("Item", style="bold")
|
550
|
+
tbl.add_column("Value")
|
551
|
+
|
552
|
+
# Header
|
553
|
+
if overall == "Healthy":
|
554
|
+
overall_txt = "[green]Healthy[/green]"
|
555
|
+
elif overall == "Error":
|
556
|
+
overall_txt = "[red]Error[/red]"
|
557
|
+
else:
|
558
|
+
overall_txt = f"[yellow]{overall}[/yellow]"
|
559
|
+
tbl.add_row("Overall", overall_txt)
|
560
|
+
tbl.add_row("", "")
|
561
|
+
|
562
|
+
# Versions
|
563
|
+
tbl.add_row("Versions", "")
|
564
|
+
ver_inst = data["version"]["installed"] or "unknown"
|
565
|
+
ver_latest = data["version"]["latest"] or "unknown"
|
566
|
+
upd = data["version"]["update_available"]
|
567
|
+
tbl.add_row(" Installed", f"[white]{ver_inst}[/white]")
|
568
|
+
if upd:
|
569
|
+
tbl.add_row(" Latest", f"[bold bright_yellow]{ver_latest}[/bold bright_yellow] [grey62](pip install -U golem-vm-provider)[/grey62]")
|
570
|
+
tbl.add_row(" Update", "[bold bright_yellow]⬆️ yes[/bold bright_yellow]")
|
571
|
+
else:
|
572
|
+
tbl.add_row(" Latest", f"[cyan]{ver_latest}[/cyan]")
|
573
|
+
tbl.add_row(" Update", "[green]no[/green]")
|
574
|
+
tbl.add_row("", "")
|
575
|
+
|
576
|
+
# Environment
|
577
|
+
tbl.add_row("Environment", "")
|
578
|
+
tbl.add_row(" Environment", env + (" (dev)" if dev_mode else ""))
|
579
|
+
tbl.add_row(" Network", net or "-")
|
580
|
+
tbl.add_row("", "")
|
581
|
+
|
582
|
+
# Multipass
|
583
|
+
mp_ver = (mp.get("version") or mp.get("error") or "-").replace("\n", ", ")
|
584
|
+
tbl.add_row("Multipass", "")
|
585
|
+
tbl.add_row(" Status", "✅ OK" if mp["ok"] else "❌ Missing")
|
586
|
+
tbl.add_row(" Path", mp.get("path") or "-")
|
587
|
+
tbl.add_row(" Version", mp_ver)
|
588
|
+
tbl.add_row("", "")
|
589
|
+
|
590
|
+
# Provider port
|
591
|
+
tbl.add_row("Provider Port", f"{host}:{port}")
|
592
|
+
tbl.add_row(" Local", ("✅ " if local["ok"] else "❌ ") + (local["detail"] or ""))
|
593
|
+
# External reachability is foundational; treat unreachable and check failures the same
|
594
|
+
_ext = external.get("status") or "unknown"
|
595
|
+
_err = external.get("error")
|
596
|
+
if _ext == "reachable":
|
597
|
+
ext_row = "✅ reachable"
|
598
|
+
elif _ext in ("unreachable", "check_failed"):
|
599
|
+
ext_row = "❌ unreachable" + (f" — {_err}" if _err else "")
|
600
|
+
elif _ext == "not_verified":
|
601
|
+
ext_row = "⚠️ not verified"
|
602
|
+
else:
|
603
|
+
ext_row = "⚠️ " + _ext + (f" — {_err}" if _err else "")
|
604
|
+
tbl.add_row(" External", ext_row)
|
605
|
+
|
606
|
+
# SSH ports (condensed, actionable)
|
607
|
+
if data["ports"].get("ssh"):
|
608
|
+
ssh = data["ports"]["ssh"]
|
609
|
+
r0, r1 = ssh['range'][0], ssh['range'][1]-1
|
610
|
+
tbl.add_row("", "")
|
611
|
+
status_val = str(ssh.get("status") or "blocked").lower()
|
612
|
+
issues_obj = ssh.get("issues") or {}
|
613
|
+
unreachable_issues = int(issues_obj.get("unreachable", 0) or 0)
|
614
|
+
not_listening_issues = int(issues_obj.get("not_listening", 0) or 0)
|
615
|
+
in_use = int(ssh.get("in_use", 0) or 0)
|
616
|
+
usable_free = ssh.get("usable_free")
|
617
|
+
|
618
|
+
# Determine clear status
|
619
|
+
if status_val == "ok":
|
620
|
+
status_txt = "[green]OK[/green]"
|
621
|
+
elif status_val == "limited":
|
622
|
+
status_txt = f"[yellow]limited — {not_listening_issues + (unreachable_issues or 0)} issue(s)[/yellow]"
|
623
|
+
else:
|
624
|
+
status_txt = "[red]blocked[/red]"
|
625
|
+
|
626
|
+
tbl.add_row("SSH Ports", f"{r0}-{r1} — {status_txt}")
|
627
|
+
|
628
|
+
# Provide only the most relevant numbers
|
629
|
+
# Usable free = free and externally reachable; avoid misleading "Free" when blocked
|
630
|
+
tbl.add_row(" Usable free", str(int(usable_free or 0)))
|
631
|
+
tbl.add_row(" In use", str(in_use))
|
632
|
+
if status_val == "blocked":
|
633
|
+
# Show total not reachable externally
|
634
|
+
total_ports = (r1 - r0 + 1)
|
635
|
+
cnt = unreachable_issues if unreachable_issues else total_ports
|
636
|
+
tbl.add_row(" Issues", f"{cnt} not reachable externally")
|
637
|
+
elif (not_listening_issues or unreachable_issues):
|
638
|
+
parts = []
|
639
|
+
if unreachable_issues:
|
640
|
+
parts.append(f"{unreachable_issues} unreachable (listening but blocked)")
|
641
|
+
if not_listening_issues:
|
642
|
+
parts.append(f"{not_listening_issues} not listening")
|
643
|
+
tbl.add_row(" Issues", ", ".join(parts))
|
644
|
+
|
645
|
+
# Issues / Tips combined at bottom
|
646
|
+
# Only show Notes when there are issues
|
647
|
+
if issues:
|
648
|
+
tbl.add_row("", "")
|
649
|
+
tbl.add_row("Issues", "\n".join(f"• {t}" for t in issues))
|
650
|
+
|
651
|
+
console.print(Panel(tbl, title="Provider Status"))
|
652
|
+
|
653
|
+
# Tips
|
654
|
+
tips = []
|
655
|
+
if update_available:
|
656
|
+
tips.append("Upgrade with: pip install -U golem-vm-provider")
|
657
|
+
if not mp["ok"]:
|
658
|
+
tips.append("Install Multipass and/or set GOLEM_PROVIDER_MULTIPASS_BINARY_PATH")
|
659
|
+
if external["status"] != "reachable":
|
660
|
+
tips.append("Ensure at least one port-check server is online (see above)")
|
661
|
+
# Tips are included in the single panel under Notes
|
662
|
+
|
663
|
+
# Restore logger level
|
664
|
+
try:
|
665
|
+
_logger.setLevel(prev_level)
|
666
|
+
except Exception:
|
667
|
+
pass
|
668
|
+
if _pv_logger and _prev_pv_level is not None:
|
669
|
+
try:
|
670
|
+
_pv_logger.setLevel(_prev_pv_level)
|
671
|
+
except Exception:
|
672
|
+
pass
|
673
|
+
if _cfg_logger and _prev_cfg_level is not None:
|
674
|
+
try:
|
675
|
+
_cfg_logger.setLevel(_prev_cfg_level)
|
676
|
+
except Exception:
|
677
|
+
pass
|
678
|
+
|
679
|
+
|
146
680
|
@wallet_app.command("faucet-l2")
|
147
681
|
def wallet_faucet_l2():
|
148
682
|
"""Request L2 faucet funds for the provider's payment address (native ETH)."""
|
@@ -126,8 +126,8 @@ class PortVerifier:
|
|
126
126
|
Returns:
|
127
127
|
Dictionary mapping ports to their verification results
|
128
128
|
"""
|
129
|
-
results = {}
|
130
|
-
attempts = []
|
129
|
+
results: Dict[int, PortVerificationResult] = {}
|
130
|
+
attempts: List[ServerAttempt] = []
|
131
131
|
|
132
132
|
# Try each server
|
133
133
|
for server in self.port_check_servers:
|
@@ -146,27 +146,29 @@ class PortVerifier:
|
|
146
146
|
|
147
147
|
if response.status == 200:
|
148
148
|
data = await response.json()
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
149
|
+
# Treat a 200 response as a successful attempt regardless of overall success flag.
|
150
|
+
# The 'success' field in the checker indicates if any port was reachable, not server health.
|
151
|
+
raw_results = data.get("results", {}) or {}
|
152
|
+
for port_key, result in raw_results.items():
|
153
|
+
try:
|
154
|
+
port = int(port_key)
|
155
|
+
except Exception:
|
156
|
+
# Some implementations might already use ints
|
157
|
+
port = int(result.get("port", 0)) if isinstance(result, dict) else 0
|
158
|
+
if not port:
|
159
|
+
continue
|
160
|
+
accessible = bool(result.get("accessible"))
|
161
|
+
err = result.get("error")
|
162
|
+
if port not in results or (accessible and not results[port].accessible):
|
163
|
+
results[port] = PortVerificationResult(
|
164
|
+
port=port,
|
165
|
+
accessible=accessible,
|
166
|
+
error=err,
|
167
|
+
verified_by=server if accessible else None,
|
168
|
+
attempts=[],
|
169
|
+
)
|
170
|
+
attempts.append(ServerAttempt(server=server, success=True))
|
171
|
+
logger.info(f"Port verification completed using {server}")
|
170
172
|
else:
|
171
173
|
attempts.append(ServerAttempt(
|
172
174
|
server=server,
|
@@ -198,7 +200,7 @@ class PortVerifier:
|
|
198
200
|
))
|
199
201
|
logger.warning(error_msg)
|
200
202
|
|
201
|
-
# If no servers
|
203
|
+
# If no servers responded successfully, fail verification
|
202
204
|
if not any(attempt.success for attempt in attempts):
|
203
205
|
error_msg = (
|
204
206
|
"Failed to connect to any port check servers. Please ensure:\n"
|
@@ -209,19 +211,15 @@ class PortVerifier:
|
|
209
211
|
logger.error(error_msg)
|
210
212
|
raise RuntimeError(error_msg)
|
211
213
|
|
212
|
-
#
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
results = {
|
217
|
-
port: PortVerificationResult(
|
214
|
+
# Ensure all requested ports are present in results; default to inaccessible
|
215
|
+
for port in ports:
|
216
|
+
if port not in results:
|
217
|
+
results[port] = PortVerificationResult(
|
218
218
|
port=port,
|
219
219
|
accessible=False,
|
220
|
-
error=
|
221
|
-
attempts=[]
|
220
|
+
error=None,
|
221
|
+
attempts=[],
|
222
222
|
)
|
223
|
-
for port in ports
|
224
|
-
}
|
225
223
|
|
226
224
|
# Add attempts to all results
|
227
225
|
for result in results.values():
|
File without changes
|
File without changes
|