portacode 1.4.11.dev5__py3-none-any.whl → 1.4.11.dev7__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.
- portacode/_version.py +2 -2
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +58 -0
- portacode/connection/handlers/__init__.py +2 -0
- portacode/connection/handlers/proxmox_infra.py +439 -94
- portacode/connection/terminal.py +3 -0
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.11.dev7.dist-info}/METADATA +1 -1
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.11.dev7.dist-info}/RECORD +11 -11
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.11.dev7.dist-info}/WHEEL +0 -0
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.11.dev7.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.11.dev7.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.11.dev5.dist-info → portacode-1.4.11.dev7.dist-info}/top_level.txt +0 -0
portacode/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.4.11.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 11, '
|
|
31
|
+
__version__ = version = '1.4.11.dev7'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 11, 'dev7')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -380,6 +380,48 @@ Emitted after a successful `create_proxmox_container` action. Contains the new c
|
|
|
380
380
|
* `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
|
|
381
381
|
* `setup_steps` (array[object]): Detailed bootstrap step results (name, stdout/stderr, elapsed time, and status).
|
|
382
382
|
|
|
383
|
+
### `proxmox_container_progress`
|
|
384
|
+
|
|
385
|
+
Sent intermittently while `create_proxmox_container` is executing so callers can display a progress indicator. Each notification describes the currently running step (validation, provisioning, or each bootstrap command) and whether it succeeded or failed.
|
|
386
|
+
|
|
387
|
+
**Event Fields:**
|
|
388
|
+
|
|
389
|
+
* `step_index` (integer): 1-based index of the current step inside the entire provisioning sequence.
|
|
390
|
+
* `total_steps` (integer): Total number of steps that must run before provisioning completes.
|
|
391
|
+
* `step_name` (string): Internal step identifier (e.g., `create_container`, `apt_update`, `portacode_connect`).
|
|
392
|
+
* `step_label` (string): Human-friendly label suitable for UI (e.g., `Create container`, `Apt update`).
|
|
393
|
+
* `status` (string): One of `in_progress`, `completed`, or `failed`.
|
|
394
|
+
* `phase` (string): Either `lifecycle` (environment/container lifecycle) or `bootstrap` (per-command bootstrap work).
|
|
395
|
+
* `message` (string): Short description of what is happening or why a failure occurred.
|
|
396
|
+
* `details` (object, optional): Contains `attempt` (if retries were needed) and `error_summary` when a step fails.
|
|
397
|
+
* `request_id` (string, optional): Mirrors the request ID from the incoming `create_proxmox_container` payload when available.
|
|
398
|
+
|
|
399
|
+
### `start_portacode_service`
|
|
400
|
+
|
|
401
|
+
Runs `sudo portacode service install` inside the container after the dashboard has created the corresponding Device record with the supplied public key.
|
|
402
|
+
|
|
403
|
+
**Payload Fields:**
|
|
404
|
+
|
|
405
|
+
* `ctid` (string, required): Container ID target.
|
|
406
|
+
* `step_index` (integer, required): Next step index to render inside `proxmox_container_progress`.
|
|
407
|
+
* `total_steps` (integer, required): The overall total number of steps (including lifecycle, bootstrap, and service installation).
|
|
408
|
+
|
|
409
|
+
**Responses:**
|
|
410
|
+
|
|
411
|
+
* Emits additional [`proxmox_container_progress`](#proxmox_container_progress-event) events to report the authentication and service-install steps.
|
|
412
|
+
* On success, emits a [`proxmox_service_started`](#proxmox_service_started-event).
|
|
413
|
+
* On failure, emits a generic [`error`](#error) event.
|
|
414
|
+
|
|
415
|
+
### `proxmox_service_started`
|
|
416
|
+
|
|
417
|
+
Indicates that `portacode service install` finished successfully inside a managed container.
|
|
418
|
+
|
|
419
|
+
**Event Fields:**
|
|
420
|
+
|
|
421
|
+
* `success` (boolean): True when the install succeeded.
|
|
422
|
+
* `message` (string): Success summary (e.g., `Portacode service install completed`).
|
|
423
|
+
* `ctid` (string): Container ID.
|
|
424
|
+
|
|
383
425
|
### `clock_sync_request`
|
|
384
426
|
|
|
385
427
|
Internal event that devices send to the gateway to request the authoritative server timestamp (used for adjusting `portacode.utils.ntp_clock`). The gateway responds immediately with [`clock_sync_response`](#clock_sync_response).
|
|
@@ -1075,6 +1117,22 @@ Emitted after a successful `create_proxmox_container` action to report the newly
|
|
|
1075
1117
|
* `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
|
|
1076
1118
|
* `setup_steps` (array[object]): Detailed bootstrap step reports including stdout/stderr, elapsed time, and pass/fail status.
|
|
1077
1119
|
|
|
1120
|
+
### `proxmox_container_progress`
|
|
1121
|
+
|
|
1122
|
+
Sent continuously while `create_proxmox_container` runs so dashboards can show a progress bar tied to each lifecycle and bootstrap step.
|
|
1123
|
+
|
|
1124
|
+
**Event Fields:**
|
|
1125
|
+
|
|
1126
|
+
* `step_index` (integer): 1-based position of the step inside the entire provisioning workflow.
|
|
1127
|
+
* `total_steps` (integer): Total number of lifecycle and bootstrap steps for the current operation.
|
|
1128
|
+
* `step_name` (string): Internal identifier (e.g., `validate_environment`, `install_deps`, `portacode_connect`).
|
|
1129
|
+
* `step_label` (string): Friendly label suitable for the UI.
|
|
1130
|
+
* `status` (string): One of `in_progress`, `completed`, or `failed`.
|
|
1131
|
+
* `phase` (string): Either `lifecycle` (node validation/container lifecycle) or `bootstrap` (commands run inside the CT).
|
|
1132
|
+
* `message` (string): Short human-readable description of the action or failure.
|
|
1133
|
+
* `details` (object, optional): Contains `attempt` (when retries are used) and `error_summary` on failure.
|
|
1134
|
+
* `request_id` (string, optional): Mirrors the `create_proxmox_container` request when provided.
|
|
1135
|
+
|
|
1078
1136
|
### <a name="clock_sync_response"></a>`clock_sync_response`
|
|
1079
1137
|
|
|
1080
1138
|
Reply sent by the gateway immediately after receiving a `clock_sync_request`. Devices use this event plus the measured round-trip time to keep their local `ntp_clock` offset accurate.
|
|
@@ -45,6 +45,7 @@ from .proxmox_infra import (
|
|
|
45
45
|
ConfigureProxmoxInfraHandler,
|
|
46
46
|
CreateProxmoxContainerHandler,
|
|
47
47
|
RevertProxmoxInfraHandler,
|
|
48
|
+
StartPortacodeServiceHandler,
|
|
48
49
|
)
|
|
49
50
|
|
|
50
51
|
__all__ = [
|
|
@@ -84,6 +85,7 @@ __all__ = [
|
|
|
84
85
|
"ProjectStateGitUnstageHandler",
|
|
85
86
|
"ProjectStateGitRevertHandler",
|
|
86
87
|
"ProjectStateGitCommitHandler",
|
|
88
|
+
"StartPortacodeServiceHandler",
|
|
87
89
|
"UpdatePortacodeHandler",
|
|
88
90
|
"RevertProxmoxInfraHandler",
|
|
89
91
|
]
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import json
|
|
6
7
|
import logging
|
|
7
8
|
import os
|
|
9
|
+
import secrets
|
|
8
10
|
import shutil
|
|
9
11
|
import stat
|
|
10
12
|
import subprocess
|
|
@@ -12,7 +14,7 @@ import sys
|
|
|
12
14
|
import time
|
|
13
15
|
from datetime import datetime
|
|
14
16
|
from pathlib import Path
|
|
15
|
-
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
17
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
|
|
16
18
|
|
|
17
19
|
import platformdirs
|
|
18
20
|
|
|
@@ -39,6 +41,55 @@ IFACES_PATH = Path("/etc/network/interfaces")
|
|
|
39
41
|
SYSCTL_PATH = Path("/etc/sysctl.d/99-portacode-forward.conf")
|
|
40
42
|
UNIT_DIR = Path("/etc/systemd/system")
|
|
41
43
|
|
|
44
|
+
ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _emit_progress_event(
|
|
48
|
+
handler: SyncHandler,
|
|
49
|
+
*,
|
|
50
|
+
step_index: int,
|
|
51
|
+
total_steps: int,
|
|
52
|
+
step_name: str,
|
|
53
|
+
step_label: str,
|
|
54
|
+
status: str,
|
|
55
|
+
message: str,
|
|
56
|
+
phase: str,
|
|
57
|
+
request_id: Optional[str],
|
|
58
|
+
details: Optional[Dict[str, Any]] = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
loop = handler.context.get("event_loop")
|
|
61
|
+
if not loop or loop.is_closed():
|
|
62
|
+
logger.debug(
|
|
63
|
+
"progress event skipped (no event loop) step=%s status=%s",
|
|
64
|
+
step_name,
|
|
65
|
+
status,
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
payload: Dict[str, Any] = {
|
|
70
|
+
"event": "proxmox_container_progress",
|
|
71
|
+
"step_name": step_name,
|
|
72
|
+
"step_label": step_label,
|
|
73
|
+
"status": status,
|
|
74
|
+
"phase": phase,
|
|
75
|
+
"step_index": step_index,
|
|
76
|
+
"total_steps": total_steps,
|
|
77
|
+
"message": message,
|
|
78
|
+
}
|
|
79
|
+
if request_id:
|
|
80
|
+
payload["request_id"] = request_id
|
|
81
|
+
if details:
|
|
82
|
+
payload["details"] = details
|
|
83
|
+
|
|
84
|
+
future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
|
|
85
|
+
future.add_done_callback(
|
|
86
|
+
lambda fut: logger.warning(
|
|
87
|
+
"Failed to emit progress event for %s: %s", step_name, fut.exception()
|
|
88
|
+
)
|
|
89
|
+
if fut.exception()
|
|
90
|
+
else None
|
|
91
|
+
)
|
|
92
|
+
|
|
42
93
|
|
|
43
94
|
def _call_subprocess(cmd: List[str], **kwargs: Any) -> subprocess.CompletedProcess[str]:
|
|
44
95
|
env = os.environ.copy()
|
|
@@ -245,6 +296,68 @@ def _format_rootfs(storage: str, disk_gib: int, storage_type: str) -> str:
|
|
|
245
296
|
return f"{storage}:{disk_gib}G"
|
|
246
297
|
|
|
247
298
|
|
|
299
|
+
def _get_provisioning_user_info(message: Dict[str, Any]) -> Tuple[str, str, str]:
|
|
300
|
+
user = (message.get("username") or "svcuser").strip() if message else "svcuser"
|
|
301
|
+
user = user or "svcuser"
|
|
302
|
+
password = message.get("password")
|
|
303
|
+
if not password:
|
|
304
|
+
password = secrets.token_urlsafe(10)
|
|
305
|
+
ssh_key = (message.get("ssh_key") or "").strip() if message else ""
|
|
306
|
+
return user, password, ssh_key
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _friendly_step_label(step_name: str) -> str:
|
|
310
|
+
if not step_name:
|
|
311
|
+
return "Step"
|
|
312
|
+
normalized = step_name.replace("_", " ").strip()
|
|
313
|
+
return normalized.capitalize()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _build_bootstrap_steps(user: str, password: str, ssh_key: str) -> List[Dict[str, Any]]:
|
|
317
|
+
steps = [
|
|
318
|
+
{
|
|
319
|
+
"name": "apt_update",
|
|
320
|
+
"cmd": "apt-get update -y",
|
|
321
|
+
"retries": 4,
|
|
322
|
+
"retry_delay_s": 5,
|
|
323
|
+
"retry_on": [
|
|
324
|
+
"Temporary failure resolving",
|
|
325
|
+
"Could not resolve",
|
|
326
|
+
"Failed to fetch",
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
"name": "install_deps",
|
|
331
|
+
"cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
|
|
332
|
+
"retries": 5,
|
|
333
|
+
"retry_delay_s": 5,
|
|
334
|
+
"retry_on": [
|
|
335
|
+
"lock-frontend",
|
|
336
|
+
"Unable to acquire the dpkg frontend lock",
|
|
337
|
+
"Temporary failure resolving",
|
|
338
|
+
"Could not resolve",
|
|
339
|
+
"Failed to fetch",
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
{"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
|
|
343
|
+
{"name": "add_sudo", "cmd": f"usermod -aG sudo {user}", "retries": 0},
|
|
344
|
+
]
|
|
345
|
+
if password:
|
|
346
|
+
steps.append({"name": "set_password", "cmd": f"echo '{user}:{password}' | chpasswd", "retries": 0})
|
|
347
|
+
if ssh_key:
|
|
348
|
+
steps.append({
|
|
349
|
+
"name": "add_ssh_key",
|
|
350
|
+
"cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
|
|
351
|
+
"retries": 0,
|
|
352
|
+
})
|
|
353
|
+
steps.extend([
|
|
354
|
+
{"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
|
|
355
|
+
{"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
|
|
356
|
+
{"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30},
|
|
357
|
+
])
|
|
358
|
+
return steps
|
|
359
|
+
|
|
360
|
+
|
|
248
361
|
def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) -> str:
|
|
249
362
|
for entry in storages:
|
|
250
363
|
if entry.get("storage") == storage_name:
|
|
@@ -317,6 +430,13 @@ def _write_container_record(vmid: int, payload: Dict[str, Any]) -> None:
|
|
|
317
430
|
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
|
318
431
|
|
|
319
432
|
|
|
433
|
+
def _read_container_record(vmid: int) -> Dict[str, Any]:
|
|
434
|
+
path = CONTAINERS_DIR / f"ct-{vmid}.json"
|
|
435
|
+
if not path.exists():
|
|
436
|
+
raise FileNotFoundError(f"Container record {path} missing")
|
|
437
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
438
|
+
|
|
439
|
+
|
|
320
440
|
def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) -> Dict[str, Any]:
|
|
321
441
|
templates = config.get("templates") or []
|
|
322
442
|
default_template = templates[0] if templates else ""
|
|
@@ -333,9 +453,7 @@ def _build_container_payload(message: Dict[str, Any], config: Dict[str, Any]) ->
|
|
|
333
453
|
if not storage:
|
|
334
454
|
raise ValueError("Storage pool could not be determined.")
|
|
335
455
|
|
|
336
|
-
user = (message
|
|
337
|
-
password = message.get("password") or ""
|
|
338
|
-
ssh_key = (message.get("ssh_key") or "").strip()
|
|
456
|
+
user, password, ssh_key = _get_provisioning_user_info(message)
|
|
339
457
|
|
|
340
458
|
payload = {
|
|
341
459
|
"template": template,
|
|
@@ -367,10 +485,10 @@ def _connect_proxmox(config: Dict[str, Any]) -> Any:
|
|
|
367
485
|
)
|
|
368
486
|
|
|
369
487
|
|
|
370
|
-
def _run_pct(vmid: int, cmd: str) -> Dict[str, Any]:
|
|
488
|
+
def _run_pct(vmid: int, cmd: str, input_text: Optional[str] = None) -> Dict[str, Any]:
|
|
371
489
|
full = ["pct", "exec", str(vmid), "--", "bash", "-lc", cmd]
|
|
372
490
|
start = time.time()
|
|
373
|
-
proc = subprocess.run(full, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
491
|
+
proc = subprocess.run(full, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=input_text)
|
|
374
492
|
return {
|
|
375
493
|
"cmd": cmd,
|
|
376
494
|
"returncode": proc.returncode,
|
|
@@ -471,18 +589,36 @@ def _summarize_error(res: Dict[str, Any]) -> str:
|
|
|
471
589
|
return "Command failed; see stdout/stderr for details."
|
|
472
590
|
|
|
473
591
|
|
|
474
|
-
def _run_setup_steps(
|
|
592
|
+
def _run_setup_steps(
|
|
593
|
+
vmid: int,
|
|
594
|
+
steps: List[Dict[str, Any]],
|
|
595
|
+
user: str,
|
|
596
|
+
progress_callback: Optional[ProgressCallback] = None,
|
|
597
|
+
start_index: int = 1,
|
|
598
|
+
total_steps: Optional[int] = None,
|
|
599
|
+
) -> Tuple[List[Dict[str, Any]], bool]:
|
|
475
600
|
results: List[Dict[str, Any]] = []
|
|
476
|
-
|
|
601
|
+
computed_total = total_steps if total_steps is not None else start_index + len(steps) - 1
|
|
602
|
+
for offset, step in enumerate(steps):
|
|
603
|
+
step_index = start_index + offset
|
|
604
|
+
if progress_callback:
|
|
605
|
+
progress_callback(step_index, computed_total, step, "in_progress", None)
|
|
606
|
+
|
|
477
607
|
if step.get("type") == "portacode_connect":
|
|
478
608
|
res = _portacode_connect_and_read_key(vmid, user, timeout_s=step.get("timeout_s", 10))
|
|
479
609
|
res["name"] = step["name"]
|
|
480
610
|
results.append(res)
|
|
481
611
|
if not res.get("ok"):
|
|
612
|
+
if progress_callback:
|
|
613
|
+
progress_callback(step_index, computed_total, step, "failed", res)
|
|
482
614
|
return results, False
|
|
615
|
+
if progress_callback:
|
|
616
|
+
progress_callback(step_index, computed_total, step, "completed", res)
|
|
483
617
|
continue
|
|
484
618
|
|
|
485
619
|
attempts = 0
|
|
620
|
+
retry_on = step.get("retry_on", [])
|
|
621
|
+
max_attempts = step.get("retries", 0) + 1
|
|
486
622
|
while True:
|
|
487
623
|
attempts += 1
|
|
488
624
|
res = _run_pct(vmid, step["cmd"])
|
|
@@ -492,61 +628,47 @@ def _run_setup_steps(vmid: int, steps: List[Dict[str, Any]], user: str) -> Tuple
|
|
|
492
628
|
res["error_summary"] = _summarize_error(res)
|
|
493
629
|
results.append(res)
|
|
494
630
|
if res["returncode"] == 0:
|
|
631
|
+
if progress_callback:
|
|
632
|
+
progress_callback(step_index, computed_total, step, "completed", res)
|
|
495
633
|
break
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
634
|
+
|
|
635
|
+
will_retry = False
|
|
636
|
+
if attempts < max_attempts and retry_on:
|
|
637
|
+
stderr_stdout = (res.get("stderr", "") + res.get("stdout", ""))
|
|
638
|
+
if any(tok in stderr_stdout for tok in retry_on):
|
|
639
|
+
will_retry = True
|
|
640
|
+
|
|
641
|
+
if progress_callback:
|
|
642
|
+
status = "retrying" if will_retry else "failed"
|
|
643
|
+
progress_callback(step_index, computed_total, step, status, res)
|
|
644
|
+
|
|
645
|
+
if will_retry:
|
|
500
646
|
time.sleep(step.get("retry_delay_s", 3))
|
|
501
647
|
continue
|
|
648
|
+
|
|
502
649
|
return results, False
|
|
503
650
|
return results, True
|
|
504
651
|
|
|
505
652
|
|
|
506
|
-
def _bootstrap_portacode(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
"lock-frontend",
|
|
526
|
-
"Unable to acquire the dpkg frontend lock",
|
|
527
|
-
"Temporary failure resolving",
|
|
528
|
-
"Could not resolve",
|
|
529
|
-
"Failed to fetch",
|
|
530
|
-
],
|
|
531
|
-
},
|
|
532
|
-
{"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
|
|
533
|
-
{"name": "add_sudo", "cmd": f"usermod -aG sudo {user}", "retries": 0},
|
|
534
|
-
]
|
|
535
|
-
if password:
|
|
536
|
-
steps.append({"name": "set_password", "cmd": f"echo '{user}:{password}' | chpasswd", "retries": 0})
|
|
537
|
-
if ssh_key:
|
|
538
|
-
steps.append({
|
|
539
|
-
"name": "add_ssh_key",
|
|
540
|
-
"cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
|
|
541
|
-
"retries": 0,
|
|
542
|
-
})
|
|
543
|
-
steps.extend([
|
|
544
|
-
{"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
|
|
545
|
-
{"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
|
|
546
|
-
{"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30},
|
|
547
|
-
])
|
|
548
|
-
|
|
549
|
-
results, ok = _run_setup_steps(vmid, steps, user)
|
|
653
|
+
def _bootstrap_portacode(
|
|
654
|
+
vmid: int,
|
|
655
|
+
user: str,
|
|
656
|
+
password: str,
|
|
657
|
+
ssh_key: str,
|
|
658
|
+
steps: Optional[List[Dict[str, Any]]] = None,
|
|
659
|
+
progress_callback: Optional[ProgressCallback] = None,
|
|
660
|
+
start_index: int = 1,
|
|
661
|
+
total_steps: Optional[int] = None,
|
|
662
|
+
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
663
|
+
actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
|
|
664
|
+
results, ok = _run_setup_steps(
|
|
665
|
+
vmid,
|
|
666
|
+
actual_steps,
|
|
667
|
+
user,
|
|
668
|
+
progress_callback=progress_callback,
|
|
669
|
+
start_index=start_index,
|
|
670
|
+
total_steps=total_steps,
|
|
671
|
+
)
|
|
550
672
|
if not ok:
|
|
551
673
|
raise RuntimeError("Portacode bootstrap steps failed.")
|
|
552
674
|
key_step = next((entry for entry in results if entry.get("name") == "portacode_connect"), None)
|
|
@@ -692,49 +814,166 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
692
814
|
|
|
693
815
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
694
816
|
logger.info("create_proxmox_container command received")
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
817
|
+
request_id = message.get("request_id")
|
|
818
|
+
bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
|
|
819
|
+
bootstrap_steps = _build_bootstrap_steps(bootstrap_user, bootstrap_password, bootstrap_ssh_key)
|
|
820
|
+
total_steps = 3 + len(bootstrap_steps) + 2
|
|
821
|
+
current_step_index = 1
|
|
822
|
+
|
|
823
|
+
def _run_lifecycle_step(
|
|
824
|
+
step_name: str,
|
|
825
|
+
step_label: str,
|
|
826
|
+
start_message: str,
|
|
827
|
+
success_message: str,
|
|
828
|
+
action,
|
|
829
|
+
):
|
|
830
|
+
nonlocal current_step_index
|
|
831
|
+
step_index = current_step_index
|
|
832
|
+
_emit_progress_event(
|
|
833
|
+
step_index=step_index,
|
|
834
|
+
total_steps=total_steps,
|
|
835
|
+
step_name=step_name,
|
|
836
|
+
step_label=step_label,
|
|
837
|
+
status="in_progress",
|
|
838
|
+
message=start_message,
|
|
839
|
+
phase="lifecycle",
|
|
840
|
+
request_id=request_id,
|
|
841
|
+
)
|
|
842
|
+
try:
|
|
843
|
+
result = action()
|
|
844
|
+
except Exception as exc:
|
|
845
|
+
_emit_progress_event(
|
|
846
|
+
step_index=step_index,
|
|
847
|
+
total_steps=total_steps,
|
|
848
|
+
step_name=step_name,
|
|
849
|
+
step_label=step_label,
|
|
850
|
+
status="failed",
|
|
851
|
+
message=f"{step_label} failed: {exc}",
|
|
852
|
+
phase="lifecycle",
|
|
853
|
+
request_id=request_id,
|
|
854
|
+
details={"error": str(exc)},
|
|
855
|
+
)
|
|
856
|
+
raise
|
|
857
|
+
_emit_progress_event(
|
|
858
|
+
step_index=step_index,
|
|
859
|
+
total_steps=total_steps,
|
|
860
|
+
step_name=step_name,
|
|
861
|
+
step_label=step_label,
|
|
862
|
+
status="completed",
|
|
863
|
+
message=success_message,
|
|
864
|
+
phase="lifecycle",
|
|
865
|
+
request_id=request_id,
|
|
866
|
+
)
|
|
867
|
+
current_step_index += 1
|
|
868
|
+
return result
|
|
869
|
+
|
|
870
|
+
def _validate_environment():
|
|
871
|
+
if os.geteuid() != 0:
|
|
872
|
+
raise PermissionError("Container creation requires root privileges.")
|
|
873
|
+
config = _load_config()
|
|
874
|
+
if not config or not config.get("token_value"):
|
|
875
|
+
raise ValueError("Proxmox infrastructure is not configured.")
|
|
876
|
+
if not config.get("network", {}).get("applied"):
|
|
877
|
+
raise RuntimeError("Proxmox bridge setup must be applied before creating containers.")
|
|
878
|
+
return config
|
|
879
|
+
|
|
880
|
+
config = _run_lifecycle_step(
|
|
881
|
+
"validate_environment",
|
|
882
|
+
"Validating infrastructure",
|
|
883
|
+
"Checking token, permissions, and bridge setup…",
|
|
884
|
+
"Infrastructure validated.",
|
|
885
|
+
_validate_environment,
|
|
720
886
|
)
|
|
721
887
|
|
|
722
|
-
|
|
888
|
+
def _create_container():
|
|
889
|
+
proxmox = _connect_proxmox(config)
|
|
890
|
+
node = config.get("node") or DEFAULT_NODE_NAME
|
|
891
|
+
payload = _build_container_payload(message, config)
|
|
892
|
+
payload["cpuunits"] = max(int(payload["cpus"] * 1024), 10)
|
|
893
|
+
payload["memory"] = int(payload["ram_mib"])
|
|
894
|
+
payload["node"] = node
|
|
895
|
+
logger.debug(
|
|
896
|
+
"Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
|
|
897
|
+
node,
|
|
898
|
+
payload["template"],
|
|
899
|
+
payload["ram_mib"],
|
|
900
|
+
payload["cpus"],
|
|
901
|
+
payload["storage"],
|
|
902
|
+
)
|
|
723
903
|
vmid, _ = _instantiate_container(proxmox, node, payload)
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
payload
|
|
730
|
-
|
|
904
|
+
payload["vmid"] = vmid
|
|
905
|
+
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
906
|
+
_write_container_record(vmid, payload)
|
|
907
|
+
return proxmox, node, vmid, payload
|
|
908
|
+
|
|
909
|
+
proxmox, node, vmid, payload = _run_lifecycle_step(
|
|
910
|
+
"create_container",
|
|
911
|
+
"Creating container",
|
|
912
|
+
"Provisioning the LXC container…",
|
|
913
|
+
"Container created.",
|
|
914
|
+
_create_container,
|
|
915
|
+
)
|
|
731
916
|
|
|
732
|
-
|
|
917
|
+
def _start_container_step():
|
|
733
918
|
_start_container(proxmox, node, vmid)
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
919
|
+
|
|
920
|
+
_run_lifecycle_step(
|
|
921
|
+
"start_container",
|
|
922
|
+
"Starting container",
|
|
923
|
+
"Booting the container…",
|
|
924
|
+
"Container startup completed.",
|
|
925
|
+
_start_container_step,
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
def _bootstrap_progress_callback(
|
|
929
|
+
step_index: int,
|
|
930
|
+
total: int,
|
|
931
|
+
step: Dict[str, Any],
|
|
932
|
+
status: str,
|
|
933
|
+
result: Optional[Dict[str, Any]],
|
|
934
|
+
):
|
|
935
|
+
label = step.get("display_name") or _friendly_step_label(step.get("name", "bootstrap"))
|
|
936
|
+
error_summary = (result or {}).get("error_summary") or (result or {}).get("error")
|
|
937
|
+
attempt = (result or {}).get("attempt")
|
|
938
|
+
if status == "in_progress":
|
|
939
|
+
message_text = f"{label} is running…"
|
|
940
|
+
elif status == "completed":
|
|
941
|
+
message_text = f"{label} completed."
|
|
942
|
+
elif status == "retrying":
|
|
943
|
+
attempt_desc = f" (attempt {attempt})" if attempt else ""
|
|
944
|
+
message_text = f"{label} failed{attempt_desc}; retrying…"
|
|
945
|
+
else:
|
|
946
|
+
message_text = f"{label} failed"
|
|
947
|
+
if error_summary:
|
|
948
|
+
message_text += f": {error_summary}"
|
|
949
|
+
details: Dict[str, Any] = {}
|
|
950
|
+
if attempt:
|
|
951
|
+
details["attempt"] = attempt
|
|
952
|
+
if error_summary:
|
|
953
|
+
details["error_summary"] = error_summary
|
|
954
|
+
_emit_progress_event(
|
|
955
|
+
step_index=step_index,
|
|
956
|
+
total_steps=total,
|
|
957
|
+
step_name=step.get("name", "bootstrap"),
|
|
958
|
+
step_label=label,
|
|
959
|
+
status=status,
|
|
960
|
+
message=message_text,
|
|
961
|
+
phase="bootstrap",
|
|
962
|
+
request_id=request_id,
|
|
963
|
+
details=details or None,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
public_key, steps = _bootstrap_portacode(
|
|
967
|
+
vmid,
|
|
968
|
+
payload["username"],
|
|
969
|
+
payload["password"],
|
|
970
|
+
payload["ssh_public_key"],
|
|
971
|
+
steps=bootstrap_steps,
|
|
972
|
+
progress_callback=_bootstrap_progress_callback,
|
|
973
|
+
start_index=current_step_index,
|
|
974
|
+
total_steps=total_steps,
|
|
975
|
+
)
|
|
976
|
+
current_step_index += len(bootstrap_steps)
|
|
738
977
|
|
|
739
978
|
return {
|
|
740
979
|
"event": "proxmox_container_created",
|
|
@@ -755,6 +994,112 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
755
994
|
}
|
|
756
995
|
|
|
757
996
|
|
|
997
|
+
class StartPortacodeServiceHandler(SyncHandler):
|
|
998
|
+
"""Start the Portacode service inside a newly created container."""
|
|
999
|
+
|
|
1000
|
+
@property
|
|
1001
|
+
def command_name(self) -> str:
|
|
1002
|
+
return "start_portacode_service"
|
|
1003
|
+
|
|
1004
|
+
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1005
|
+
ctid = message.get("ctid")
|
|
1006
|
+
if not ctid:
|
|
1007
|
+
raise ValueError("ctid is required")
|
|
1008
|
+
try:
|
|
1009
|
+
vmid = int(ctid)
|
|
1010
|
+
except ValueError:
|
|
1011
|
+
raise ValueError("ctid must be an integer")
|
|
1012
|
+
|
|
1013
|
+
record = _read_container_record(vmid)
|
|
1014
|
+
user = record.get("username")
|
|
1015
|
+
password = record.get("password")
|
|
1016
|
+
if not user or not password:
|
|
1017
|
+
raise RuntimeError("Container credentials unavailable")
|
|
1018
|
+
|
|
1019
|
+
start_index = int(message.get("step_index", 1))
|
|
1020
|
+
total_steps = int(message.get("total_steps", start_index + 2))
|
|
1021
|
+
request_id = message.get("request_id")
|
|
1022
|
+
|
|
1023
|
+
auth_step_name = "setup_device_authentication"
|
|
1024
|
+
auth_label = "Setting up device authentication"
|
|
1025
|
+
_emit_progress_event(
|
|
1026
|
+
self,
|
|
1027
|
+
step_index=start_index,
|
|
1028
|
+
total_steps=total_steps,
|
|
1029
|
+
step_name=auth_step_name,
|
|
1030
|
+
step_label=auth_label,
|
|
1031
|
+
status="in_progress",
|
|
1032
|
+
message="Notifying the server of the new device…",
|
|
1033
|
+
phase="service",
|
|
1034
|
+
request_id=request_id,
|
|
1035
|
+
)
|
|
1036
|
+
_emit_progress_event(
|
|
1037
|
+
self,
|
|
1038
|
+
step_index=start_index,
|
|
1039
|
+
total_steps=total_steps,
|
|
1040
|
+
step_name=auth_step_name,
|
|
1041
|
+
step_label=auth_label,
|
|
1042
|
+
status="completed",
|
|
1043
|
+
message="Authentication metadata recorded.",
|
|
1044
|
+
phase="service",
|
|
1045
|
+
request_id=request_id,
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
install_step = start_index + 1
|
|
1049
|
+
install_label = "Launching Portacode service"
|
|
1050
|
+
_emit_progress_event(
|
|
1051
|
+
self,
|
|
1052
|
+
step_index=install_step,
|
|
1053
|
+
total_steps=total_steps,
|
|
1054
|
+
step_name="launch_portacode_service",
|
|
1055
|
+
step_label=install_label,
|
|
1056
|
+
status="in_progress",
|
|
1057
|
+
message="Running sudo portacode service install…",
|
|
1058
|
+
phase="service",
|
|
1059
|
+
request_id=request_id,
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
cmd = f"su - {user} -c 'sudo -S portacode service install'"
|
|
1063
|
+
res = _run_pct(vmid, cmd, input_text=password + "\n")
|
|
1064
|
+
|
|
1065
|
+
if res["returncode"] != 0:
|
|
1066
|
+
_emit_progress_event(
|
|
1067
|
+
self,
|
|
1068
|
+
step_index=install_step,
|
|
1069
|
+
total_steps=total_steps,
|
|
1070
|
+
step_name="launch_portacode_service",
|
|
1071
|
+
step_label=install_label,
|
|
1072
|
+
status="failed",
|
|
1073
|
+
message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
|
|
1074
|
+
phase="service",
|
|
1075
|
+
request_id=request_id,
|
|
1076
|
+
details={
|
|
1077
|
+
"stderr": res.get("stderr"),
|
|
1078
|
+
"stdout": res.get("stdout"),
|
|
1079
|
+
},
|
|
1080
|
+
)
|
|
1081
|
+
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
1082
|
+
|
|
1083
|
+
_emit_progress_event(
|
|
1084
|
+
self,
|
|
1085
|
+
step_index=install_step,
|
|
1086
|
+
total_steps=total_steps,
|
|
1087
|
+
step_name="launch_portacode_service",
|
|
1088
|
+
step_label=install_label,
|
|
1089
|
+
status="completed",
|
|
1090
|
+
message="Portacode service install finished.",
|
|
1091
|
+
phase="service",
|
|
1092
|
+
request_id=request_id,
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
return {
|
|
1096
|
+
"event": "proxmox_service_started",
|
|
1097
|
+
"success": True,
|
|
1098
|
+
"message": "Portacode service install completed",
|
|
1099
|
+
"ctid": str(vmid),
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
|
|
758
1103
|
class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
759
1104
|
@property
|
|
760
1105
|
def command_name(self) -> str:
|
portacode/connection/terminal.py
CHANGED
|
@@ -55,6 +55,7 @@ from .handlers import (
|
|
|
55
55
|
ConfigureProxmoxInfraHandler,
|
|
56
56
|
CreateProxmoxContainerHandler,
|
|
57
57
|
RevertProxmoxInfraHandler,
|
|
58
|
+
StartPortacodeServiceHandler,
|
|
58
59
|
)
|
|
59
60
|
from .handlers.project_aware_file_handlers import (
|
|
60
61
|
ProjectAwareFileWriteHandler,
|
|
@@ -414,6 +415,7 @@ class TerminalManager:
|
|
|
414
415
|
"mux": mux,
|
|
415
416
|
"use_content_caching": True, # Enable content caching optimization
|
|
416
417
|
"debug": self.debug,
|
|
418
|
+
"event_loop": asyncio.get_running_loop(),
|
|
417
419
|
}
|
|
418
420
|
|
|
419
421
|
# Initialize command registry
|
|
@@ -477,6 +479,7 @@ class TerminalManager:
|
|
|
477
479
|
# System management handlers
|
|
478
480
|
self._command_registry.register(ConfigureProxmoxInfraHandler)
|
|
479
481
|
self._command_registry.register(CreateProxmoxContainerHandler)
|
|
482
|
+
self._command_registry.register(StartPortacodeServiceHandler)
|
|
480
483
|
self._command_registry.register(RevertProxmoxInfraHandler)
|
|
481
484
|
self._command_registry.register(UpdatePortacodeHandler)
|
|
482
485
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
|
|
2
2
|
portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
|
|
3
3
|
portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
|
|
4
|
-
portacode/_version.py,sha256=
|
|
4
|
+
portacode/_version.py,sha256=rMZzV0VJav1wTlnbu-swqozHj1iDFuOjk8XIEpAzY8o,719
|
|
5
5
|
portacode/cli.py,sha256=mGLKoZ-T2FBF7IA9wUq0zyG0X9__-A1ao7gajjcVRH8,21828
|
|
6
6
|
portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
|
|
7
7
|
portacode/keypair.py,sha256=0OO4vHDcF1XMxCDqce61xFTlFwlTcmqe5HyGsXFEt7s,5838
|
|
@@ -12,17 +12,17 @@ portacode/connection/README.md,sha256=f9rbuIEKa7cTm9C98rCiBbEtbiIXQU11esGSNhSMiJ
|
|
|
12
12
|
portacode/connection/__init__.py,sha256=atqcVGkViIEd7pRa6cP2do07RJOM0UWpbnz5zXjGktU,250
|
|
13
13
|
portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5weiU,14181
|
|
14
14
|
portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
|
|
15
|
-
portacode/connection/terminal.py,sha256=
|
|
15
|
+
portacode/connection/terminal.py,sha256=07wxG_55JMy3yQ9TXCBldW9h43qCW3U8rv2yzGMx4FM,44757
|
|
16
16
|
portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
|
|
17
|
-
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=
|
|
18
|
-
portacode/connection/handlers/__init__.py,sha256=
|
|
17
|
+
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=Z5L0gJWoHxV7UonHqxHko_PXZd7Z1mbI6yWtmjxna-s,93951
|
|
18
|
+
portacode/connection/handlers/__init__.py,sha256=y-Aj5SXqc_QJt7i1xkl7kv381Fd2CIcUiG5gR1F8qkI,2711
|
|
19
19
|
portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
|
|
20
20
|
portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
|
|
21
21
|
portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZbQw6sF04M3dM6rUV8Q,24477
|
|
22
22
|
portacode/connection/handlers/file_handlers.py,sha256=nAJH8nXnX07xxD28ngLpgIUzcTuRwZBNpEGEKdRqohw,39507
|
|
23
23
|
portacode/connection/handlers/project_aware_file_handlers.py,sha256=AqgMnDqX2893T2NsrvUSCwjN5VKj4Pb2TN0S_SuboOE,9803
|
|
24
24
|
portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
|
|
25
|
-
portacode/connection/handlers/proxmox_infra.py,sha256=
|
|
25
|
+
portacode/connection/handlers/proxmox_infra.py,sha256=KeYgb7IVdK_nSPJmrPp2W0HR0rlNCQ46DTLRQ63OF9Y,41117
|
|
26
26
|
portacode/connection/handlers/registry.py,sha256=qXGE60sYEWg6ZtVQzFcZ5YI2XWR6lMgw4hAL9x5qR1I,6181
|
|
27
27
|
portacode/connection/handlers/session.py,sha256=uNGfiO_1B9-_yjJKkpvmbiJhIl6b-UXlT86UTfd6WYE,42219
|
|
28
28
|
portacode/connection/handlers/system_handlers.py,sha256=AKh7IbwptlLYrbSw5f-DHigvlaKHsg9lDP-lkAUm8cE,10755
|
|
@@ -64,7 +64,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
|
|
|
64
64
|
portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
|
|
65
65
|
portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
|
|
66
66
|
portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
|
|
67
|
-
portacode-1.4.11.
|
|
67
|
+
portacode-1.4.11.dev7.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
68
68
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
69
69
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
70
70
|
test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
|
|
@@ -90,8 +90,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
|
|
|
90
90
|
testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
|
|
91
91
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
92
92
|
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
93
|
-
portacode-1.4.11.
|
|
94
|
-
portacode-1.4.11.
|
|
95
|
-
portacode-1.4.11.
|
|
96
|
-
portacode-1.4.11.
|
|
97
|
-
portacode-1.4.11.
|
|
93
|
+
portacode-1.4.11.dev7.dist-info/METADATA,sha256=6A_o1zGxAkhlk0n-T3_tozO_7X-fdVO3n6RXjOCCBvs,13051
|
|
94
|
+
portacode-1.4.11.dev7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
95
|
+
portacode-1.4.11.dev7.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
96
|
+
portacode-1.4.11.dev7.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
97
|
+
portacode-1.4.11.dev7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|