portacode 1.4.15__py3-none-any.whl → 1.4.15.dev0__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 +8 -14
- portacode/connection/handlers/project_state/manager.py +3 -25
- portacode/connection/handlers/proxmox_infra.py +127 -364
- portacode/connection/terminal.py +0 -80
- {portacode-1.4.15.dist-info → portacode-1.4.15.dev0.dist-info}/METADATA +1 -1
- {portacode-1.4.15.dist-info → portacode-1.4.15.dev0.dist-info}/RECORD +11 -11
- {portacode-1.4.15.dist-info → portacode-1.4.15.dev0.dist-info}/WHEEL +1 -1
- {portacode-1.4.15.dist-info → portacode-1.4.15.dev0.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.15.dist-info → portacode-1.4.15.dev0.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.15.dist-info → portacode-1.4.15.dev0.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.15'
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 15)
|
|
31
|
+
__version__ = version = '1.4.15.dev0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 15, 'dev0')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -27,10 +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
|
-
### Proxying infrastructure updates
|
|
31
|
-
|
|
32
|
-
Portacode infrastructure devices (like the proxmox host) can send events on behalf of the LXC Devices they manage. Such messages include the optional `on_behalf_of_device` field and the server silently replaces `device_id` with that child device before routing. The gateway enforces that the sender is the child’s `proxmox_parent` (via `Device.proxmox_parent`) so only the infrastructure owner can impersonate a child device. Messages that fail this check are dropped.
|
|
33
|
-
|
|
34
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.
|
|
35
31
|
|
|
36
32
|
## Table of Contents
|
|
@@ -330,9 +326,10 @@ Configures a Proxmox node for Portacode infrastructure usage (API token validati
|
|
|
330
326
|
|
|
331
327
|
**Payload Fields:**
|
|
332
328
|
|
|
333
|
-
* `token_identifier` (string,
|
|
334
|
-
* `token_value` (string,
|
|
335
|
-
* `verify_ssl` (boolean, optional): When true, the handler verifies SSL certificates; defaults to `false`.
|
|
329
|
+
* `token_identifier` (string, optional when reconfiguring): API token identifier in the form `user@realm!tokenid`. Required on first configuration or when replacing the stored token.
|
|
330
|
+
* `token_value` (string, optional when reconfiguring): Secret value associated with the token. Required when `token_identifier` is supplied.
|
|
331
|
+
* `verify_ssl` (boolean, optional): When true, the handler verifies SSL certificates; defaults to `false`. When omitted, the last configured value is preserved.
|
|
332
|
+
* `cloudflare_api_token` (string, optional): Cloudflare API token the host can reuse later to provision tunnels.
|
|
336
333
|
|
|
337
334
|
**Responses:**
|
|
338
335
|
|
|
@@ -365,7 +362,7 @@ Creates a Portacode-managed LXC container, starts it, and bootstraps the Portaco
|
|
|
365
362
|
* `username` (string, optional): OS user to provision (defaults to `svcuser`).
|
|
366
363
|
* `password` (string, optional): Password for the user (used only during provisioning).
|
|
367
364
|
* `ssh_key` (string, optional): SSH public key to add to the user.
|
|
368
|
-
* `device_id` (string,
|
|
365
|
+
* `device_id` (string, optional): ID of the Device record that already exists on the dashboard.
|
|
369
366
|
* `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.
|
|
370
367
|
* `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.
|
|
371
368
|
|
|
@@ -425,8 +422,7 @@ Emitted after a successful `create_proxmox_container` action. Contains the new c
|
|
|
425
422
|
* `public_key` (string): Portacode public auth key created inside the new container.
|
|
426
423
|
* `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
|
|
427
424
|
* `setup_steps` (array[object]): Detailed bootstrap step results (name, stdout/stderr, elapsed time, and status).
|
|
428
|
-
* `device_id` (string): Mirrors the
|
|
429
|
-
* `on_behalf_of_device` (string): Same value as `device_id` when the container host is reporting progress for the child device; only proxmox parents may include this field.
|
|
425
|
+
* `device_id` (string, optional): Mirrors the `device_id` supplied with `create_proxmox_container`, if any.
|
|
430
426
|
* `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`.
|
|
431
427
|
|
|
432
428
|
### `proxmox_container_progress`
|
|
@@ -444,7 +440,6 @@ Sent intermittently while `create_proxmox_container` is executing so callers can
|
|
|
444
440
|
* `message` (string): Short description of what is happening or why a failure occurred.
|
|
445
441
|
* `details` (object, optional): Contains `attempt` (if retries were needed) and `error_summary` when a step fails.
|
|
446
442
|
* `request_id` (string, optional): Mirrors the request ID from the incoming `create_proxmox_container` payload when available.
|
|
447
|
-
* `on_behalf_of_device` (string, optional): When present the proxmox device is reporting progress for the referenced dashboard device; the gateway verifies the proxmox node is the child’s `proxmox_parent` before routing the event.
|
|
448
443
|
|
|
449
444
|
### `start_portacode_service`
|
|
450
445
|
|
|
@@ -1132,6 +1127,8 @@ Provides system information in response to a `system_info` action. Handled by [`
|
|
|
1132
1127
|
* `message` (string|null): Informational text about the network setup attempt.
|
|
1133
1128
|
* `bridge` (string): The bridge interface configured (typically `vmbr1`).
|
|
1134
1129
|
* `health` (string|null): `"healthy"` when the connectivity verification succeeded.
|
|
1130
|
+
* `cloudflare` (object): Optional Cloudflare metadata collected for future tunnels:
|
|
1131
|
+
* `configured` (boolean): True when a Cloudflare API token is stored.
|
|
1135
1132
|
* `node_status` (object|null): Status response returned by the Proxmox API when validating the token.
|
|
1136
1133
|
* `managed_containers` (object): Cached summary of the Portacode-managed containers:
|
|
1137
1134
|
* `updated_at` (string): ISO timestamp when this snapshot was last refreshed.
|
|
@@ -1183,8 +1180,6 @@ Emitted after a successful `create_proxmox_container` action to report the newly
|
|
|
1183
1180
|
* `public_key` (string): Portacode public auth key discovered inside the container.
|
|
1184
1181
|
* `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
|
|
1185
1182
|
* `setup_steps` (array[object]): Detailed bootstrap step reports including stdout/stderr, elapsed time, and pass/fail status.
|
|
1186
|
-
* `device_id` (string): Mirrors the device ID supplied with `create_proxmox_container` and persisted inside the host metadata file for this CT.
|
|
1187
|
-
* `on_behalf_of_device` (string): Same value as `device_id` when the container host is reporting progress for the child device.
|
|
1188
1183
|
|
|
1189
1184
|
### `proxmox_container_progress`
|
|
1190
1185
|
|
|
@@ -1201,7 +1196,6 @@ Sent continuously while `create_proxmox_container` runs so dashboards can show a
|
|
|
1201
1196
|
* `message` (string): Short human-readable description of the action or failure.
|
|
1202
1197
|
* `details` (object, optional): Contains `attempt` (when retries are used) and `error_summary` on failure.
|
|
1203
1198
|
* `request_id` (string, optional): Mirrors the `create_proxmox_container` request when provided.
|
|
1204
|
-
* `on_behalf_of_device` (string, optional): Mirrors the child device ID when a proxmox host reports progress for that child; only proxmox parents can supply this field.
|
|
1205
1199
|
|
|
1206
1200
|
### `proxmox_container_action`
|
|
1207
1201
|
|
|
@@ -21,24 +21,6 @@ from .git_manager import GitManager
|
|
|
21
21
|
from .file_system_watcher import FileSystemWatcher
|
|
22
22
|
from ....logging_categories import get_categorized_logger, LogCategory
|
|
23
23
|
|
|
24
|
-
def _deterministic_file_tab_id(file_path: str) -> str:
|
|
25
|
-
try:
|
|
26
|
-
resolved = os.path.abspath(file_path)
|
|
27
|
-
mtime = int(Path(resolved).stat().st_mtime)
|
|
28
|
-
except OSError:
|
|
29
|
-
mtime = "unknown"
|
|
30
|
-
resolved = os.path.abspath(file_path)
|
|
31
|
-
return f"{resolved}:{mtime}"
|
|
32
|
-
|
|
33
|
-
def _deterministic_diff_tab_id(
|
|
34
|
-
file_path: str,
|
|
35
|
-
from_ref: str,
|
|
36
|
-
to_ref: str,
|
|
37
|
-
from_hash: Optional[str],
|
|
38
|
-
to_hash: Optional[str]
|
|
39
|
-
) -> str:
|
|
40
|
-
return f"diff:{os.path.abspath(file_path)}:{from_ref}:{to_ref}:{from_hash or ''}:{to_hash or ''}"
|
|
41
|
-
|
|
42
24
|
logger = get_categorized_logger(__name__)
|
|
43
25
|
|
|
44
26
|
# Global singleton instance
|
|
@@ -675,10 +657,10 @@ class ProjectStateManager:
|
|
|
675
657
|
# Create new file tab using tab factory
|
|
676
658
|
from ..tab_factory import get_tab_factory
|
|
677
659
|
tab_factory = get_tab_factory()
|
|
678
|
-
|
|
660
|
+
|
|
679
661
|
try:
|
|
680
662
|
logger.info(f"About to create tab for file: {file_path}")
|
|
681
|
-
new_tab = await tab_factory.create_file_tab(file_path
|
|
663
|
+
new_tab = await tab_factory.create_file_tab(file_path)
|
|
682
664
|
logger.info(f"Tab created successfully, adding to project state")
|
|
683
665
|
project_state.open_tabs[tab_key] = new_tab
|
|
684
666
|
if set_active:
|
|
@@ -860,11 +842,7 @@ class ProjectStateManager:
|
|
|
860
842
|
diff_title = f"{os.path.basename(file_path)} ({' '.join(title_parts)})"
|
|
861
843
|
|
|
862
844
|
diff_tab = await tab_factory.create_diff_tab_with_title(
|
|
863
|
-
file_path,
|
|
864
|
-
original_content,
|
|
865
|
-
modified_content,
|
|
866
|
-
diff_title,
|
|
867
|
-
tab_id=_deterministic_diff_tab_id(file_path, from_ref, to_ref, from_hash, to_hash),
|
|
845
|
+
file_path, original_content, modified_content, diff_title,
|
|
868
846
|
diff_details=diff_details
|
|
869
847
|
)
|
|
870
848
|
|
|
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
-
import math
|
|
9
8
|
import os
|
|
10
9
|
import secrets
|
|
11
10
|
import shlex
|
|
@@ -16,7 +15,7 @@ import sys
|
|
|
16
15
|
import tempfile
|
|
17
16
|
import time
|
|
18
17
|
import threading
|
|
19
|
-
from datetime import datetime
|
|
18
|
+
from datetime import datetime
|
|
20
19
|
from pathlib import Path
|
|
21
20
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
22
21
|
|
|
@@ -47,7 +46,6 @@ UNIT_DIR = Path("/etc/systemd/system")
|
|
|
47
46
|
_MANAGED_CONTAINERS_CACHE_TTL_S = 30.0
|
|
48
47
|
_MANAGED_CONTAINERS_CACHE: Dict[str, Any] = {"timestamp": 0.0, "summary": None}
|
|
49
48
|
_MANAGED_CONTAINERS_CACHE_LOCK = threading.Lock()
|
|
50
|
-
TEMPLATES_REFRESH_INTERVAL_S = 300
|
|
51
49
|
|
|
52
50
|
ProgressCallback = Callable[[int, int, Dict[str, Any], str, Optional[Dict[str, Any]]], None]
|
|
53
51
|
|
|
@@ -64,7 +62,6 @@ def _emit_progress_event(
|
|
|
64
62
|
phase: str,
|
|
65
63
|
request_id: Optional[str],
|
|
66
64
|
details: Optional[Dict[str, Any]] = None,
|
|
67
|
-
on_behalf_of_device: Optional[str] = None,
|
|
68
65
|
) -> None:
|
|
69
66
|
loop = handler.context.get("event_loop")
|
|
70
67
|
if not loop or loop.is_closed():
|
|
@@ -89,8 +86,6 @@ def _emit_progress_event(
|
|
|
89
86
|
payload["request_id"] = request_id
|
|
90
87
|
if details:
|
|
91
88
|
payload["details"] = details
|
|
92
|
-
if on_behalf_of_device:
|
|
93
|
-
payload["on_behalf_of_device"] = str(on_behalf_of_device)
|
|
94
89
|
|
|
95
90
|
future = asyncio.run_coroutine_threadsafe(handler.send_response(payload), loop)
|
|
96
91
|
future.add_done_callback(
|
|
@@ -180,64 +175,6 @@ def _list_templates(client: Any, node: str, storages: Iterable[Dict[str, Any]])
|
|
|
180
175
|
return templates
|
|
181
176
|
|
|
182
177
|
|
|
183
|
-
def _build_proxmox_client_from_config(config: Dict[str, Any]):
|
|
184
|
-
user = config.get("user")
|
|
185
|
-
token_name = config.get("token_name")
|
|
186
|
-
token_value = config.get("token_value")
|
|
187
|
-
if not user or not token_name or not token_value:
|
|
188
|
-
raise RuntimeError("Proxmox API credentials are missing")
|
|
189
|
-
ProxmoxAPI = _ensure_proxmoxer()
|
|
190
|
-
return ProxmoxAPI(
|
|
191
|
-
config.get("host", DEFAULT_HOST),
|
|
192
|
-
user=user,
|
|
193
|
-
token_name=token_name,
|
|
194
|
-
token_value=token_value,
|
|
195
|
-
verify_ssl=config.get("verify_ssl", False),
|
|
196
|
-
timeout=30,
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def _current_time_iso() -> str:
|
|
201
|
-
return datetime.now(timezone.utc).isoformat()
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
def _parse_iso_timestamp(value: str) -> Optional[datetime]:
|
|
205
|
-
if not value:
|
|
206
|
-
return None
|
|
207
|
-
text = value
|
|
208
|
-
if text.endswith("Z"):
|
|
209
|
-
text = text[:-1] + "+00:00"
|
|
210
|
-
try:
|
|
211
|
-
return datetime.fromisoformat(text)
|
|
212
|
-
except ValueError:
|
|
213
|
-
return None
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def _templates_need_refresh(config: Dict[str, Any]) -> bool:
|
|
217
|
-
if not config or not config.get("token_value"):
|
|
218
|
-
return False
|
|
219
|
-
last = _parse_iso_timestamp(config.get("templates_last_refreshed") or "")
|
|
220
|
-
if not last:
|
|
221
|
-
return True
|
|
222
|
-
return (datetime.now(timezone.utc) - last).total_seconds() >= TEMPLATES_REFRESH_INTERVAL_S
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _ensure_templates_refreshed_on_startup(config: Dict[str, Any]) -> None:
|
|
226
|
-
if not _templates_need_refresh(config):
|
|
227
|
-
return
|
|
228
|
-
try:
|
|
229
|
-
client = _build_proxmox_client_from_config(config)
|
|
230
|
-
node = config.get("node") or _pick_node(client)
|
|
231
|
-
storages = client.nodes(node).storage.get()
|
|
232
|
-
templates = _list_templates(client, node, storages)
|
|
233
|
-
if templates:
|
|
234
|
-
config["templates"] = templates
|
|
235
|
-
config["templates_last_refreshed"] = _current_time_iso()
|
|
236
|
-
_save_config(config)
|
|
237
|
-
except Exception as exc:
|
|
238
|
-
logger.warning("Unable to refresh Proxmox templates on startup: %s", exc)
|
|
239
|
-
|
|
240
|
-
|
|
241
178
|
def _pick_storage(storages: Iterable[Dict[str, Any]]) -> str:
|
|
242
179
|
candidates = [s for s in storages if "rootdir" in s.get("content", "") and s.get("avail", 0) > 0]
|
|
243
180
|
if not candidates:
|
|
@@ -496,171 +433,48 @@ def _friendly_step_label(step_name: str) -> str:
|
|
|
496
433
|
return normalized.capitalize()
|
|
497
434
|
|
|
498
435
|
|
|
499
|
-
_NETWORK_WAIT_CMD = (
|
|
500
|
-
"count=0; "
|
|
501
|
-
"while [ \"$count\" -lt 20 ]; do "
|
|
502
|
-
" if command -v ip >/dev/null 2>&1 && ip route get 1.1.1.1 >/dev/null 2>&1; then break; fi; "
|
|
503
|
-
" if [ -f /proc/net/route ] && grep -q '^00000000' /proc/net/route >/dev/null 2>&1; then break; fi; "
|
|
504
|
-
" sleep 1; "
|
|
505
|
-
" count=$((count+1)); "
|
|
506
|
-
"done"
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
_PACKAGE_MANAGER_PROFILES: Dict[str, Dict[str, Any]] = {
|
|
510
|
-
"apt": {
|
|
511
|
-
"update_cmd": "apt-get update -y",
|
|
512
|
-
"update_step_name": "apt_update",
|
|
513
|
-
"install_cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
|
|
514
|
-
"install_step_name": "install_deps",
|
|
515
|
-
"update_retries": 4,
|
|
516
|
-
"install_retries": 5,
|
|
517
|
-
},
|
|
518
|
-
"dnf": {
|
|
519
|
-
"update_cmd": "dnf check-update || true",
|
|
520
|
-
"update_step_name": "dnf_update",
|
|
521
|
-
"install_cmd": "dnf install -y python3 python3-pip sudo",
|
|
522
|
-
"install_step_name": "install_deps",
|
|
523
|
-
"update_retries": 3,
|
|
524
|
-
"install_retries": 5,
|
|
525
|
-
},
|
|
526
|
-
"yum": {
|
|
527
|
-
"update_cmd": "yum makecache",
|
|
528
|
-
"update_step_name": "yum_update",
|
|
529
|
-
"install_cmd": "yum install -y python3 python3-pip sudo",
|
|
530
|
-
"install_step_name": "install_deps",
|
|
531
|
-
"update_retries": 3,
|
|
532
|
-
"install_retries": 5,
|
|
533
|
-
},
|
|
534
|
-
"apk": {
|
|
535
|
-
"update_cmd": "apk update",
|
|
536
|
-
"update_step_name": "apk_update",
|
|
537
|
-
"install_cmd": "apk add --no-cache python3 py3-pip sudo shadow",
|
|
538
|
-
"install_step_name": "install_deps",
|
|
539
|
-
"update_retries": 3,
|
|
540
|
-
"install_retries": 5,
|
|
541
|
-
},
|
|
542
|
-
"pacman": {
|
|
543
|
-
"update_cmd": "pacman -Sy --noconfirm",
|
|
544
|
-
"update_step_name": "pacman_update",
|
|
545
|
-
"install_cmd": "pacman -S --noconfirm python python-pip sudo",
|
|
546
|
-
"install_step_name": "install_deps",
|
|
547
|
-
"update_retries": 3,
|
|
548
|
-
"install_retries": 5,
|
|
549
|
-
},
|
|
550
|
-
"zypper": {
|
|
551
|
-
"update_cmd": "zypper refresh",
|
|
552
|
-
"update_step_name": "zypper_update",
|
|
553
|
-
"install_cmd": "zypper install -y python3 python3-pip sudo",
|
|
554
|
-
"install_step_name": "install_deps",
|
|
555
|
-
"update_retries": 3,
|
|
556
|
-
"install_retries": 5,
|
|
557
|
-
},
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
_UPDATE_RETRY_ON = [
|
|
561
|
-
"Temporary failure resolving",
|
|
562
|
-
"Could not resolve",
|
|
563
|
-
"Failed to fetch",
|
|
564
|
-
]
|
|
565
|
-
|
|
566
|
-
_INSTALL_RETRY_ON = [
|
|
567
|
-
"lock-frontend",
|
|
568
|
-
"Unable to acquire the dpkg frontend lock",
|
|
569
|
-
"Temporary failure resolving",
|
|
570
|
-
"Could not resolve",
|
|
571
|
-
"Failed to fetch",
|
|
572
|
-
]
|
|
573
|
-
|
|
574
|
-
|
|
575
436
|
def _build_bootstrap_steps(
|
|
576
437
|
user: str,
|
|
577
438
|
password: str,
|
|
578
439
|
ssh_key: str,
|
|
579
440
|
include_portacode_connect: bool = True,
|
|
580
|
-
package_manager: str = "apt",
|
|
581
441
|
) -> List[Dict[str, Any]]:
|
|
582
|
-
|
|
583
|
-
steps: List[Dict[str, Any]] = [
|
|
584
|
-
{"name": "wait_for_network", "cmd": _NETWORK_WAIT_CMD, "retries": 0},
|
|
585
|
-
]
|
|
586
|
-
update_cmd = profile.get("update_cmd")
|
|
587
|
-
if update_cmd:
|
|
588
|
-
steps.append(
|
|
589
|
-
{
|
|
590
|
-
"name": profile.get("update_step_name", "package_update"),
|
|
591
|
-
"cmd": update_cmd,
|
|
592
|
-
"retries": profile.get("update_retries", 3),
|
|
593
|
-
"retry_delay_s": 5,
|
|
594
|
-
"retry_on": _UPDATE_RETRY_ON,
|
|
595
|
-
}
|
|
596
|
-
)
|
|
597
|
-
install_cmd = profile.get("install_cmd")
|
|
598
|
-
if install_cmd:
|
|
599
|
-
steps.append(
|
|
600
|
-
{
|
|
601
|
-
"name": profile.get("install_step_name", "install_deps"),
|
|
602
|
-
"cmd": install_cmd,
|
|
603
|
-
"retries": profile.get("install_retries", 5),
|
|
604
|
-
"retry_delay_s": 5,
|
|
605
|
-
"retry_on": _INSTALL_RETRY_ON,
|
|
606
|
-
}
|
|
607
|
-
)
|
|
608
|
-
steps.extend(
|
|
609
|
-
[
|
|
610
|
-
{
|
|
611
|
-
"name": "user_exists",
|
|
612
|
-
"cmd": (
|
|
613
|
-
f"id -u {user} >/dev/null 2>&1 || "
|
|
614
|
-
f"(if command -v adduser >/dev/null 2>&1 && adduser --disabled-password --help >/dev/null 2>&1; then "
|
|
615
|
-
f" adduser --disabled-password --gecos '' {user}; "
|
|
616
|
-
"else "
|
|
617
|
-
f" useradd -m -s /bin/sh {user}; "
|
|
618
|
-
"fi)"
|
|
619
|
-
),
|
|
620
|
-
"retries": 0,
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
"name": "add_sudo",
|
|
624
|
-
"cmd": (
|
|
625
|
-
f"if command -v usermod >/dev/null 2>&1; then "
|
|
626
|
-
" if ! getent group sudo >/dev/null 2>&1; then "
|
|
627
|
-
" if command -v groupadd >/dev/null 2>&1; then "
|
|
628
|
-
" groupadd sudo >/dev/null 2>&1 || true; "
|
|
629
|
-
" fi; "
|
|
630
|
-
" fi; "
|
|
631
|
-
f" usermod -aG sudo {user}; "
|
|
632
|
-
"else "
|
|
633
|
-
" for grp in wheel sudo; do "
|
|
634
|
-
" if ! getent group \"$grp\" >/dev/null 2>&1 && command -v groupadd >/dev/null 2>&1; then "
|
|
635
|
-
" groupadd \"$grp\" >/dev/null 2>&1 || true; "
|
|
636
|
-
" fi; "
|
|
637
|
-
" addgroup \"$grp\" >/dev/null 2>&1 || true; "
|
|
638
|
-
f" addgroup {user} \"$grp\" >/dev/null 2>&1 || true; "
|
|
639
|
-
" done; "
|
|
640
|
-
"fi"
|
|
641
|
-
),
|
|
642
|
-
"retries": 0,
|
|
643
|
-
},
|
|
442
|
+
steps = [
|
|
644
443
|
{
|
|
645
|
-
"name": "
|
|
646
|
-
"cmd":
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
444
|
+
"name": "apt_update",
|
|
445
|
+
"cmd": "apt-get update -y",
|
|
446
|
+
"retries": 4,
|
|
447
|
+
"retry_delay_s": 5,
|
|
448
|
+
"retry_on": [
|
|
449
|
+
"Temporary failure resolving",
|
|
450
|
+
"Could not resolve",
|
|
451
|
+
"Failed to fetch",
|
|
452
|
+
],
|
|
651
453
|
},
|
|
652
|
-
|
|
653
|
-
|
|
454
|
+
{
|
|
455
|
+
"name": "install_deps",
|
|
456
|
+
"cmd": "apt-get install -y python3 python3-pip sudo --fix-missing",
|
|
457
|
+
"retries": 5,
|
|
458
|
+
"retry_delay_s": 5,
|
|
459
|
+
"retry_on": [
|
|
460
|
+
"lock-frontend",
|
|
461
|
+
"Unable to acquire the dpkg frontend lock",
|
|
462
|
+
"Temporary failure resolving",
|
|
463
|
+
"Could not resolve",
|
|
464
|
+
"Failed to fetch",
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
{"name": "user_exists", "cmd": f"id -u {user} >/dev/null 2>&1 || adduser --disabled-password --gecos '' {user}", "retries": 0},
|
|
468
|
+
{"name": "add_sudo", "cmd": f"usermod -aG sudo {user}", "retries": 0},
|
|
469
|
+
]
|
|
654
470
|
if password:
|
|
655
471
|
steps.append({"name": "set_password", "cmd": f"echo '{user}:{password}' | chpasswd", "retries": 0})
|
|
656
472
|
if ssh_key:
|
|
657
|
-
steps.append(
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
}
|
|
663
|
-
)
|
|
473
|
+
steps.append({
|
|
474
|
+
"name": "add_ssh_key",
|
|
475
|
+
"cmd": f"install -d -m 700 /home/{user}/.ssh && echo '{ssh_key}' >> /home/{user}/.ssh/authorized_keys && chown -R {user}:{user} /home/{user}/.ssh",
|
|
476
|
+
"retries": 0,
|
|
477
|
+
})
|
|
664
478
|
steps.extend(
|
|
665
479
|
[
|
|
666
480
|
{"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
|
|
@@ -672,45 +486,6 @@ def _build_bootstrap_steps(
|
|
|
672
486
|
return steps
|
|
673
487
|
|
|
674
488
|
|
|
675
|
-
def _guess_package_manager_from_template(template: str) -> str:
|
|
676
|
-
normalized = (template or "").lower()
|
|
677
|
-
if "alpine" in normalized:
|
|
678
|
-
return "apk"
|
|
679
|
-
if "archlinux" in normalized:
|
|
680
|
-
return "pacman"
|
|
681
|
-
if "centos-7" in normalized:
|
|
682
|
-
return "yum"
|
|
683
|
-
if any(keyword in normalized for keyword in ("centos-8", "centos-9", "centos-9-stream", "centos-8-stream")):
|
|
684
|
-
return "dnf"
|
|
685
|
-
if any(keyword in normalized for keyword in ("rockylinux", "almalinux", "fedora")):
|
|
686
|
-
return "dnf"
|
|
687
|
-
if "opensuse" in normalized or "suse" in normalized:
|
|
688
|
-
return "zypper"
|
|
689
|
-
if any(keyword in normalized for keyword in ("debian", "ubuntu", "devuan", "turnkeylinux")):
|
|
690
|
-
return "apt"
|
|
691
|
-
if normalized.startswith("system/") and "linux" in normalized:
|
|
692
|
-
return "apt"
|
|
693
|
-
return "apt"
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
def _detect_package_manager(vmid: int) -> str:
|
|
697
|
-
candidates = [
|
|
698
|
-
("apt", "apt-get"),
|
|
699
|
-
("dnf", "dnf"),
|
|
700
|
-
("yum", "yum"),
|
|
701
|
-
("apk", "apk"),
|
|
702
|
-
("pacman", "pacman"),
|
|
703
|
-
("zypper", "zypper"),
|
|
704
|
-
]
|
|
705
|
-
for name, binary in candidates:
|
|
706
|
-
res = _run_pct(vmid, f"command -v {binary} >/dev/null 2>&1")
|
|
707
|
-
if res.get("returncode") == 0:
|
|
708
|
-
logger.debug("Detected package manager %s inside container %s", name, vmid)
|
|
709
|
-
return name
|
|
710
|
-
logger.warning("Unable to detect package manager inside container %s; defaulting to apt", vmid)
|
|
711
|
-
return "apt"
|
|
712
|
-
|
|
713
|
-
|
|
714
489
|
def _get_storage_type(storages: Iterable[Dict[str, Any]], storage_name: str) -> str:
|
|
715
490
|
for entry in storages:
|
|
716
491
|
if entry.get("storage") == storage_name:
|
|
@@ -898,8 +673,7 @@ def _connect_proxmox(config: Dict[str, Any]) -> Any:
|
|
|
898
673
|
|
|
899
674
|
|
|
900
675
|
def _run_pct(vmid: int, cmd: str, input_text: Optional[str] = None) -> Dict[str, Any]:
|
|
901
|
-
|
|
902
|
-
full = ["pct", "exec", str(vmid), "--", shell, "-c", cmd]
|
|
676
|
+
full = ["pct", "exec", str(vmid), "--", "bash", "-lc", cmd]
|
|
903
677
|
start = time.time()
|
|
904
678
|
proc = subprocess.run(full, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=input_text)
|
|
905
679
|
return {
|
|
@@ -911,10 +685,6 @@ def _run_pct(vmid: int, cmd: str, input_text: Optional[str] = None) -> Dict[str,
|
|
|
911
685
|
}
|
|
912
686
|
|
|
913
687
|
|
|
914
|
-
def _su_command(user: str, command: str) -> str:
|
|
915
|
-
return f"su - {user} -s /bin/sh -c {shlex.quote(command)}"
|
|
916
|
-
|
|
917
|
-
|
|
918
688
|
def _run_pct_check(vmid: int, cmd: str) -> Dict[str, Any]:
|
|
919
689
|
res = _run_pct(vmid, cmd)
|
|
920
690
|
if res["returncode"] != 0:
|
|
@@ -974,7 +744,7 @@ def _push_bytes_to_container(
|
|
|
974
744
|
|
|
975
745
|
|
|
976
746
|
def _resolve_portacode_key_dir(vmid: int, user: str) -> str:
|
|
977
|
-
data_dir_cmd =
|
|
747
|
+
data_dir_cmd = f"su - {user} -c 'echo -n ${{XDG_DATA_HOME:-$HOME/.local/share}}'"
|
|
978
748
|
data_home = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
|
|
979
749
|
portacode_dir = f"{data_home}/portacode"
|
|
980
750
|
_run_pct_exec_check(vmid, ["mkdir", "-p", portacode_dir])
|
|
@@ -991,19 +761,18 @@ def _deploy_device_keypair(vmid: int, user: str, private_key: str, public_key: s
|
|
|
991
761
|
|
|
992
762
|
|
|
993
763
|
def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -> Dict[str, Any]:
|
|
994
|
-
|
|
995
|
-
cmd = ["pct", "exec", str(vmid), "--", "/bin/sh", "-c", su_connect_cmd]
|
|
764
|
+
cmd = ["pct", "exec", str(vmid), "--", "bash", "-lc", f"su - {user} -c 'portacode connect'"]
|
|
996
765
|
proc = subprocess.Popen(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
997
766
|
start = time.time()
|
|
998
767
|
|
|
999
|
-
data_dir_cmd =
|
|
768
|
+
data_dir_cmd = f"su - {user} -c 'echo -n ${{XDG_DATA_HOME:-$HOME/.local/share}}'"
|
|
1000
769
|
data_dir = _run_pct_check(vmid, data_dir_cmd)["stdout"].strip()
|
|
1001
770
|
key_dir = f"{data_dir}/portacode/keys"
|
|
1002
771
|
pub_path = f"{key_dir}/id_portacode.pub"
|
|
1003
772
|
priv_path = f"{key_dir}/id_portacode"
|
|
1004
773
|
|
|
1005
774
|
def file_size(path: str) -> Optional[int]:
|
|
1006
|
-
stat_cmd =
|
|
775
|
+
stat_cmd = f"su - {user} -c 'test -s {path} && stat -c %s {path}'"
|
|
1007
776
|
res = _run_pct(vmid, stat_cmd)
|
|
1008
777
|
if res["returncode"] != 0:
|
|
1009
778
|
return None
|
|
@@ -1061,7 +830,7 @@ def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -
|
|
|
1061
830
|
final_pub = file_size(pub_path)
|
|
1062
831
|
final_priv = file_size(priv_path)
|
|
1063
832
|
if final_pub and final_priv:
|
|
1064
|
-
key_res = _run_pct(vmid,
|
|
833
|
+
key_res = _run_pct(vmid, f"su - {user} -c 'cat {pub_path}'")
|
|
1065
834
|
if not process_exited:
|
|
1066
835
|
proc.terminate()
|
|
1067
836
|
try:
|
|
@@ -1101,7 +870,7 @@ def _portacode_connect_and_read_key(vmid: int, user: str, timeout_s: int = 10) -
|
|
|
1101
870
|
except subprocess.TimeoutExpired:
|
|
1102
871
|
proc.kill()
|
|
1103
872
|
|
|
1104
|
-
key_res = _run_pct(vmid,
|
|
873
|
+
key_res = _run_pct(vmid, f"su - {user} -c 'cat {pub_path}'")
|
|
1105
874
|
return {
|
|
1106
875
|
"ok": True,
|
|
1107
876
|
"public_key": key_res["stdout"].strip(),
|
|
@@ -1194,16 +963,7 @@ def _bootstrap_portacode(
|
|
|
1194
963
|
total_steps: Optional[int] = None,
|
|
1195
964
|
default_public_key: Optional[str] = None,
|
|
1196
965
|
) -> Tuple[str, List[Dict[str, Any]]]:
|
|
1197
|
-
if steps is not None
|
|
1198
|
-
actual_steps = steps
|
|
1199
|
-
else:
|
|
1200
|
-
detected_manager = _detect_package_manager(vmid)
|
|
1201
|
-
actual_steps = _build_bootstrap_steps(
|
|
1202
|
-
user,
|
|
1203
|
-
password,
|
|
1204
|
-
ssh_key,
|
|
1205
|
-
package_manager=detected_manager,
|
|
1206
|
-
)
|
|
966
|
+
actual_steps = steps if steps is not None else _build_bootstrap_steps(user, password, ssh_key)
|
|
1207
967
|
results, ok = _run_setup_steps(
|
|
1208
968
|
vmid,
|
|
1209
969
|
actual_steps,
|
|
@@ -1227,15 +987,6 @@ def _bootstrap_portacode(
|
|
|
1227
987
|
else:
|
|
1228
988
|
command_text = str(command)
|
|
1229
989
|
command_suffix = f" command={command_text}" if command_text else ""
|
|
1230
|
-
stdout = details.get("stdout")
|
|
1231
|
-
stderr = details.get("stderr")
|
|
1232
|
-
if stdout or stderr:
|
|
1233
|
-
logger.debug(
|
|
1234
|
-
"Bootstrap command output%s%s%s",
|
|
1235
|
-
f" stdout={stdout!r}" if stdout else "",
|
|
1236
|
-
" " if stdout and stderr else "",
|
|
1237
|
-
f"stderr={stderr!r}" if stderr else "",
|
|
1238
|
-
)
|
|
1239
990
|
if summary:
|
|
1240
991
|
logger.warning(
|
|
1241
992
|
"Portacode bootstrap failure summary=%s%s%s",
|
|
@@ -1243,15 +994,10 @@ def _bootstrap_portacode(
|
|
|
1243
994
|
f" history_len={len(history)}" if history else "",
|
|
1244
995
|
f" command={command_text}" if command_text else "",
|
|
1245
996
|
)
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
f" stderr={stderr!r}" if stderr else "",
|
|
1251
|
-
)
|
|
1252
|
-
raise RuntimeError(
|
|
1253
|
-
f"Portacode bootstrap steps failed: {summary}{history_snippet}{command_suffix}"
|
|
1254
|
-
)
|
|
997
|
+
raise RuntimeError(
|
|
998
|
+
f"Portacode bootstrap steps failed: {summary}{history_snippet}{command_suffix}"
|
|
999
|
+
)
|
|
1000
|
+
raise RuntimeError("Portacode bootstrap steps failed.")
|
|
1255
1001
|
key_step = next((entry for entry in results if entry.get("name") == "portacode_connect"), None)
|
|
1256
1002
|
public_key = key_step.get("public_key") if key_step else default_public_key
|
|
1257
1003
|
if not public_key:
|
|
@@ -1259,6 +1005,12 @@ def _bootstrap_portacode(
|
|
|
1259
1005
|
return public_key, results
|
|
1260
1006
|
|
|
1261
1007
|
|
|
1008
|
+
def _build_cloudflare_snapshot(cloudflare_config: Dict[str, Any] | None) -> Dict[str, Any]:
|
|
1009
|
+
if not cloudflare_config:
|
|
1010
|
+
return {"configured": False}
|
|
1011
|
+
return {"configured": bool(cloudflare_config.get("api_token"))}
|
|
1012
|
+
|
|
1013
|
+
|
|
1262
1014
|
def build_snapshot(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
1263
1015
|
network = config.get("network", {})
|
|
1264
1016
|
base_network = {
|
|
@@ -1266,9 +1018,13 @@ def build_snapshot(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1266
1018
|
"message": network.get("message"),
|
|
1267
1019
|
"bridge": network.get("bridge", DEFAULT_BRIDGE),
|
|
1268
1020
|
}
|
|
1021
|
+
cloudflare_snapshot = _build_cloudflare_snapshot(config.get("cloudflare"))
|
|
1269
1022
|
if not config:
|
|
1270
|
-
return {
|
|
1271
|
-
|
|
1023
|
+
return {
|
|
1024
|
+
"configured": False,
|
|
1025
|
+
"network": base_network,
|
|
1026
|
+
"cloudflare": cloudflare_snapshot,
|
|
1027
|
+
}
|
|
1272
1028
|
return {
|
|
1273
1029
|
"configured": True,
|
|
1274
1030
|
"host": config.get("host"),
|
|
@@ -1279,18 +1035,54 @@ def build_snapshot(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
1279
1035
|
"templates": config.get("templates") or [],
|
|
1280
1036
|
"last_verified": config.get("last_verified"),
|
|
1281
1037
|
"network": base_network,
|
|
1038
|
+
"cloudflare": cloudflare_snapshot,
|
|
1282
1039
|
}
|
|
1283
1040
|
|
|
1284
1041
|
|
|
1285
|
-
def
|
|
1042
|
+
def _resolve_proxmox_credentials(
|
|
1043
|
+
token_identifier: Optional[str],
|
|
1044
|
+
token_value: Optional[str],
|
|
1045
|
+
existing: Dict[str, Any],
|
|
1046
|
+
) -> Tuple[str, str, str]:
|
|
1047
|
+
if token_identifier:
|
|
1048
|
+
if not token_value:
|
|
1049
|
+
raise ValueError("token_value is required when providing a new token_identifier")
|
|
1050
|
+
user, token_name = _parse_token(token_identifier)
|
|
1051
|
+
return user, token_name, token_value
|
|
1052
|
+
if existing and existing.get("user") and existing.get("token_name") and existing.get("token_value"):
|
|
1053
|
+
return existing["user"], existing["token_name"], existing["token_value"]
|
|
1054
|
+
raise ValueError("Proxmox token identifier and value are required when no existing configuration is available")
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _build_cloudflare_config(existing: Dict[str, Any], api_token: Optional[str]) -> Dict[str, Any]:
|
|
1058
|
+
cloudflare: Dict[str, Any] = dict(existing.get("cloudflare", {}) or {})
|
|
1059
|
+
if api_token:
|
|
1060
|
+
cloudflare["api_token"] = api_token
|
|
1061
|
+
if cloudflare.get("api_token"):
|
|
1062
|
+
cloudflare["configured"] = True
|
|
1063
|
+
elif "configured" in cloudflare:
|
|
1064
|
+
cloudflare.pop("configured", None)
|
|
1065
|
+
return cloudflare
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def configure_infrastructure(
|
|
1069
|
+
token_identifier: Optional[str] = None,
|
|
1070
|
+
token_value: Optional[str] = None,
|
|
1071
|
+
verify_ssl: Optional[bool] = None,
|
|
1072
|
+
cloudflare_api_token: Optional[str] = None,
|
|
1073
|
+
) -> Dict[str, Any]:
|
|
1286
1074
|
ProxmoxAPI = _ensure_proxmoxer()
|
|
1287
|
-
|
|
1075
|
+
existing = _load_config()
|
|
1076
|
+
user, token_name, resolved_token_value = _resolve_proxmox_credentials(
|
|
1077
|
+
token_identifier, token_value, existing
|
|
1078
|
+
)
|
|
1079
|
+
actual_verify_ssl = verify_ssl if verify_ssl is not None else existing.get("verify_ssl", False)
|
|
1288
1080
|
client = ProxmoxAPI(
|
|
1289
1081
|
DEFAULT_HOST,
|
|
1290
1082
|
user=user,
|
|
1291
1083
|
token_name=token_name,
|
|
1292
|
-
token_value=
|
|
1293
|
-
verify_ssl=
|
|
1084
|
+
token_value=resolved_token_value,
|
|
1085
|
+
verify_ssl=actual_verify_ssl,
|
|
1294
1086
|
timeout=30,
|
|
1295
1087
|
)
|
|
1296
1088
|
node = _pick_node(client)
|
|
@@ -1298,32 +1090,35 @@ def configure_infrastructure(token_identifier: str, token_value: str, verify_ssl
|
|
|
1298
1090
|
storages = client.nodes(node).storage.get()
|
|
1299
1091
|
default_storage = _pick_storage(storages)
|
|
1300
1092
|
templates = _list_templates(client, node, storages)
|
|
1301
|
-
network
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1093
|
+
network = dict(existing.get("network", {}) or {})
|
|
1094
|
+
if not network.get("applied"):
|
|
1095
|
+
try:
|
|
1096
|
+
network = _ensure_bridge()
|
|
1097
|
+
# Wait for network convergence before validating connectivity
|
|
1098
|
+
time.sleep(2)
|
|
1099
|
+
if not _verify_connectivity():
|
|
1100
|
+
raise RuntimeError("Connectivity check failed; bridge reverted")
|
|
1101
|
+
network["health"] = "healthy"
|
|
1102
|
+
except Exception as exc:
|
|
1103
|
+
logger.warning("Bridge setup failed; reverting previous changes: %s", exc)
|
|
1104
|
+
_revert_bridge()
|
|
1105
|
+
raise
|
|
1313
1106
|
config = {
|
|
1314
1107
|
"host": DEFAULT_HOST,
|
|
1315
1108
|
"node": node,
|
|
1316
1109
|
"user": user,
|
|
1317
1110
|
"token_name": token_name,
|
|
1318
|
-
"token_value":
|
|
1319
|
-
"verify_ssl":
|
|
1111
|
+
"token_value": resolved_token_value,
|
|
1112
|
+
"verify_ssl": actual_verify_ssl,
|
|
1320
1113
|
"default_storage": default_storage,
|
|
1321
|
-
"last_verified": datetime.utcnow().isoformat() + "Z",
|
|
1322
1114
|
"templates": templates,
|
|
1323
|
-
"
|
|
1115
|
+
"last_verified": datetime.utcnow().isoformat() + "Z",
|
|
1324
1116
|
"network": network,
|
|
1325
1117
|
"node_status": status,
|
|
1326
1118
|
}
|
|
1119
|
+
cloudflare = _build_cloudflare_config(existing, cloudflare_api_token)
|
|
1120
|
+
if cloudflare:
|
|
1121
|
+
config["cloudflare"] = cloudflare
|
|
1327
1122
|
_save_config(config)
|
|
1328
1123
|
snapshot = build_snapshot(config)
|
|
1329
1124
|
snapshot["node_status"] = status
|
|
@@ -1374,7 +1169,7 @@ def _instantiate_container(proxmox: Any, node: str, payload: Dict[str, Any]) ->
|
|
|
1374
1169
|
memory=int(payload["ram_mib"]),
|
|
1375
1170
|
swap=int(payload.get("swap_mb", 0)),
|
|
1376
1171
|
cores=max(int(payload.get("cores", 1)), 1),
|
|
1377
|
-
|
|
1172
|
+
cpuunits=int(payload.get("cpuunits", 256)),
|
|
1378
1173
|
net0=payload["net0"],
|
|
1379
1174
|
unprivileged=int(payload.get("unprivileged", 1)),
|
|
1380
1175
|
description=payload.get("description", MANAGED_MARKER),
|
|
@@ -1382,13 +1177,6 @@ def _instantiate_container(proxmox: Any, node: str, payload: Dict[str, Any]) ->
|
|
|
1382
1177
|
ssh_public_keys=payload.get("ssh_public_key") or None,
|
|
1383
1178
|
)
|
|
1384
1179
|
status, elapsed = _wait_for_task(proxmox, node, upid)
|
|
1385
|
-
exitstatus = (status or {}).get("exitstatus")
|
|
1386
|
-
if exitstatus and exitstatus.upper() != "OK":
|
|
1387
|
-
msg = status.get("status") or "unknown error"
|
|
1388
|
-
details = status.get("error") or status.get("errmsg") or status.get("description") or status
|
|
1389
|
-
raise RuntimeError(
|
|
1390
|
-
f"Container creation task failed ({exitstatus}): {msg} details={details}"
|
|
1391
|
-
)
|
|
1392
1180
|
return vmid, elapsed
|
|
1393
1181
|
except ResourceException as exc:
|
|
1394
1182
|
raise RuntimeError(f"Failed to create container: {exc}") from exc
|
|
@@ -1404,24 +1192,16 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1404
1192
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1405
1193
|
logger.info("create_proxmox_container command received")
|
|
1406
1194
|
request_id = message.get("request_id")
|
|
1407
|
-
|
|
1408
|
-
device_id = str(raw_device_id or "").strip()
|
|
1409
|
-
if not device_id:
|
|
1410
|
-
raise ValueError("device_id is required to create a container")
|
|
1195
|
+
device_id = message.get("device_id")
|
|
1411
1196
|
device_public_key = (message.get("device_public_key") or "").strip()
|
|
1412
1197
|
device_private_key = (message.get("device_private_key") or "").strip()
|
|
1413
1198
|
has_device_keypair = bool(device_public_key and device_private_key)
|
|
1414
|
-
config_guess = _load_config()
|
|
1415
|
-
template_candidates = config_guess.get("templates") or []
|
|
1416
|
-
template_hint = (message.get("template") or (template_candidates[0] if template_candidates else "")).strip()
|
|
1417
|
-
package_manager = _guess_package_manager_from_template(template_hint)
|
|
1418
1199
|
bootstrap_user, bootstrap_password, bootstrap_ssh_key = _get_provisioning_user_info(message)
|
|
1419
1200
|
bootstrap_steps = _build_bootstrap_steps(
|
|
1420
1201
|
bootstrap_user,
|
|
1421
1202
|
bootstrap_password,
|
|
1422
1203
|
bootstrap_ssh_key,
|
|
1423
1204
|
include_portacode_connect=not has_device_keypair,
|
|
1424
|
-
package_manager=package_manager,
|
|
1425
1205
|
)
|
|
1426
1206
|
total_steps = 3 + len(bootstrap_steps) + 2
|
|
1427
1207
|
current_step_index = 1
|
|
@@ -1444,7 +1224,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1444
1224
|
message=start_message,
|
|
1445
1225
|
phase="lifecycle",
|
|
1446
1226
|
request_id=request_id,
|
|
1447
|
-
on_behalf_of_device=device_id,
|
|
1448
1227
|
)
|
|
1449
1228
|
try:
|
|
1450
1229
|
result = action()
|
|
@@ -1460,7 +1239,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1460
1239
|
phase="lifecycle",
|
|
1461
1240
|
request_id=request_id,
|
|
1462
1241
|
details={"error": str(exc)},
|
|
1463
|
-
on_behalf_of_device=device_id,
|
|
1464
1242
|
)
|
|
1465
1243
|
raise
|
|
1466
1244
|
_emit_progress_event(
|
|
@@ -1473,7 +1251,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1473
1251
|
message=success_message,
|
|
1474
1252
|
phase="lifecycle",
|
|
1475
1253
|
request_id=request_id,
|
|
1476
|
-
on_behalf_of_device=device_id,
|
|
1477
1254
|
)
|
|
1478
1255
|
current_step_index += 1
|
|
1479
1256
|
return result
|
|
@@ -1500,8 +1277,7 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1500
1277
|
proxmox = _connect_proxmox(config)
|
|
1501
1278
|
node = config.get("node") or DEFAULT_NODE_NAME
|
|
1502
1279
|
payload = _build_container_payload(message, config)
|
|
1503
|
-
payload["
|
|
1504
|
-
payload["cores"] = int(max(math.ceil(payload["cpus"]), 1))
|
|
1280
|
+
payload["cpuunits"] = max(int(payload["cpus"] * 1024), 10)
|
|
1505
1281
|
payload["memory"] = int(payload["ram_mib"])
|
|
1506
1282
|
payload["node"] = node
|
|
1507
1283
|
logger.debug(
|
|
@@ -1516,7 +1292,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1516
1292
|
payload["vmid"] = vmid
|
|
1517
1293
|
payload["created_at"] = datetime.utcnow().isoformat() + "Z"
|
|
1518
1294
|
payload["status"] = "creating"
|
|
1519
|
-
payload["device_id"] = device_id
|
|
1520
1295
|
_write_container_record(vmid, payload)
|
|
1521
1296
|
return proxmox, node, vmid, payload
|
|
1522
1297
|
|
|
@@ -1577,7 +1352,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1577
1352
|
phase="bootstrap",
|
|
1578
1353
|
request_id=request_id,
|
|
1579
1354
|
details=details or None,
|
|
1580
|
-
on_behalf_of_device=device_id,
|
|
1581
1355
|
)
|
|
1582
1356
|
|
|
1583
1357
|
public_key, steps = _bootstrap_portacode(
|
|
@@ -1621,7 +1395,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1621
1395
|
message="Notifying the server of the new device…",
|
|
1622
1396
|
phase="service",
|
|
1623
1397
|
request_id=request_id,
|
|
1624
|
-
on_behalf_of_device=device_id,
|
|
1625
1398
|
)
|
|
1626
1399
|
_emit_progress_event(
|
|
1627
1400
|
self,
|
|
@@ -1633,7 +1406,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1633
1406
|
message="Authentication metadata recorded.",
|
|
1634
1407
|
phase="service",
|
|
1635
1408
|
request_id=request_id,
|
|
1636
|
-
on_behalf_of_device=device_id,
|
|
1637
1409
|
)
|
|
1638
1410
|
|
|
1639
1411
|
install_step = service_start_index + 1
|
|
@@ -1648,10 +1420,9 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1648
1420
|
message="Running sudo portacode service install…",
|
|
1649
1421
|
phase="service",
|
|
1650
1422
|
request_id=request_id,
|
|
1651
|
-
on_behalf_of_device=device_id,
|
|
1652
1423
|
)
|
|
1653
1424
|
|
|
1654
|
-
cmd =
|
|
1425
|
+
cmd = f"su - {payload['username']} -c 'sudo -S portacode service install'"
|
|
1655
1426
|
res = _run_pct(vmid, cmd, input_text=payload["password"] + "\n")
|
|
1656
1427
|
|
|
1657
1428
|
if res["returncode"] != 0:
|
|
@@ -1669,7 +1440,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1669
1440
|
"stderr": res.get("stderr"),
|
|
1670
1441
|
"stdout": res.get("stdout"),
|
|
1671
1442
|
},
|
|
1672
|
-
on_behalf_of_device=device_id,
|
|
1673
1443
|
)
|
|
1674
1444
|
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
1675
1445
|
|
|
@@ -1683,7 +1453,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1683
1453
|
message="Portacode service install finished.",
|
|
1684
1454
|
phase="service",
|
|
1685
1455
|
request_id=request_id,
|
|
1686
|
-
on_behalf_of_device=device_id,
|
|
1687
1456
|
)
|
|
1688
1457
|
|
|
1689
1458
|
logger.info("create_proxmox_container: portacode service install completed inside ct %s", vmid)
|
|
@@ -1707,7 +1476,6 @@ class CreateProxmoxContainerHandler(SyncHandler):
|
|
|
1707
1476
|
},
|
|
1708
1477
|
"setup_steps": steps,
|
|
1709
1478
|
"device_id": device_id,
|
|
1710
|
-
"on_behalf_of_device": device_id,
|
|
1711
1479
|
"service_installed": service_installed,
|
|
1712
1480
|
}
|
|
1713
1481
|
|
|
@@ -1733,9 +1501,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1733
1501
|
password = record.get("password")
|
|
1734
1502
|
if not user or not password:
|
|
1735
1503
|
raise RuntimeError("Container credentials unavailable")
|
|
1736
|
-
on_behalf_of_device = record.get("device_id")
|
|
1737
|
-
if on_behalf_of_device:
|
|
1738
|
-
on_behalf_of_device = str(on_behalf_of_device)
|
|
1739
1504
|
|
|
1740
1505
|
start_index = int(message.get("step_index", 1))
|
|
1741
1506
|
total_steps = int(message.get("total_steps", start_index + 2))
|
|
@@ -1753,7 +1518,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1753
1518
|
message="Notifying the server of the new device…",
|
|
1754
1519
|
phase="service",
|
|
1755
1520
|
request_id=request_id,
|
|
1756
|
-
on_behalf_of_device=on_behalf_of_device,
|
|
1757
1521
|
)
|
|
1758
1522
|
_emit_progress_event(
|
|
1759
1523
|
self,
|
|
@@ -1765,7 +1529,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1765
1529
|
message="Authentication metadata recorded.",
|
|
1766
1530
|
phase="service",
|
|
1767
1531
|
request_id=request_id,
|
|
1768
|
-
on_behalf_of_device=on_behalf_of_device,
|
|
1769
1532
|
)
|
|
1770
1533
|
|
|
1771
1534
|
install_step = start_index + 1
|
|
@@ -1780,10 +1543,9 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1780
1543
|
message="Running sudo portacode service install…",
|
|
1781
1544
|
phase="service",
|
|
1782
1545
|
request_id=request_id,
|
|
1783
|
-
on_behalf_of_device=on_behalf_of_device,
|
|
1784
1546
|
)
|
|
1785
1547
|
|
|
1786
|
-
cmd =
|
|
1548
|
+
cmd = f"su - {user} -c 'sudo -S portacode service install'"
|
|
1787
1549
|
res = _run_pct(vmid, cmd, input_text=password + "\n")
|
|
1788
1550
|
|
|
1789
1551
|
if res["returncode"] != 0:
|
|
@@ -1801,7 +1563,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1801
1563
|
"stderr": res.get("stderr"),
|
|
1802
1564
|
"stdout": res.get("stdout"),
|
|
1803
1565
|
},
|
|
1804
|
-
on_behalf_of_device=on_behalf_of_device,
|
|
1805
1566
|
)
|
|
1806
1567
|
raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
|
|
1807
1568
|
|
|
@@ -1815,7 +1576,6 @@ class StartPortacodeServiceHandler(SyncHandler):
|
|
|
1815
1576
|
message="Portacode service install finished.",
|
|
1816
1577
|
phase="service",
|
|
1817
1578
|
request_id=request_id,
|
|
1818
|
-
on_behalf_of_device=on_behalf_of_device,
|
|
1819
1579
|
)
|
|
1820
1580
|
|
|
1821
1581
|
return {
|
|
@@ -1934,10 +1694,13 @@ class ConfigureProxmoxInfraHandler(SyncHandler):
|
|
|
1934
1694
|
def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
|
1935
1695
|
token_identifier = message.get("token_identifier")
|
|
1936
1696
|
token_value = message.get("token_value")
|
|
1937
|
-
verify_ssl =
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1697
|
+
verify_ssl = message.get("verify_ssl")
|
|
1698
|
+
snapshot = configure_infrastructure(
|
|
1699
|
+
token_identifier=token_identifier,
|
|
1700
|
+
token_value=token_value,
|
|
1701
|
+
verify_ssl=verify_ssl,
|
|
1702
|
+
cloudflare_api_token=message.get("cloudflare_api_token"),
|
|
1703
|
+
)
|
|
1941
1704
|
return {
|
|
1942
1705
|
"event": "proxmox_infra_configured",
|
|
1943
1706
|
"success": True,
|
portacode/connection/terminal.py
CHANGED
|
@@ -685,7 +685,6 @@ class TerminalManager:
|
|
|
685
685
|
try:
|
|
686
686
|
# Initialize project state
|
|
687
687
|
project_state = await manager.initialize_project_state(session_name, project_folder_path)
|
|
688
|
-
await self._restore_tabs_from_session_metadata(manager, project_state, session)
|
|
689
688
|
|
|
690
689
|
# Send initial project state to the client
|
|
691
690
|
initial_state_payload = {
|
|
@@ -711,85 +710,6 @@ class TerminalManager:
|
|
|
711
710
|
|
|
712
711
|
except Exception as exc:
|
|
713
712
|
logger.exception("terminal_manager: Error initializing project states for new sessions: %s", exc)
|
|
714
|
-
|
|
715
|
-
async def _restore_tabs_from_session_metadata(self, manager, project_state, session):
|
|
716
|
-
"""Restore open tabs/active tab from client session metadata if available."""
|
|
717
|
-
if not session or not project_state:
|
|
718
|
-
return
|
|
719
|
-
|
|
720
|
-
descriptors = session.get("open_tabs") or []
|
|
721
|
-
if not descriptors:
|
|
722
|
-
return
|
|
723
|
-
|
|
724
|
-
session_id = project_state.client_session_id
|
|
725
|
-
logger.info("terminal_manager: 🧭 Restoring %d tabs from metadata for session %s", len(descriptors), session_id)
|
|
726
|
-
|
|
727
|
-
for descriptor in descriptors:
|
|
728
|
-
parsed = self._parse_tab_descriptor(descriptor)
|
|
729
|
-
if not parsed:
|
|
730
|
-
continue
|
|
731
|
-
|
|
732
|
-
tab_type = parsed.get("tab_type")
|
|
733
|
-
file_path = parsed.get("file_path")
|
|
734
|
-
metadata = parsed.get("metadata", {})
|
|
735
|
-
|
|
736
|
-
if tab_type == "file" and file_path:
|
|
737
|
-
try:
|
|
738
|
-
await manager.open_file(session_id, file_path, set_active=False)
|
|
739
|
-
except Exception as exc:
|
|
740
|
-
logger.warning("terminal_manager: Failed to restore file tab %s for session %s: %s", file_path, session_id, exc)
|
|
741
|
-
continue
|
|
742
|
-
|
|
743
|
-
if tab_type == "diff" and file_path:
|
|
744
|
-
from_ref = metadata.get("from") or metadata.get("from_ref")
|
|
745
|
-
to_ref = metadata.get("to") or metadata.get("to_ref")
|
|
746
|
-
if not from_ref or not to_ref:
|
|
747
|
-
logger.warning("terminal_manager: Skipping diff tab %s for session %s because from/to references are missing", file_path, session_id)
|
|
748
|
-
continue
|
|
749
|
-
from_hash = metadata.get("from_hash") or metadata.get("fromHash")
|
|
750
|
-
to_hash = metadata.get("to_hash") or metadata.get("toHash")
|
|
751
|
-
try:
|
|
752
|
-
await manager.open_diff_tab(session_id, file_path, from_ref, to_ref, from_hash=from_hash, to_hash=to_hash)
|
|
753
|
-
except Exception as exc:
|
|
754
|
-
logger.warning("terminal_manager: Failed to restore diff tab %s for session %s: %s", file_path, session_id, exc)
|
|
755
|
-
continue
|
|
756
|
-
|
|
757
|
-
logger.debug("terminal_manager: Unknown tab descriptor ignored for session %s: %s", session_id, descriptor)
|
|
758
|
-
|
|
759
|
-
active_index = session.get("active_tab")
|
|
760
|
-
try:
|
|
761
|
-
active_index_int = int(active_index) if active_index is not None else None
|
|
762
|
-
except (TypeError, ValueError):
|
|
763
|
-
active_index_int = None
|
|
764
|
-
|
|
765
|
-
if active_index_int is not None and active_index_int >= 0:
|
|
766
|
-
current_tabs = list(project_state.open_tabs.values())
|
|
767
|
-
if 0 <= active_index_int < len(current_tabs):
|
|
768
|
-
try:
|
|
769
|
-
await manager.set_active_tab(session_id, current_tabs[active_index_int].tab_id)
|
|
770
|
-
except Exception as exc:
|
|
771
|
-
logger.warning("terminal_manager: Failed to set active tab for session %s: %s", session_id, exc)
|
|
772
|
-
else:
|
|
773
|
-
logger.debug("terminal_manager: Active tab index %s out of range for session %s", active_index_int, session_id)
|
|
774
|
-
|
|
775
|
-
def _parse_tab_descriptor(self, descriptor: str) -> Optional[Dict[str, Any]]:
|
|
776
|
-
"""Parse a URL-friendly tab descriptor string."""
|
|
777
|
-
if not descriptor:
|
|
778
|
-
return None
|
|
779
|
-
|
|
780
|
-
try:
|
|
781
|
-
parts = descriptor.split("|")
|
|
782
|
-
tab_type = parts[0] if parts else None
|
|
783
|
-
file_path = parts[1] if len(parts) > 1 else None
|
|
784
|
-
metadata = {}
|
|
785
|
-
for part in parts[2:]:
|
|
786
|
-
if "=" in part:
|
|
787
|
-
key, value = part.split("=", 1)
|
|
788
|
-
metadata[key] = value
|
|
789
|
-
return {"tab_type": tab_type, "file_path": file_path, "metadata": metadata}
|
|
790
|
-
except Exception as exc:
|
|
791
|
-
logger.warning("terminal_manager: Failed to parse tab descriptor '%s': %s", descriptor, exc)
|
|
792
|
-
return None
|
|
793
713
|
|
|
794
714
|
async def _send_targeted_terminal_list(self, message: Dict[str, Any], target_sessions: List[str]) -> None:
|
|
795
715
|
"""Send terminal_list command to specific client sessions.
|
|
@@ -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=ZC_kn1NEQ9Aet-DPiqGe5ZTNbDk0ZOpeb6OhHRUx77M,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,9 +12,9 @@ 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=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=bW3coRPyolXX8GIFQXfOyonxg2ad7UOfmMenN23_5FQ,99509
|
|
18
18
|
portacode/connection/handlers/__init__.py,sha256=WSeBmi65GWFQPYt9M3E10rn0uZ_EPCJzNJOzSf2HZyw,2921
|
|
19
19
|
portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
|
|
20
20
|
portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
|
|
@@ -22,7 +22,7 @@ portacode/connection/handlers/diff_handlers.py,sha256=iYTIRCcpEQ03vIPKZCsMTE5aZb
|
|
|
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=K9s0e9lb97I9_kNmnpoJMpoO1uTrl2ef9KTUwIbiF-g,63287
|
|
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=fr12QpOr_Z8KYGUU-AYrTQwRPAcrLK85hvj3SEq1Kw8,14757
|
|
@@ -35,7 +35,7 @@ portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL
|
|
|
35
35
|
portacode/connection/handlers/project_state/file_system_watcher.py,sha256=r9_UKxWTbzum0jGqxIafe68Ced2Y3xOp3ZmkpBOfRpw,8573
|
|
36
36
|
portacode/connection/handlers/project_state/git_manager.py,sha256=iGQ7LYIA7uHsZHdj3HEc_LYo7S1Lqv6-AeyyMwknBPo,70027
|
|
37
37
|
portacode/connection/handlers/project_state/handlers.py,sha256=qgOSt26rxAGNxW07AoevTwDPBdxblX4J-dX-EjOKtg4,38232
|
|
38
|
-
portacode/connection/handlers/project_state/manager.py,sha256=
|
|
38
|
+
portacode/connection/handlers/project_state/manager.py,sha256=pRMZqPOTK9YE3abNxiAbnERIJmRys673HFOEIBiKnm4,67184
|
|
39
39
|
portacode/connection/handlers/project_state/models.py,sha256=EZTKvxHKs8QlQUbzI0u2IqfzfRRXZixUIDBwTGCJATI,4313
|
|
40
40
|
portacode/connection/handlers/project_state/utils.py,sha256=LsbQr9TH9Bz30FqikmtTxco4PlB_n0kUIuPKQ6Fb_mo,1665
|
|
41
41
|
portacode/link_capture/__init__.py,sha256=93LjyYDqzOimsIDBhsPibTl7tr-8DiIzyDF7JWQkE2A,1231
|
|
@@ -65,7 +65,7 @@ portacode/utils/__init__.py,sha256=NgBlWTuNJESfIYJzP_3adI1yJQJR0XJLRpSdVNaBAN0,3
|
|
|
65
65
|
portacode/utils/diff_apply.py,sha256=4Oi7ft3VUCKmiUE4VM-OeqO7Gk6H7PF3WnN4WHXtjxI,15157
|
|
66
66
|
portacode/utils/diff_renderer.py,sha256=S76StnQ2DLfsz4Gg0m07UwPfRp8270PuzbNaQq-rmYk,13850
|
|
67
67
|
portacode/utils/ntp_clock.py,sha256=VqCnWCTehCufE43W23oB-WUdAZGeCcLxkmIOPwInYHc,2499
|
|
68
|
-
portacode-1.4.15.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
68
|
+
portacode-1.4.15.dev0.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
|
|
69
69
|
test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
|
|
70
70
|
test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
|
|
71
71
|
test_modules/test_device_online.py,sha256=QtYq0Dq9vME8Gp2O4fGSheqVf8LUtpsSKosXXk56gGM,1654
|
|
@@ -91,8 +91,8 @@ testing_framework/core/playwright_manager.py,sha256=Tw46qwxIhOFkS48C2IWIQHHNpEe-
|
|
|
91
91
|
testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
|
|
92
92
|
testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
|
|
93
93
|
testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
|
|
94
|
-
portacode-1.4.15.dist-info/METADATA,sha256=
|
|
95
|
-
portacode-1.4.15.dist-info/WHEEL,sha256=
|
|
96
|
-
portacode-1.4.15.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
-
portacode-1.4.15.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
-
portacode-1.4.15.dist-info/RECORD,,
|
|
94
|
+
portacode-1.4.15.dev0.dist-info/METADATA,sha256=R7FEzw07kw5YY1i3Sj0-W88K5pOVHVwMYGPE1kVDduA,13051
|
|
95
|
+
portacode-1.4.15.dev0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
96
|
+
portacode-1.4.15.dev0.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.15.dev0.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.15.dev0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|