@le-space/rootfs 0.9.5 → 0.9.6

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/rootfs",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -5,6 +5,7 @@ import ipaddress
5
5
  import json
6
6
  import os
7
7
  import subprocess
8
+ import sys
8
9
  import threading
9
10
  import time
10
11
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -92,6 +93,28 @@ def _parse_env_file(path: str) -> dict[str, str]:
92
93
  return values
93
94
 
94
95
 
96
+ def _log(message: str) -> None:
97
+ """Say it once, on stderr, where journalctl picks it up.
98
+
99
+ This file had no logging and the HTTP handler silenced its own, so a relay
100
+ that never configured itself left a journal with one "Started" line in it.
101
+ That silence is what made "the guest never acknowledged" an unexplainable
102
+ failure rather than a readable one.
103
+ """
104
+ print(f"[bootstrap] {message}", file=sys.stderr, flush=True)
105
+
106
+
107
+ _LOGGED_ONCE: set[str] = set()
108
+
109
+
110
+ def _log_once(key: str, message: str) -> None:
111
+ """For states that repeat every few seconds and are worth saying once."""
112
+ if key in _LOGGED_ONCE:
113
+ return
114
+ _LOGGED_ONCE.add(key)
115
+ _log(message)
116
+
117
+
95
118
  def _extract_guest_bootstrap_locator() -> tuple[str | None, str | None]:
