portacode 1.4.13.dev1__py3-none-any.whl → 1.4.13.dev2__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 +6 -2
- portacode/connection/handlers/base.py +15 -45
- portacode/connection/handlers/proxmox_infra.py +161 -119
- portacode/connection/handlers/test_proxmox_infra.py +13 -0
- {portacode-1.4.13.dev1.dist-info → portacode-1.4.13.dev2.dist-info}/METADATA +1 -1
- {portacode-1.4.13.dev1.dist-info → portacode-1.4.13.dev2.dist-info}/RECORD +11 -10
- {portacode-1.4.13.dev1.dist-info → portacode-1.4.13.dev2.dist-info}/WHEEL +0 -0
- {portacode-1.4.13.dev1.dist-info → portacode-1.4.13.dev2.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.13.dev1.dist-info → portacode-1.4.13.dev2.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.13.dev1.dist-info → portacode-1.4.13.dev2.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.13.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 13, '
|
|
31
|
+
__version__ = version = '1.4.13.dev2'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 13, 'dev2')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -27,8 +27,6 @@ The Portacode server acts as a **routing middleman** between client sessions and
|
|
|
27
27
|
|
|
28
28
|
- **`source_client_session`** (Server → Device): Server **adds this** when forwarding client commands to devices (so device knows which client sent the command and can target responses back). Clients never include this field.
|
|
29
29
|
|
|
30
|
-
Device handlers that want to respond only to the session that issued a given command can rely on the helper defined in `portacode/connection/handlers/base.py` (`BaseHandler.send_response_to_source_session`). It wraps the boilerplate of reusing `request_id`/`trace`, injecting `client_sessions=[source_client_session]`, and sending via the control channel so the handler does not need to manually reconstruct routing metadata for each reply.
|
|
31
|
-
|
|
32
30
|
This document describes the complete protocol for communicating with devices through the server, guiding app developers on how to get their client sessions to communicate with devices.
|
|
33
31
|
|
|
34
32
|
## Table of Contents
|
|
@@ -363,6 +361,9 @@ Creates a Portacode-managed LXC container, starts it, and bootstraps the Portaco
|
|
|
363
361
|
* `username` (string, optional): OS user to provision (defaults to `svcuser`).
|
|
364
362
|
* `password` (string, optional): Password for the user (used only during provisioning).
|
|
365
363
|
* `ssh_key` (string, optional): SSH public key to add to the user.
|
|
364
|
+
* `device_id` (string, optional): ID of the Device record that already exists on the dashboard.
|
|
365
|
+
* `device_public_key` (string, optional): PEM-encoded Portacode public key. When supplied together with `device_private_key` the handler injects the keypair, records the device metadata, and runs `portacode service install` automatically.
|
|
366
|
+
* `device_private_key` (string, optional): PEM-encoded private key that pairs with `device_public_key`. Both key fields must be present for the automatic service-install mode.
|
|
366
367
|
|
|
367
368
|
**Responses:**
|
|
368
369
|
|
|
@@ -420,6 +421,8 @@ Emitted after a successful `create_proxmox_container` action. Contains the new c
|
|
|
420
421
|
* `public_key` (string): Portacode public auth key created inside the new container.
|
|
421
422
|
* `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
|
|
422
423
|
* `setup_steps` (array[object]): Detailed bootstrap step results (name, stdout/stderr, elapsed time, and status).
|
|
424
|
+
* `device_id` (string, optional): Mirrors the `device_id` supplied with `create_proxmox_container`, if any.
|
|
425
|
+
* `service_installed` (boolean): True when the handler already ran `portacode service install` (with a provided keypair); otherwise it remains False and the dashboard can call `start_portacode_service`.
|
|
423
426
|
|
|
424
427
|
### `proxmox_container_progress`
|
|
425
428
|
|
|
@@ -452,6 +455,7 @@ Runs `sudo portacode service install` inside the container after the dashboard h
|
|
|
452
455
|
* Emits additional [`proxmox_container_progress`](#proxmox_container_progress-event) events to report the authentication and service-install steps.
|
|
453
456
|
* On success, emits a [`proxmox_service_started`](#proxmox_service_started-event).
|
|
454
457
|
* On failure, emits a generic [`error`](#error) event.
|
|
458
|
+
* When `create_proxmox_container` already provided a dashboard-generated keypair, the handler may have installed the service already, so this call is optional unless you need to re-run the install.
|
|
455
459
|
|
|
456
460
|
### `proxmox_service_started`
|
|
457
461
|
|
|
@@ -88,42 +88,6 @@ class BaseHandler(ABC):
|
|
|
88
88
|
if reply_channel:
|
|
89
89
|
payload["reply_channel"] = reply_channel
|
|
90
90
|
await self.control_channel.send(payload)
|
|
91
|
-
|
|
92
|
-
async def send_response_to_source_session(
|
|
93
|
-
self,
|
|
94
|
-
message: Dict[str, Any],
|
|
95
|
-
payload: Dict[str, Any],
|
|
96
|
-
reply_channel: Optional[str] = None,
|
|
97
|
-
) -> None:
|
|
98
|
-
"""Send a response directly to the client session that issued ``message``.
|
|
99
|
-
|
|
100
|
-
This bypasses ``ClientSessionManager`` routing and ensures only the session
|
|
101
|
-
whose ``source_client_session`` was injected by the server will receive the
|
|
102
|
-
response. The helper also copies ``request_id``/``trace`` timing data so tracing
|
|
103
|
-
and logging remain consistent.
|
|
104
|
-
"""
|
|
105
|
-
response = dict(payload)
|
|
106
|
-
request_id = message.get("request_id")
|
|
107
|
-
if request_id and "request_id" not in response:
|
|
108
|
-
response["request_id"] = request_id
|
|
109
|
-
|
|
110
|
-
if "trace" in message and "request_id" in message and "trace" not in response:
|
|
111
|
-
trace_data = dict(message["trace"])
|
|
112
|
-
handler_complete_time = ntp_clock.now_ms()
|
|
113
|
-
if handler_complete_time is not None:
|
|
114
|
-
trace_data["handler_complete"] = handler_complete_time
|
|
115
|
-
if "client_send" in trace_data:
|
|
116
|
-
trace_data["ping"] = handler_complete_time - trace_data["client_send"]
|
|
117
|
-
response["trace"] = trace_data
|
|
118
|
-
|
|
119
|
-
source_client_session = message.get("source_client_session")
|
|
120
|
-
if source_client_session:
|
|
121
|
-
response["client_sessions"] = [source_client_session]
|
|
122
|
-
|
|
123
|
-
if reply_channel:
|
|
124
|
-
response["reply_channel"] = reply_channel
|
|
125
|
-
|
|
126
|
-
await self.control_channel.send(response)
|
|
127
91
|
|
|
128
92
|
async def send_error(self, message: str, reply_channel: Optional[str] = None, project_id: str = None) -> None:
|
|
129
93
|
"""Send an error response with client session awareness.
|
|
@@ -177,16 +141,22 @@ class AsyncHandler(BaseHandler):
|
|
|
177
141
|
if "request_id" in message and "request_id" not in response:
|
|
178
142
|
response["request_id"] = message["request_id"]
|
|
179
143
|
|
|
180
|
-
|
|
181
|
-
|
|
144
|
+
# Pass through trace from request to response (add to existing trace, don't create new one)
|
|
145
|
+
if "trace" in message and "request_id" in message:
|
|
146
|
+
response["trace"] = dict(message["trace"])
|
|
182
147
|
handler_complete_time = ntp_clock.now_ms()
|
|
183
148
|
if handler_complete_time is not None:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
149
|
+
response["trace"]["handler_complete"] = handler_complete_time
|
|
150
|
+
# Update ping to show total time from client_send
|
|
151
|
+
if "client_send" in response["trace"]:
|
|
152
|
+
response["trace"]["ping"] = handler_complete_time - response["trace"]["client_send"]
|
|
153
|
+
logger.info(f"✅ Handler completed: {message['request_id']} ({self.command_name})")
|
|
154
|
+
|
|
155
|
+
# Extract project_id from response for session targeting
|
|
156
|
+
project_id = response.get("project_id")
|
|
157
|
+
logger.info("handler: %s response project_id=%s, response=%s",
|
|
158
|
+
self.command_name, project_id, response)
|
|
159
|
+
await self.send_response(response, reply_channel, project_id)
|
|
190
160
|
else:
|
|
191
161
|
logger.info("handler: %s handled response transmission directly", self.command_name)
|
|
192
162
|
except Exception as exc:
|
|
@@ -246,4 +216,4 @@ class SyncHandler(BaseHandler):
|
|
|
246
216
|
logger.exception("Error in sync handler %s: %s", self.command_name, exc)
|
|
247
217
|
# Extract project_id from original message for error targeting
|
|
248
218
|
project_id = message.get("project_id")
|
|
249
|
-
await self.send_error(str(exc), reply_channel, project_id)
|
|
219
|
+
await self.send_error(str(exc), reply_channel, project_id)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import base64
|
|
6
7
|
import json
|
|
7
8
|
import logging
|
|
8
9
|
import os
|
|
@@ -11,7 +12,6 @@ import shutil
|
|
|
11
12
|
import stat
|
|
12
13
|
import subprocess
|
|
13
14
|
import sys
|
|
14
|
-
import time
|
|
15
15
|
import threading
|
|
16
16
|
from datetime import datetime
|
|
17
17
|
from pathlib import Path
|
|
@@ -59,7 +59,6 @@ def _emit_progress_event(
|
|
|
59
59
|
message: str,
|
|
60
60
|
phase: str,
|
|
61
61
|
request_id: Optional[str],
|
|
62
|
-
client_sessions: Optional[List[str]] = None,
|
|
63
62
|
details: Optional[Dict[str, Any]] = None,
|
|
64
63
|
) -> None:
|
|
65
64
|
loop = handler.context.get("event_loop")
|
|
@@ -85,8 +84,6 @@ def _emit_progress_event(
|
|
|
85
84
|
payload["request_id"] = request_id
|
|
86
85
|
if details:
|
|
87
86
|
payload["details"] = details
|
|
88
|
-
if client_sessions:
|
|
89
|
-
payload["client_sessions"] = client_sessions
|
|
90
87
|
|
|
91
88
|
future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
|
|
92
89
|
future.add_done_callback(
|
|
@@ -431,7 +428,12 @@ def _friendly_step_label(step_name: str) -> str:
|
|
|
431
428
|
return normalized.capitalize()
|
|
432
429
|
|
|
433
430
|
|
|
434
|
-
def _build_bootstrap_steps(
|
|
431
|
+
def _build_bootstrap_steps(
|
|
432
|
+
user: str,
|
|
433
|
+
password: str,
|
|
434
|
+
ssh_key: str,
|
|
435
|
+
include_portacode_connect: bool = True,
|
|
436
|
+
) -> List[Dict[str, Any]]:
|
|
435
437
|
steps = [
|
|
436
438
|
{
|
|
437
439
|
"name": "apt_update",
|
|
@@ -468,11 +470,14 @@ def _build_bootstrap_steps(user: str, password: str, ssh_key: str) -> List[Dict[
|
|
|
468
470
|
"cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
|
|
469
471
|
"retries": 0,
|
|
470
472
|
})
|
|
471
|
-
steps.extend(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
473
|
+
steps.extend(
|
|
474
|
+
[
|
|
475
|
+
{"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
|
|
476
|
+
{"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
|
|
477
|
+
]
|
|
478
|
+
)
|
|
479
|
+
if include_portacode_connect:
|
|
480
|
+
steps.append({"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30})
|
|
476
481
|
return steps
|
|
477
482
|
|
|
478
483
|
|
|
@@ -682,6 +687,36 @@ def _run_pct_check(vmid: int, cmd: str) -> Dict[str, Any]:
|
|
|
682
687
|
return res
|
|
683
688
|
|
|
684
689
|
|
|
690
|
+
def _resolve_portacode_key_dir(vmid: int, user: str) -> str:
|
|
691
|
+
data_dir_cmd = f"su - {user} -c 'echo -n ${{XDG_DATA_HOME:-$HOME/.local/share}}'"
|
|
692
|
+
data_home = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
|
|
693
|
+
return f"{data_home}/portacode/keys"
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _write_bytes_as_user(vmid: int, user: str, path: str, data: bytes, mode: int = 0o600) -> None:
|
|
697
|
+
encoded = base64.b64encode(data).decode()
|
|
698
|
+
path_literal = json.dumps(path)
|
|
699
|
+
script = (
|
|
700
|
+
f"su - {user} -c 'python3 - <<\"PY\"\n"
|
|
701
|
+
"import base64\n"
|
|
702
|
+
"from pathlib import Path\n"
|
|
703
|
+
f"path = Path({path_literal})\n"
|
|
704
|
+
"path.parent.mkdir(parents=True, exist_ok=True)\n"
|
|
705
|
+
f"path.write_bytes(base64.b64decode(\"{encoded}\"))\n"
|
|
706
|
+
f"path.chmod({mode})\n"
|
|
707
|
+
"PY'"
|
|
708
|
+
)
|
|
709
|
+
_run_pct_check(vmid, script)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _deploy_device_keypair(vmid: int, user: str, private_key: str, public_key: str) -> None:
|
|
713
|
+
key_dir = _resolve_portacode_key_dir(vmid, user)
|
|
714
|
+
priv_path = f"{key_dir}/id_portacode"
|
|
715
|
+
pub_path = f"{key_dir}/id_portacode.pub"
|
|
716
|
+
_write_bytes_as_user(vmid, user, priv_path, private_key.encode(), mode=0o600)
|
|
717
|
+
_write_bytes_as_user(vmid, user, pub_path, public_key.encode(), mode=0o644)
|
|
718
|
+
|
|
719
|
+
|
|
685
720
|
def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -> Dict[str, Any]:
|
|
686
721
|
cmd = ["pct", "exec", str(vmid), "--", "bash", "-lc", f"su - {user} -c 'portacode connect'"]
|
|
687
722
|
proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
@@ -883,6 +918,7 @@ def _bootstrap_portacode(
|
|
|
883
918
|
progress_callback: Optional[ProgressCallback] = None,
|
|
884
919
|
start_index: int = 1,
|
|
885
920
|
total_steps: Optional[int] = None,
|
|
921
|
+
default_public_key: Optional[str] = None,
|
|
886
922
|
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
887
923
|
actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
|
|
888
924
|
results, ok = _run_setup_steps(
|
|
@@ -909,7 +945,7 @@ def _bootstrap_portacode(
|
|
|
909
945
|
raise RuntimeError(f"Portacode bootstrap steps failed: {summary}{history_snippet}")
|
|
910
946
|
raise RuntimeError("Portacode bootstrap steps failed.")
|
|
911
947
|
key_step = next((entry for entry in results if entry.get("name") == "portacode_connect"), None)
|
|
912
|
-
public_key = key_step.get("public_key") if key_step else
|
|
948
|
+
public_key = key_step.get("public_key") if key_step else default_public_key
|
|
913
949
|
if not public_key:
|
|
914
950
|
raise RuntimeError("Portacode connect did not return a public key.")
|
|
915
951
|
return public_key, results
|
|
@@ -1053,16 +1089,19 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1053
1089
|
return "create_proxmox_container"
|
|
1054
1090
|
|
|
1055
1091
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1092
|
+
logger.info("create_proxmox_container command received")
|
|
1056
1093
|
request_id = message.get("request_id")
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
source_client_session,
|
|
1062
|
-
)
|
|
1063
|
-
client_sessions = [source_client_session] if source_client_session else None
|
|
1094
|
+
device_id = message.get("device_id")
|
|
1095
|
+
device_public_key = (message.get("device_public_key") or "").strip()
|
|
1096
|
+
device_private_key = (message.get("device_private_key") or "").strip()
|
|
1097
|
+
has_device_keypair = bool(device_public_key and device_private_key)
|
|
1064
1098
|
bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
|
|
1065
|
-
bootstrap_steps = _build_bootstrap_steps(
|
|
1099
|
+
bootstrap_steps = _build_bootstrap_steps(
|
|
1100
|
+
bootstrap_user,
|
|
1101
|
+
bootstrap_password,
|
|
1102
|
+
bootstrap_ssh_key,
|
|
1103
|
+
include_portacode_connect=not has_device_keypair,
|
|
1104
|
+
)
|
|
1066
1105
|
total_steps = 3 + len(bootstrap_steps) + 2
|
|
1067
1106
|
current_step_index = 1
|
|
1068
1107
|
|
|
@@ -1084,7 +1123,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1084
1123
|
message=start_message,
|
|
1085
1124
|
phase="lifecycle",
|
|
1086
1125
|
request_id=request_id,
|
|
1087
|
-
client_sessions=client_sessions,
|
|
1088
1126
|
)
|
|
1089
1127
|
try:
|
|
1090
1128
|
result = action()
|
|
@@ -1099,7 +1137,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1099
1137
|
message=f"{step_label} failed: {exc}",
|
|
1100
1138
|
phase="lifecycle",
|
|
1101
1139
|
request_id=request_id,
|
|
1102
|
-
client_sessions=client_sessions,
|
|
1103
1140
|
details={"error": str(exc)},
|
|
1104
1141
|
)
|
|
1105
1142
|
raise
|
|
@@ -1113,7 +1150,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1113
1150
|
message=success_message,
|
|
1114
1151
|
phase="lifecycle",
|
|
1115
1152
|
request_id=request_id,
|
|
1116
|
-
client_sessions=client_sessions,
|
|
1117
1153
|
)
|
|
1118
1154
|
current_step_index += 1
|
|
1119
1155
|
return result
|
|
@@ -1143,7 +1179,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1143
1179
|
payload["cpuunits"] = max(int(payload["cpus"] * 1024), 10)
|
|
1144
1180
|
payload["memory"] = int(payload["ram_mib"])
|
|
1145
1181
|
payload["node"] = node
|
|
1146
|
-
logger.
|
|
1182
|
+
logger.debug(
|
|
1147
1183
|
"Provisioning container node=%s template=%s ram=%s cpu=%s storage=%s",
|
|
1148
1184
|
node,
|
|
1149
1185
|
payload["template"],
|
|
@@ -1156,12 +1192,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1156
1192
|
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
1157
1193
|
payload["status"] = "creating"
|
|
1158
1194
|
_write_container_record(vmid, payload)
|
|
1159
|
-
logger.info(
|
|
1160
|
-
"Container record written vmid=%s hostname=%s client_session=%s",
|
|
1161
|
-
vmid,
|
|
1162
|
-
payload["hostname"],
|
|
1163
|
-
source_client_session,
|
|
1164
|
-
)
|
|
1165
1195
|
return proxmox, node, vmid, payload
|
|
1166
1196
|
|
|
1167
1197
|
proxmox, node, vmid, payload = _run_lifecycle_step(
|
|
@@ -1220,16 +1250,9 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1220
1250
|
message=message_text,
|
|
1221
1251
|
phase="bootstrap",
|
|
1222
1252
|
request_id=request_id,
|
|
1223
|
-
client_sessions=client_sessions,
|
|
1224
1253
|
details=details or None,
|
|
1225
1254
|
)
|
|
1226
1255
|
|
|
1227
|
-
logger.info(
|
|
1228
|
-
"Bootstrapping Portacode in container vmid=%s user=%s client_session=%s",
|
|
1229
|
-
vmid,
|
|
1230
|
-
payload["username"],
|
|
1231
|
-
source_client_session,
|
|
1232
|
-
)
|
|
1233
1256
|
public_key, steps = _bootstrap_portacode(
|
|
1234
1257
|
vmid,
|
|
1235
1258
|
payload["username"],
|
|
@@ -1239,10 +1262,103 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1239
1262
|
progress_callback=_bootstrap_progress_callback,
|
|
1240
1263
|
start_index=current_step_index,
|
|
1241
1264
|
total_steps=total_steps,
|
|
1265
|
+
default_public_key=device_public_key if has_device_keypair else None,
|
|
1242
1266
|
)
|
|
1243
1267
|
current_step_index += len(bootstrap_steps)
|
|
1244
1268
|
|
|
1245
|
-
|
|
1269
|
+
service_installed = False
|
|
1270
|
+
if has_device_keypair:
|
|
1271
|
+
logger.info(
|
|
1272
|
+
"deploying dashboard-provided Portacode keypair (device_id=%s) into container %s",
|
|
1273
|
+
device_id,
|
|
1274
|
+
vmid,
|
|
1275
|
+
)
|
|
1276
|
+
_deploy_device_keypair(
|
|
1277
|
+
vmid,
|
|
1278
|
+
payload["username"],
|
|
1279
|
+
device_private_key,
|
|
1280
|
+
device_public_key,
|
|
1281
|
+
)
|
|
1282
|
+
service_installed = True
|
|
1283
|
+
service_start_index = current_step_index
|
|
1284
|
+
|
|
1285
|
+
auth_step_name = "setup_device_authentication"
|
|
1286
|
+
auth_label = "Setting up device authentication"
|
|
1287
|
+
_emit_progress_event(
|
|
1288
|
+
self,
|
|
1289
|
+
step_index=service_start_index,
|
|
1290
|
+
total_steps=total_steps,
|
|
1291
|
+
step_name=auth_step_name,
|
|
1292
|
+
step_label=auth_label,
|
|
1293
|
+
status="in_progress",
|
|
1294
|
+
message="Notifying the server of the new device…",
|
|
1295
|
+
phase="service",
|
|
1296
|
+
request_id=request_id,
|
|
1297
|
+
)
|
|
1298
|
+
_emit_progress_event(
|
|
1299
|
+
self,
|
|
1300
|
+
step_index=service_start_index,
|
|
1301
|
+
total_steps=total_steps,
|
|
1302
|
+
step_name=auth_step_name,
|
|
1303
|
+
step_label=auth_label,
|
|
1304
|
+
status="completed",
|
|
1305
|
+
message="Authentication metadata recorded.",
|
|
1306
|
+
phase="service",
|
|
1307
|
+
request_id=request_id,
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
install_step = service_start_index + 1
|
|
1311
|
+
install_label = "Launching Portacode service"
|
|
1312
|
+
_emit_progress_event(
|
|
1313
|
+
self,
|
|
1314
|
+
step_index=install_step,
|
|
1315
|
+
total_steps=total_steps,
|
|
1316
|
+
step_name="launch_portacode_service",
|
|
1317
|
+
step_label=install_label,
|
|
1318
|
+
status="in_progress",
|
|
1319
|
+
message="Running sudo portacode service install…",
|
|
1320
|
+
phase="service",
|
|
1321
|
+
request_id=request_id,
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
cmd = f"su - {payload['username']} -c 'sudo -S portacode service install'"
|
|
1325
|
+
res = _run_pct(vmid, cmd, input_text=payload["password"] + "\n")
|
|
1326
|
+
|
|
1327
|
+
if res["returncode"] != 0:
|
|
1328
|
+
_emit_progress_event(
|
|
1329
|
+
self,
|
|
1330
|
+
step_index=install_step,
|
|
1331
|
+
total_steps=total_steps,
|
|
1332
|
+
step_name="launch_portacode_service",
|
|
1333
|
+
step_label=install_label,
|
|
1334
|
+
status="failed",
|
|
1335
|
+
message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
|
|
1336
|
+
phase="service",
|
|
1337
|
+
request_id=request_id,
|
|
1338
|
+
details={
|
|
1339
|
+
"stderr": res.get("stderr"),
|
|
1340
|
+
"stdout": res.get("stdout"),
|
|
1341
|
+
},
|
|
1342
|
+
)
|
|
1343
|
+
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
1344
|
+
|
|
1345
|
+
_emit_progress_event(
|
|
1346
|
+
self,
|
|
1347
|
+
step_index=install_step,
|
|
1348
|
+
total_steps=total_steps,
|
|
1349
|
+
step_name="launch_portacode_service",
|
|
1350
|
+
step_label=install_label,
|
|
1351
|
+
status="completed",
|
|
1352
|
+
message="Portacode service install finished.",
|
|
1353
|
+
phase="service",
|
|
1354
|
+
request_id=request_id,
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
logger.info("create_proxmox_container: portacode service install completed inside ct %s", vmid)
|
|
1358
|
+
|
|
1359
|
+
current_step_index += 2
|
|
1360
|
+
|
|
1361
|
+
return {
|
|
1246
1362
|
"event": "proxmox_container_created",
|
|
1247
1363
|
"success": True,
|
|
1248
1364
|
"message": f"Container {vmid} is ready and Portacode key captured.",
|
|
@@ -1258,11 +1374,9 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1258
1374
|
"cpus": payload["cpus"],
|
|
1259
1375
|
},
|
|
1260
1376
|
"setup_steps": steps,
|
|
1377
|
+
"device_id": device_id,
|
|
1378
|
+
"service_installed": service_installed,
|
|
1261
1379
|
}
|
|
1262
|
-
if client_sessions:
|
|
1263
|
-
response["client_sessions"] = client_sessions
|
|
1264
|
-
response["_reply_to_source_session"] = True
|
|
1265
|
-
return response
|
|
1266
1380
|
|
|
1267
1381
|
|
|
1268
1382
|
class StartPortacodeServiceHandler(SyncHandler):
|
|
@@ -1290,15 +1404,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1290
1404
|
start_index = int(message.get("step_index", 1))
|
|
1291
1405
|
total_steps = int(message.get("total_steps", start_index + 2))
|
|
1292
1406
|
request_id = message.get("request_id")
|
|
1293
|
-
source_client_session = message.get("source_client_session")
|
|
1294
|
-
client_sessions = [source_client_session] if source_client_session else None
|
|
1295
|
-
|
|
1296
|
-
logger.info(
|
|
1297
|
-
"start_portacode_service invoked vmid=%s user=%s client_session=%s",
|
|
1298
|
-
vmid,
|
|
1299
|
-
user,
|
|
1300
|
-
source_client_session,
|
|
1301
|
-
)
|
|
1302
1407
|
|
|
1303
1408
|
auth_step_name = "setup_device_authentication"
|
|
1304
1409
|
auth_label = "Setting up device authentication"
|
|
@@ -1312,7 +1417,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1312
1417
|
message="Notifying the server of the new device…",
|
|
1313
1418
|
phase="service",
|
|
1314
1419
|
request_id=request_id,
|
|
1315
|
-
client_sessions=client_sessions,
|
|
1316
1420
|
)
|
|
1317
1421
|
_emit_progress_event(
|
|
1318
1422
|
self,
|
|
@@ -1324,7 +1428,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1324
1428
|
message="Authentication metadata recorded.",
|
|
1325
1429
|
phase="service",
|
|
1326
1430
|
request_id=request_id,
|
|
1327
|
-
client_sessions=client_sessions,
|
|
1328
1431
|
)
|
|
1329
1432
|
|
|
1330
1433
|
install_step = start_index + 1
|
|
@@ -1339,7 +1442,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1339
1442
|
message="Running sudo portacode service install…",
|
|
1340
1443
|
phase="service",
|
|
1341
1444
|
request_id=request_id,
|
|
1342
|
-
client_sessions=client_sessions,
|
|
1343
1445
|
)
|
|
1344
1446
|
|
|
1345
1447
|
cmd = f"su - {user} -c 'sudo -S portacode service install'"
|
|
@@ -1356,7 +1458,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1356
1458
|
message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
|
|
1357
1459
|
phase="service",
|
|
1358
1460
|
request_id=request_id,
|
|
1359
|
-
client_sessions=client_sessions,
|
|
1360
1461
|
details={
|
|
1361
1462
|
"stderr": res.get("stderr"),
|
|
1362
1463
|
"stdout": res.get("stdout"),
|
|
@@ -1364,12 +1465,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1364
1465
|
)
|
|
1365
1466
|
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
1366
1467
|
|
|
1367
|
-
logger.info(
|
|
1368
|
-
"portacode service install command completed vmid=%s returncode=%s",
|
|
1369
|
-
vmid,
|
|
1370
|
-
res["returncode"],
|
|
1371
|
-
)
|
|
1372
|
-
|
|
1373
1468
|
_emit_progress_event(
|
|
1374
1469
|
self,
|
|
1375
1470
|
step_index=install_step,
|
|
@@ -1380,19 +1475,14 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1380
1475
|
message="Portacode service install finished.",
|
|
1381
1476
|
phase="service",
|
|
1382
1477
|
request_id=request_id,
|
|
1383
|
-
client_sessions=client_sessions,
|
|
1384
1478
|
)
|
|
1385
1479
|
|
|
1386
|
-
|
|
1480
|
+
return {
|
|
1387
1481
|
"event": "proxmox_service_started",
|
|
1388
1482
|
"success": True,
|
|
1389
1483
|
"message": "Portacode service install completed",
|
|
1390
1484
|
"ctid": str(vmid),
|
|
1391
1485
|
}
|
|
1392
|
-
if client_sessions:
|
|
1393
|
-
response["client_sessions"] = client_sessions
|
|
1394
|
-
response["_reply_to_source_session"] = True
|
|
1395
|
-
return response
|
|
1396
1486
|
|
|
1397
1487
|
|
|
1398
1488
|
class StartProxmoxContainerHandler(SyncHandler):
|
|
@@ -1408,20 +1498,12 @@ class StartProxmoxContainerHandler(SyncHandler):
|
|
|
1408
1498
|
proxmox = _connect_proxmox(config)
|
|
1409
1499
|
node = _get_node_from_config(config)
|
|
1410
1500
|
_ensure_container_managed(proxmox, node, vmid)
|
|
1411
|
-
source_client_session = message.get("source_client_session")
|
|
1412
|
-
client_sessions = [source_client_session] if source_client_session else None
|
|
1413
|
-
|
|
1414
|
-
logger.info(
|
|
1415
|
-
"start_proxmox_container invoked vmid=%s client_session=%s",
|
|
1416
|
-
vmid,
|
|
1417
|
-
source_client_session,
|
|
1418
|
-
)
|
|
1419
1501
|
|
|
1420
1502
|
status, elapsed = _start_container(proxmox, node, vmid)
|
|
1421
1503
|
_update_container_record(vmid, {"status": "running"})
|
|
1422
1504
|
|
|
1423
1505
|
infra = get_infra_snapshot()
|
|
1424
|
-
|
|
1506
|
+
return {
|
|
1425
1507
|
"event": "proxmox_container_action",
|
|
1426
1508
|
"action": "start",
|
|
1427
1509
|
"success": True,
|
|
@@ -1431,10 +1513,6 @@ class StartProxmoxContainerHandler(SyncHandler):
|
|
|
1431
1513
|
"status": status.get("status"),
|
|
1432
1514
|
"infra": infra,
|
|
1433
1515
|
}
|
|
1434
|
-
if client_sessions:
|
|
1435
|
-
response["client_sessions"] = client_sessions
|
|
1436
|
-
response["_reply_to_source_session"] = True
|
|
1437
|
-
return response
|
|
1438
1516
|
|
|
1439
1517
|
|
|
1440
1518
|
class StopProxmoxContainerHandler(SyncHandler):
|
|
@@ -1450,14 +1528,6 @@ class StopProxmoxContainerHandler(SyncHandler):
|
|
|
1450
1528
|
proxmox = _connect_proxmox(config)
|
|
1451
1529
|
node = _get_node_from_config(config)
|
|
1452
1530
|
_ensure_container_managed(proxmox, node, vmid)
|
|
1453
|
-
source_client_session = message.get("source_client_session")
|
|
1454
|
-
client_sessions = [source_client_session] if source_client_session else None
|
|
1455
|
-
|
|
1456
|
-
logger.info(
|
|
1457
|
-
"stop_proxmox_container invoked vmid=%s client_session=%s",
|
|
1458
|
-
vmid,
|
|
1459
|
-
source_client_session,
|
|
1460
|
-
)
|
|
1461
1531
|
|
|
1462
1532
|
status, elapsed = _stop_container(proxmox, node, vmid)
|
|
1463
1533
|
final_status = status.get("status") or "stopped"
|
|
@@ -1469,7 +1539,7 @@ class StopProxmoxContainerHandler(SyncHandler):
|
|
|
1469
1539
|
if final_status != "running" and elapsed == 0.0
|
|
1470
1540
|
else f"Stopped container {vmid} in {elapsed:.1f}s."
|
|
1471
1541
|
)
|
|
1472
|
-
|
|
1542
|
+
return {
|
|
1473
1543
|
"event": "proxmox_container_action",
|
|
1474
1544
|
"action": "stop",
|
|
1475
1545
|
"success": True,
|
|
@@ -1479,10 +1549,6 @@ class StopProxmoxContainerHandler(SyncHandler):
|
|
|
1479
1549
|
"status": final_status,
|
|
1480
1550
|
"infra": infra,
|
|
1481
1551
|
}
|
|
1482
|
-
if client_sessions:
|
|
1483
|
-
response["client_sessions"] = client_sessions
|
|
1484
|
-
response["_reply_to_source_session"] = True
|
|
1485
|
-
return response
|
|
1486
1552
|
|
|
1487
1553
|
|
|
1488
1554
|
class RemoveProxmoxContainerHandler(SyncHandler):
|
|
@@ -1498,21 +1564,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
1498
1564
|
proxmox = _connect_proxmox(config)
|
|
1499
1565
|
node = _get_node_from_config(config)
|
|
1500
1566
|
_ensure_container_managed(proxmox, node, vmid)
|
|
1501
|
-
source_client_session = message.get("source_client_session")
|
|
1502
|
-
client_sessions = [source_client_session] if source_client_session else None
|
|
1503
|
-
|
|
1504
|
-
logger.info(
|
|
1505
|
-
"remove_proxmox_container invoked vmid=%s client_session=%s",
|
|
1506
|
-
vmid,
|
|
1507
|
-
source_client_session,
|
|
1508
|
-
)
|
|
1509
1567
|
|
|
1510
1568
|
stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
|
|
1511
1569
|
delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
|
|
1512
1570
|
_remove_container_record(vmid)
|
|
1513
1571
|
|
|
1514
1572
|
infra = get_infra_snapshot()
|
|
1515
|
-
|
|
1573
|
+
return {
|
|
1516
1574
|
"event": "proxmox_container_action",
|
|
1517
1575
|
"action": "remove",
|
|
1518
1576
|
"success": True,
|
|
@@ -1525,10 +1583,6 @@ class RemoveProxmoxContainerHandler(SyncHandler):
|
|
|
1525
1583
|
"status": "deleted",
|
|
1526
1584
|
"infra": infra,
|
|
1527
1585
|
}
|
|
1528
|
-
if client_sessions:
|
|
1529
|
-
response["client_sessions"] = client_sessions
|
|
1530
|
-
response["_reply_to_source_session"] = True
|
|
1531
|
-
return response
|
|
1532
1586
|
|
|
1533
1587
|
|
|
1534
1588
|
class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
@@ -1542,19 +1596,13 @@ class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
|
1542
1596
|
verify_ssl = bool(message.get("verify_ssl"))
|
|
1543
1597
|
if not token_identifier or not token_value:
|
|
1544
1598
|
raise ValueError("token_identifier and token_value are required")
|
|
1545
|
-
source_client_session = message.get("source_client_session")
|
|
1546
|
-
client_sessions = [source_client_session] if source_client_session else None
|
|
1547
1599
|
snapshot = configure_infrastructure(token_identifier, token_value, verify_ssl=verify_ssl)
|
|
1548
|
-
|
|
1600
|
+
return {
|
|
1549
1601
|
"event": "proxmox_infra_configured",
|
|
1550
1602
|
"success": True,
|
|
1551
1603
|
"message": "Proxmox infrastructure configured",
|
|
1552
1604
|
"infra": snapshot,
|
|
1553
1605
|
}
|
|
1554
|
-
if client_sessions:
|
|
1555
|
-
response["client_sessions"] = client_sessions
|
|
1556
|
-
response["_reply_to_source_session"] = True
|
|
1557
|
-
return response
|
|
1558
1606
|
|
|
1559
1607
|
|
|
1560
1608
|
class RevertProxmoxInfraHandler(SyncHandler):
|
|
@@ -1563,16 +1611,10 @@ class RevertProxmoxInfraHandler(SyncHandler):
|
|
|
1563
1611
|
return "revert_proxmox_infra"
|
|
1564
1612
|
|
|
1565
1613
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1566
|
-
source_client_session = message.get("source_client_session")
|
|
1567
|
-
client_sessions = [source_client_session] if source_client_session else None
|
|
1568
1614
|
snapshot = revert_infrastructure()
|
|
1569
|
-
|
|
1615
|
+
return {
|
|
1570
1616
|
"event": "proxmox_infra_reverted",
|
|
1571
1617
|
"success": True,
|
|
1572
1618
|
"message": "Proxmox infrastructure configuration reverted",
|
|
1573
1619
|
"infra": snapshot,
|
|
1574
1620
|
}
|
|
1575
|
-
if client_sessions:
|
|
1576
|
-
response["client_sessions"] = client_sessions
|
|
1577
|
-
response["_reply_to_source_session"] = True
|
|
1578
|
-
return response
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
|
|
3
|
+
from portacode.connection.handlers.proxmox_infra import _build_bootstrap_steps
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProxmoxInfraHandlerTests(TestCase):
|
|
7
|
+
def test_build_bootstrap_steps_includes_portacode_connect_by_default(self):
|
|
8
|
+
steps = _build_bootstrap_steps("svcuser", "pass", "", include_portacode_connect=True)
|
|
9
|
+
self.assertTrue(any(step.get("name") == "portacode_connect" for step in steps))
|
|
10
|
+
|
|
11
|
+
def test_build_bootstrap_steps_skips_portacode_connect_when_requested(self):
|
|
12
|
+
steps = _build_bootstrap_steps("svcuser", "pass", "", include_portacode_connect=False)
|
|
13
|
+
self.assertFalse(any(step.get("name") == "portacode_connect" for step in steps))
|
|
@@ -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=DNXD9h6WnEeb90CTXVgfmtrpDU4zmIz1R3oJab1QQyY,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
|
|
@@ -14,20 +14,21 @@ portacode/connection/client.py,sha256=jtLb9_YufqPkzi9t8VQH3iz_JEMisbtY6a8L9U5wei
|
|
|
14
14
|
portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
|
|
15
15
|
portacode/connection/terminal.py,sha256=oyLPOVLPlUuN_eRvHPGazB51yi8W8JEF3oOEYxucGTE,45069
|
|
16
16
|
portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
|
|
17
|
-
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256
|
|
17
|
+
portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=--NNK-qrqQhZ9O4RHuAQfDQb6QxKtl_No101c1Sy_cQ,98994
|
|
18
18
|
portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
|
|
19
|
-
portacode/connection/handlers/base.py,sha256=
|
|
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=m9JKbJUBIpuv-7QGU1LBVqoZJXvPQzhNcx1MLatjNhs,58972
|
|
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
|
|
29
29
|
portacode/connection/handlers/tab_factory.py,sha256=yn93h6GASjD1VpvW1oqpax3EpoT0r7r97zFXxML1wdA,16173
|
|
30
30
|
portacode/connection/handlers/terminal_handlers.py,sha256=HRwHW1GiqG1NtHVEqXHKaYkFfQEzCDDH6YIlHcb4XD8,11866
|
|
31
|
+
portacode/connection/handlers/test_proxmox_infra.py,sha256=d6iBB4pwAqWWdEGRayLxDEexqCElbGZDJlCB4bXba24,682
|
|
31
32
|
portacode/connection/handlers/update_handler.py,sha256=f2K4LmG4sHJZ3LahzzoRtHBULTKkPUNwuyhwuAAg3RA,2054
|
|
32
33
|
portacode/connection/handlers/project_state/README.md,sha256=trdd4ig6ungmwH5SpbSLfyxbL-QgPlGNU-_XrMEiXtw,10114
|
|
33
34
|
portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL8r_wVs5Tlt6B9J7yQH6yQUt7gc,2541
|
|
@@ -64,7 +65,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
|
|
|
64
65
|
portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
|
|
65
66
|
portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
|
|
66
67
|
portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
|
|
67
|
-
portacode-1.4.13.
|
|
68
|
+
portacode-1.4.13.dev2.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
68
69
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
69
70
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
70
71
|
test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
|
|
@@ -90,8 +91,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
|
|
|
90
91
|
testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
|
|
91
92
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
92
93
|
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
93
|
-
portacode-1.4.13.
|
|
94
|
-
portacode-1.4.13.
|
|
95
|
-
portacode-1.4.13.
|
|
96
|
-
portacode-1.4.13.
|
|
97
|
-
portacode-1.4.13.
|
|
94
|
+
portacode-1.4.13.dev2.dist-info/METADATA,sha256=mVp7O5PlhPfvcaUdyshscyW9aH8bfYJ-FNIRkhunycE,13051
|
|
95
|
+
portacode-1.4.13.dev2.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
96
|
+
portacode-1.4.13.dev2.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.13.dev2.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.13.dev2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|