@le-space/rootfs 0.3.11 → 0.3.13

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.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "Shared rootfs contract parsing, reference profile assets, and build helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -74,7 +74,14 @@ https://${proxy_hostname} {
74
74
  disable_http_challenge
75
75
  }
76
76
  }
77
- reverse_proxy http://127.0.0.1:${WS_BACKEND_PORT}
77
+
78
+ handle_path /bootstrap/* {
79
+ reverse_proxy http://127.0.0.1:80
80
+ }
81
+
82
+ handle {
83
+ reverse_proxy http://127.0.0.1:${WS_BACKEND_PORT}
84
+ }
78
85
  }
79
86
  EOF
80
87
  }
@@ -6,7 +6,9 @@ import subprocess
6
6
  import threading
7
7
  import time
8
8
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
9
- from urllib.parse import urlsplit
9
+ from urllib.error import HTTPError, URLError
10
+ from urllib.parse import urlencode, urlsplit
11
+ from urllib.request import urlopen
10
12
 
11
13
 
12
14
  ENV_FILE = os.environ.get("ENV_FILE", "/etc/default/uc-go-peer")
@@ -16,6 +18,12 @@ DESCRIBE_SCRIPT = "/usr/local/sbin/uc-go-peer-describe.py"
16
18
  BOOTSTRAP_SERVICE = os.environ.get("BOOTSTRAP_SERVICE", "uc-go-peer-bootstrap.service")
17
19
  METADATA_FILE = os.environ.get("METADATA_FILE", "/run/uc-go-peer-setup-metadata.json")
18
20
  METADATA_ERROR_FILE = os.environ.get("METADATA_ERROR_FILE", "/run/uc-go-peer-setup-metadata.error")
21
+ AUTHORIZED_KEYS_FILE = os.environ.get("AUTHORIZED_KEYS_FILE", "/root/.ssh/authorized_keys")
22
+ ALEPH_API_HOST = os.environ.get("ALEPH_API_HOST", "https://api2.aleph.im")
23
+ BOOTSTRAP_CONFIG_KEY = os.environ.get("BOOTSTRAP_CONFIG_KEY", "vm-bootstrap-config")
24
+ BOOTSTRAP_CONFIG_POLL_SECONDS = float(os.environ.get("BOOTSTRAP_CONFIG_POLL_SECONDS", "5"))
25
+
26
+ CONFIGURE_LOCK = threading.Lock()
19
27
 
20
28
 
21
29
  def _cors_headers(handler: BaseHTTPRequestHandler) -> None:
@@ -46,6 +54,142 @@ def _validate_proxy_hostname(value: object) -> str | None:
46
54
  return parsed.hostname
47
55
 
48
56
 
57
+ def _extract_guest_bootstrap_locator() -> tuple[str | None, str | None]:
58
+ try:
59
+ with open(AUTHORIZED_KEYS_FILE, encoding="utf-8") as handle:
60
+ for raw_line in handle:
61
+ line = raw_line.strip()
62
+ if not line or line.startswith("#"):
63
+ continue
64
+ for part in line.split():
65
+ if part.startswith("aleph-bootstrap-config:"):
66
+ _, owner_address, deployment_token = part.split(":", 2)
67
+ owner_address = owner_address.strip().lower()
68
+ deployment_token = deployment_token.strip()
69
+ if owner_address and deployment_token:
70
+ return owner_address, deployment_token
71
+ except (FileNotFoundError, ValueError):
72
+ return None, None
73
+
74
+ return None, None
75
+
76
+
77
+ def _clear_metadata_state() -> None:
78
+ for path in (METADATA_FILE, METADATA_ERROR_FILE):
79
+ try:
80
+ os.remove(path)
81
+ except FileNotFoundError:
82
+ pass
83
+
84
+
85
+ def _start_metadata_generation() -> None:
86
+ _clear_metadata_state()
87
+ threading.Thread(target=_generate_metadata_files, daemon=True).start()
88
+
89
+
90
+ def _build_configure_args(record: dict) -> list[str]:
91
+ runtime = record.get("runtime") if isinstance(record.get("runtime"), dict) else {}
92
+ bootstrap = record.get("bootstrap") if isinstance(record.get("bootstrap"), dict) else {}
93
+ public_ipv4 = str(runtime.get("publicIpv4") or "").strip()
94
+ if not public_ipv4:
95
+ raise ValueError("bootstrap config missing runtime.publicIpv4")
96
+
97
+ mapped_ports = runtime.get("mappedPorts") if isinstance(runtime.get("mappedPorts"), dict) else {}
98
+ relay_tcp = mapped_ports.get("9095") if isinstance(mapped_ports.get("9095"), dict) else {}
99
+ relay_ws = mapped_ports.get("9097") if isinstance(mapped_ports.get("9097"), dict) else {}
100
+ relay_udp = mapped_ports.get("9095") if isinstance(mapped_ports.get("9095"), dict) else {}
101
+
102
+ args = [CONFIGURE_SCRIPT, "--public-ipv4", public_ipv4]
103
+
104
+ public_ipv6 = str(runtime.get("publicIpv6") or "").strip()
105
+ if public_ipv6:
106
+ args.extend(["--public-ipv6", public_ipv6])
107
+
108
+ proxy_hostname = _validate_proxy_hostname(runtime.get("proxyUrl"))
109
+ if proxy_hostname:
110
+ args.extend(["--proxy-hostname", proxy_hostname])
111
+
112
+ relay_tcp_host = relay_tcp.get("host")
113
+ relay_ws_host = relay_ws.get("host")
114
+ relay_udp_host = relay_udp.get("host") if relay_udp.get("udp") is True else None
115
+
116
+ if isinstance(relay_tcp_host, int) and relay_tcp_host > 0:
117
+ args.extend(["--tcp-port", str(relay_tcp_host)])
118
+ if isinstance(relay_ws_host, int) and relay_ws_host > 0:
119
+ args.extend(["--ws-port", str(relay_ws_host)])
120
+ if isinstance(relay_udp_host, int) and relay_udp_host > 0:
121
+ args.extend(["--udp-port", str(relay_udp_host)])
122
+
123
+ registration_id = str(bootstrap.get("registrationId") or "").strip()
124
+ if registration_id:
125
+ args.extend(["--bootstrap-registration-id", registration_id])
126
+ owner_auth = str(bootstrap.get("ownerAuthorizationBase64") or "").strip()
127
+ if owner_auth:
128
+ args.extend(["--bootstrap-owner-authorization-b64", owner_auth])
129
+
130
+ return args
131
+
132
+
133
+ def _run_configure_process(args: list[str]) -> tuple[bool, str]:
134
+ with CONFIGURE_LOCK:
135
+ if os.path.exists(READY_FILE):
136
+ return True, "ready"
137
+ try:
138
+ result = subprocess.run(args, check=True, capture_output=True, text=True)
139
+ except subprocess.CalledProcessError as error:
140
+ return False, error.stderr.strip() or error.stdout.strip() or str(error)
141
+
142
+ _start_metadata_generation()
143
+ return True, result.stdout.strip()
144
+
145
+
146
+ def _load_vm_bootstrap_record() -> tuple[str | None, str | None, dict | None]:
147
+ owner_address, deployment_token = _extract_guest_bootstrap_locator()
148
+ if not owner_address or not deployment_token:
149
+ return owner_address, deployment_token, None
150
+
151
+ request_url = (
152
+ f"{ALEPH_API_HOST.rstrip('/')}/api/v0/aggregates/{owner_address}.json?"
153
+ f"{urlencode({'keys': BOOTSTRAP_CONFIG_KEY})}"
154
+ )
155
+
156
+ try:
157
+ with urlopen(request_url, timeout=10) as response:
158
+ payload = json.loads(response.read().decode("utf-8") or "{}")
159
+ except HTTPError as error:
160
+ if error.code == 404:
161
+ return owner_address, deployment_token, None
162
+ return owner_address, deployment_token, None
163
+ except (URLError, json.JSONDecodeError, TimeoutError):
164
+ return owner_address, deployment_token, None
165
+
166
+ aggregate = payload.get("data", {}).get(BOOTSTRAP_CONFIG_KEY, {})
167
+ if not isinstance(aggregate, dict):
168
+ return owner_address, deployment_token, None
169
+
170
+ record = aggregate.get(deployment_token)
171
+ return owner_address, deployment_token, record if isinstance(record, dict) else None
172
+
173
+
174
+ def _poll_bootstrap_config_loop() -> None:
175
+ while not os.path.exists(READY_FILE):
176
+ _, deployment_token, record = _load_vm_bootstrap_record()
177
+ if deployment_token and isinstance(record, dict):
178
+ status = str(record.get("status") or "").strip().lower()
179
+ if status != "pending":
180
+ time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
181
+ continue
182
+ try:
183
+ args = _build_configure_args(record)
184
+ except ValueError:
185
+ time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
186
+ continue
187
+ ok, _ = _run_configure_process(args)
188
+ if ok:
189
+ return
190
+ time.sleep(BOOTSTRAP_CONFIG_POLL_SECONDS)
191
+
192
+
49
193
  class Handler(BaseHTTPRequestHandler):
50
194
  server_version = "UcGoPeerSetup/1.0"
51
195
 
@@ -184,26 +328,22 @@ class Handler(BaseHTTPRequestHandler):
184
328
  self._send_json(400, {"status": "bad-request", "error": str(error)})
185
329
  return
186
330
 
187
- try:
188
- result = subprocess.run(args, check=True, capture_output=True, text=True)
189
- except subprocess.CalledProcessError as error:
331
+ ok, output = _run_configure_process(args)
332
+ if not ok:
190
333
  self._send_json(
191
334
  500,
192
335
  {
193
336
  "status": "error",
194
- "error": error.stderr.strip() or error.stdout.strip() or str(error),
337
+ "error": output or "configure failed",
195
338
  },
196
339
  )
197
340
  return
198
341
 
199
- _clear_metadata_state()
200
- threading.Thread(target=_generate_metadata_files, daemon=True).start()
201
-
202
342
  self._send_json(
203
343
  200,
204
344
  {
205
345
  "status": "configured",
206
- "stdout": result.stdout.strip(),
346
+ "stdout": output,
207
347
  "metadata_pending": True,
208
348
  },
209
349
  )
@@ -215,14 +355,6 @@ def _stop_bootstrap_service() -> None:
215
355
  subprocess.run(["systemctl", "stop", BOOTSTRAP_SERVICE], check=False)
216
356
 
217
357
 
218
- def _clear_metadata_state() -> None:
219
- for path in (METADATA_FILE, METADATA_ERROR_FILE):
220
- try:
221
- os.remove(path)
222
- except FileNotFoundError:
223
- pass
224
-
225
-
226
358
  def _generate_metadata_files() -> None:
227
359
  try:
228
360
  describe = subprocess.run(
@@ -240,6 +372,7 @@ def _generate_metadata_files() -> None:
240
372
 
241
373
 
242
374
  def main() -> None:
375
+ threading.Thread(target=_poll_bootstrap_config_loop, daemon=True).start()
243
376
  server = ThreadingHTTPServer(("0.0.0.0", 80), Handler)
244
377
  server.serve_forever()
245
378