96
119
  """Read the deployment locator the deployer left in our SSH key.
97
120
 
@@ -112,9 +135,15 @@ def _extract_guest_bootstrap_locator() -> tuple[str | None, str | None]:
112
135
  deployment_token = deployment_token.strip()
113
136
  if owner_address and deployment_token:
114
137
  return owner_address, deployment_token
115
- except (FileNotFoundError, ValueError):
138
+ except (FileNotFoundError, ValueError) as error:
139
+ _log_once("locator-error", f"cannot read {AUTHORIZED_KEYS_FILE}: {error!r}")
116
140
  return None, None
117
141
 
142
+ _log_once(
143
+ "locator-missing",
144
+ f"no aleph-bootstrap-config marker in {AUTHORIZED_KEYS_FILE}; "
145
+ "nothing to poll for, the relay will stay unconfigured",
146
+ )
118
147
  return None, None
119
148
 
120
149
 
@@ -374,6 +403,10 @@ def _load_vm_bootstrap_record() -> tuple[str | None, str | None, dict | None]:
374
403
 
375
404
 
376
405
  def _poll_bootstrap_config_loop() -> None:
406
+ _log(
407
+ f"waiting for bootstrap config, polling every {BOOTSTRAP_CONFIG_POLL_SECONDS}s "
408
+ f"against {ALEPH_API_HOST}"
409
+ )
377
410
  while not os.path.exists(READY_FILE):
378
411
  _, deployment_token, record = _load_vm_bootstrap_record()
379
412
  if deployment_token and isinstance(record, dict):
@@ -381,12 +414,20 @@ def _poll_bootstrap_config_loop() -> None:
381
414
  if status == "pending":
382
415
  try:
383
416
  args = _build_configure_args_from_record(record)
384
- except ValueError:
417
+ except ValueError as error:
418
+ # The record arrived and was unusable — which used to look
419
+ # exactly like never having received one.
420
+ _log_once("record-invalid", f"record cannot be turned into a config: {error}")
385
421
  time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
386
422
  continue
387
- ok, _ = _run_configure_process(args, record)
423
+ _log("record found and pending, configuring the relay now")
424
+ ok, detail = _run_configure_process(args, record)
388
425
  if ok:
426
+ _log("relay configured, setup endpoint is done")
389
427
  return
428
+ _log(f"configure failed, will retry: {detail}")
429
+ else:
430
+ _log_once(f"status-{status}", f"record status is {status!r}, not acting on it")
390
431
  time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
391
432
 
392
433
 
@@ -5,6 +5,7 @@ import ipaddress
5
5
  import json
6
6
  import os
7
7
  import subprocess
8
+ import sys
8
9
  import threading
9
10
  import time
10
11
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
@@ -85,6 +86,29 @@ def _validate_proxy_hostname(value: object) -> str | None:
85
86
  return parsed.hostname
86
87
 
87
88
 
89
+ def _log(message: str) -> None:
90
+ """Say it once, on stderr, where journalctl picks it up.
91
+
92
+ This file had no logging at all, and the HTTP handler silenced its own.
93
+ That is how a relay can sit for hours doing nothing while `journalctl -u
94
+ uc-go-peer-bootstrap` shows a single "Started" line — which is what it did
95
+ on 2026-09-02, leaving no way to tell whether the guest had found its
96
+ locator, asked Aleph and been told nothing, or never looked.
97
+ """
98
+ print(f"[bootstrap] {message}", file=sys.stderr, flush=True)
99
+
100
+
101
+ _LOGGED_ONCE: set[str] = set()
102
+
103
+
104
+ def _log_once(key: str, message: str) -> None:
105
+ """For states that repeat every few seconds and are worth saying once."""
106
+ if key in _LOGGED_ONCE:
107
+ return
108
+ _LOGGED_ONCE.add(key)
109
+ _log(message)
110
+
111
+
88
112
  def _extract_guest_bootstrap_locator() -> tuple[str | None, str | None]:
89
113
  try:
90
114
  with open(AUTHORIZED_KEYS_FILE, encoding="utf-8") as handle:
@@ -98,10 +122,25 @@ def _extract_guest_bootstrap_locator() -> tuple[str | None, str | None]:
98
122
  owner_address = owner_address.strip()
99
123
  deployment_token = deployment_token.strip()
100
124
  if owner_address and deployment_token:
125
+ _log_once(
126
+ "locator",
127
+ f"locator found in authorized_keys: owner={owner_address} "
128
+ f"token={deployment_token}",
129
+ )
101
130
  return owner_address, deployment_token
102
- except (FileNotFoundError, ValueError):
131
+ except (FileNotFoundError, ValueError) as error:
132
+ _log_once("locator-error", f"cannot read {AUTHORIZED_KEYS_FILE}: {error!r}")
103
133
  return None, None
104
134
 
135
+ # Without this the guest can never find its record, and every deployment
136
+ # spends its full config window before failing over. The browser appends
137
+ # the marker to the SSH key it puts in the instance message; if it is not
138
+ # here, either no key was configured or something stripped the comment.
139
+ _log_once(
140
+ "locator-missing",
141
+ f"no aleph-bootstrap-config marker in {AUTHORIZED_KEYS_FILE}; "
142
+ "nothing to poll for, the relay will stay unconfigured",
143
+ )
105
144
  return None, None
106
145
 
107
146
 
@@ -415,10 +454,13 @@ def _load_vm_bootstrap_record() -> tuple[str | None, str | None, dict | None]:
415
454
  with urlopen(request_url, timeout=10) as response:
416
455
  payload = json.loads(response.read().decode("utf-8") or "{}")
417
456
  except HTTPError as error:
418
- if error.code == 404:
419
- return owner_address, deployment_token, None
457
+ # A 404 is ordinary early on: the browser publishes the aggregate only
458
+ # once the runtime is up. Anything else is worth naming.
459
+ key = "aggregate-404" if error.code == 404 else f"aggregate-{error.code}"
460
+ _log_once(key, f"aggregate query returned HTTP {error.code} for {owner_address}")
420
461
  return owner_address, deployment_token, None
421
- except (URLError, json.JSONDecodeError, TimeoutError):
462
+ except (URLError, json.JSONDecodeError, TimeoutError) as error:
463
+ _log_once("aggregate-unreachable", f"aggregate query failed: {error!r}")
422
464
  return owner_address, deployment_token, None
423
465
 
424
466
  aggregate = payload.get("data", {}).get(BOOTSTRAP_CONFIG_KEY, {})
@@ -426,25 +468,47 @@ def _load_vm_bootstrap_record() -> tuple[str | None, str | None, dict | None]:
426
468
  return owner_address, deployment_token, None
427
469
 
428
470
  record = aggregate.get(deployment_token)
429
- return owner_address, deployment_token, record if isinstance(record, dict) else None
471
+ if not isinstance(record, dict):
472
+ _log_once(
473
+ "record-missing",
474
+ f"aggregate has no record for token {deployment_token} "
475
+ f"({len(aggregate)} record(s) present)",
476
+ )
477
+ return owner_address, deployment_token, None
478
+
479
+ return owner_address, deployment_token, record
430
480
 
431
481
 
432
482
  def _poll_bootstrap_config_loop() -> None:
483
+ _log(
484
+ f"waiting for bootstrap config, polling every {BOOTSTRAP_CONFIG_POLL_SECONDS}s "
485
+ f"against {ALEPH_API_HOST}"
486
+ )
433
487
  while not os.path.exists(READY_FILE):
434
488
  _, deployment_token, record = _load_vm_bootstrap_record()
435
489
  if deployment_token and isinstance(record, dict):
436
490
  status = str(record.get("status") or "").strip().lower()
437
491
  if status != "pending":
492
+ # Somebody else's turn, or already done. Worth saying once per
493
+ # status: a record stuck on "applied" while the relay is down
494
+ # means something very different from no record at all.
495
+ _log_once(f"status-{status}", f"record status is {status!r}, not acting on it")
438
496
  time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
439
497
  continue
440
498
  try:
441
499
  args = _build_configure_args(record)
442
- except ValueError:
500
+ except ValueError as error:
501
+ # The record arrived and was unusable. Before this, that was
502
+ # indistinguishable from never having received one.
503
+ _log_once("record-invalid", f"record cannot be turned into a config: {error}")
443
504
  time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
444
505
  continue
445
- ok, _ = _run_configure_process(args, record)
506
+ _log("record found and pending, configuring the relay now")
507
+ ok, detail = _run_configure_process(args, record)
446
508
  if ok:
509
+ _log("relay configured, setup endpoint is done")
447
510
  return
511
+ _log(f"configure failed, will retry: {detail}")
448
512
  time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
449
513
 
450
514