portacode 1.4.13.dev0__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 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.dev0'
32
- __version_tuple__ = version_tuple = (1, 4, 13, 'dev0')
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
@@ -361,6 +361,9 @@ Creates a Portacode-managed LXC container, starts it, and bootstraps the Portaco
361
361
  * `username` (string, optional): OS user to provision (defaults to `svcuser`).
362
362
  * `password` (string, optional): Password for the user (used only during provisioning).
363
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.
364
367
 
365
368
  **Responses:**
366
369
 
@@ -418,6 +421,8 @@ Emitted after a successful `create_proxmox_container` action. Contains the new c
418
421
  * `public_key` (string): Portacode public auth key created inside the new container.
419
422
  * `container` (object): Metadata such as `vmid`, `hostname`, `template`, `storage`, `disk_gib`, `ram_mib`, and `cpus`.
420
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`.
421
426
 
422
427
  ### `proxmox_container_progress`
423
428
 
@@ -450,6 +455,7 @@ Runs `sudo portacode service install` inside the container after the dashboard h
450
455
  * Emits additional [`proxmox_container_progress`](#proxmox_container_progress-event) events to report the authentication and service-install steps.
451
456
  * On success, emits a [`proxmox_service_started`](#proxmox_service_started-event).
452
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.
453
459
 
454
460
  ### `proxmox_service_started`
455
461
 
@@ -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(user: str, password: str, ssh_key: str) -> List[Dict[str, Any]]:
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
- {"name": "pip_upgrade", "cmd": "python3 -m pip install --upgrade pip", "retries": 0},
473
- {"name": "install_portacode", "cmd": "python3 -m pip install --upgrade portacode", "retries": 0},
474
- {"name": "portacode_connect", "type": "portacode_connect", "timeout_s": 30},
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 None
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
- source_client_session = message.get("source_client_session")
1058
- logger.info(
1059
- "create_proxmox_container command received request_id=%s client_session=%s",
1060
- request_id,
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(bootstrap_user, bootstrap_password, bootstrap_ssh_key)
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.info(
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
- response = {
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,10 +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
- return response
1265
1380
 
1266
1381
 
1267
1382
  class StartPortacodeServiceHandler(SyncHandler):
@@ -1289,15 +1404,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1289
1404
  start_index = int(message.get("step_index", 1))
1290
1405
  total_steps = int(message.get("total_steps", start_index + 2))
1291
1406
  request_id = message.get("request_id")
1292
- source_client_session = message.get("source_client_session")
1293
- client_sessions = [source_client_session] if source_client_session else None
1294
-
1295
- logger.info(
1296
- "start_portacode_service invoked vmid=%s user=%s client_session=%s",
1297
- vmid,
1298
- user,
1299
- source_client_session,
1300
- )
1301
1407
 
1302
1408
  auth_step_name = "setup_device_authentication"
1303
1409
  auth_label = "Setting up device authentication"
@@ -1311,7 +1417,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1311
1417
  message="Notifying the server of the new device…",
1312
1418
  phase="service",
1313
1419
  request_id=request_id,
1314
- client_sessions=client_sessions,
1315
1420
  )
1316
1421
  _emit_progress_event(
1317
1422
  self,
@@ -1323,7 +1428,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1323
1428
  message="Authentication metadata recorded.",
1324
1429
  phase="service",
1325
1430
  request_id=request_id,
1326
- client_sessions=client_sessions,
1327
1431
  )
1328
1432
 
1329
1433
  install_step = start_index + 1
@@ -1338,7 +1442,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1338
1442
  message="Running sudo portacode service install…",
1339
1443
  phase="service",
1340
1444
  request_id=request_id,
1341
- client_sessions=client_sessions,
1342
1445
  )
1343
1446
 
1344
1447
  cmd = f"su - {user} -c 'sudo -S portacode service install'"
@@ -1355,7 +1458,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1355
1458
  message=f"{install_label} failed: {res.get('stderr') or res.get('stdout')}",
1356
1459
  phase="service",
1357
1460
  request_id=request_id,
1358
- client_sessions=client_sessions,
1359
1461
  details={
1360
1462
  "stderr": res.get("stderr"),
1361
1463
  "stdout": res.get("stdout"),
@@ -1363,12 +1465,6 @@ class StartPortacodeServiceHandler(SyncHandler):
1363
1465
  )
1364
1466
  raise RuntimeError(res.get("stderr") or res.get("stdout") or "Service install failed")
1365
1467
 
1366
- logger.info(
1367
- "portacode service install command completed vmid=%s returncode=%s",
1368
- vmid,
1369
- res["returncode"],
1370
- )
1371
-
1372
1468
  _emit_progress_event(
1373
1469
  self,
1374
1470
  step_index=install_step,
@@ -1379,18 +1475,14 @@ class StartPortacodeServiceHandler(SyncHandler):
1379
1475
  message="Portacode service install finished.",
1380
1476
  phase="service",
1381
1477
  request_id=request_id,
1382
- client_sessions=client_sessions,
1383
1478
  )
1384
1479
 
1385
- response = {
1480
+ return {
1386
1481
  "event": "proxmox_service_started",
1387
1482
  "success": True,
1388
1483
  "message": "Portacode service install completed",
1389
1484
  "ctid": str(vmid),
1390
1485
  }
1391
- if client_sessions:
1392
- response["client_sessions"] = client_sessions
1393
- return response
1394
1486
 
1395
1487
 
1396
1488
  class StartProxmoxContainerHandler(SyncHandler):
@@ -1406,20 +1498,12 @@ class StartProxmoxContainerHandler(SyncHandler):
1406
1498
  proxmox = _connect_proxmox(config)
1407
1499
  node = _get_node_from_config(config)
1408
1500
  _ensure_container_managed(proxmox, node, vmid)
1409
- source_client_session = message.get("source_client_session")
1410
- client_sessions = [source_client_session] if source_client_session else None
1411
-
1412
- logger.info(
1413
- "start_proxmox_container invoked vmid=%s client_session=%s",
1414
- vmid,
1415
- source_client_session,
1416
- )
1417
1501
 
1418
1502
  status, elapsed = _start_container(proxmox, node, vmid)
1419
1503
  _update_container_record(vmid, {"status": "running"})
1420
1504
 
1421
1505
  infra = get_infra_snapshot()
1422
- response = {
1506
+ return {
1423
1507
  "event": "proxmox_container_action",
1424
1508
  "action": "start",
1425
1509
  "success": True,
@@ -1429,9 +1513,6 @@ class StartProxmoxContainerHandler(SyncHandler):
1429
1513
  "status": status.get("status"),
1430
1514
  "infra": infra,
1431
1515
  }
1432
- if client_sessions:
1433
- response["client_sessions"] = client_sessions
1434
- return response
1435
1516
 
1436
1517
 
1437
1518
  class StopProxmoxContainerHandler(SyncHandler):
@@ -1447,14 +1528,6 @@ class StopProxmoxContainerHandler(SyncHandler):
1447
1528
  proxmox = _connect_proxmox(config)
1448
1529
  node = _get_node_from_config(config)
1449
1530
  _ensure_container_managed(proxmox, node, vmid)
1450
- source_client_session = message.get("source_client_session")
1451
- client_sessions = [source_client_session] if source_client_session else None
1452
-
1453
- logger.info(
1454
- "stop_proxmox_container invoked vmid=%s client_session=%s",
1455
- vmid,
1456
- source_client_session,
1457
- )
1458
1531
 
1459
1532
  status, elapsed = _stop_container(proxmox, node, vmid)
1460
1533
  final_status = status.get("status") or "stopped"
@@ -1466,7 +1539,7 @@ class StopProxmoxContainerHandler(SyncHandler):
1466
1539
  if final_status != "running" and elapsed == 0.0
1467
1540
  else f"Stopped container {vmid} in {elapsed:.1f}s."
1468
1541
  )
1469
- response = {
1542
+ return {
1470
1543
  "event": "proxmox_container_action",
1471
1544
  "action": "stop",
1472
1545
  "success": True,
@@ -1476,9 +1549,6 @@ class StopProxmoxContainerHandler(SyncHandler):
1476
1549
  "status": final_status,
1477
1550
  "infra": infra,
1478
1551
  }
1479
- if client_sessions:
1480
- response["client_sessions"] = client_sessions
1481
- return response
1482
1552
 
1483
1553
 
1484
1554
  class RemoveProxmoxContainerHandler(SyncHandler):
@@ -1494,21 +1564,13 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1494
1564
  proxmox = _connect_proxmox(config)
1495
1565
  node = _get_node_from_config(config)
1496
1566
  _ensure_container_managed(proxmox, node, vmid)
1497
- source_client_session = message.get("source_client_session")
1498
- client_sessions = [source_client_session] if source_client_session else None
1499
-
1500
- logger.info(
1501
- "remove_proxmox_container invoked vmid=%s client_session=%s",
1502
- vmid,
1503
- source_client_session,
1504
- )
1505
1567
 
1506
1568
  stop_status, stop_elapsed = _stop_container(proxmox, node, vmid)
1507
1569
  delete_status, delete_elapsed = _delete_container(proxmox, node, vmid)
1508
1570
  _remove_container_record(vmid)
1509
1571
 
1510
1572
  infra = get_infra_snapshot()
1511
- response = {
1573
+ return {
1512
1574
  "event": "proxmox_container_action",
1513
1575
  "action": "remove",
1514
1576
  "success": True,
@@ -1521,9 +1583,6 @@ class RemoveProxmoxContainerHandler(SyncHandler):
1521
1583
  "status": "deleted",
1522
1584
  "infra": infra,
1523
1585
  }
1524
- if client_sessions:
1525
- response["client_sessions"] = client_sessions
1526
- return response
1527
1586
 
1528
1587
 
1529
1588
  class ConfigureProxmoxInfraHandler(SyncHandler):
@@ -1537,18 +1596,13 @@ class ConfigureProxmoxInfraHandler(SyncHandler):
1537
1596
  verify_ssl = bool(message.get("verify_ssl"))
1538
1597
  if not token_identifier or not token_value:
1539
1598
  raise ValueError("token_identifier and token_value are required")
1540
- source_client_session = message.get("source_client_session")
1541
- client_sessions = [source_client_session] if source_client_session else None
1542
1599
  snapshot = configure_infrastructure(token_identifier, token_value, verify_ssl=verify_ssl)
1543
- response = {
1600
+ return {
1544
1601
  "event": "proxmox_infra_configured",
1545
1602
  "success": True,
1546
1603
  "message": "Proxmox infrastructure configured",
1547
1604
  "infra": snapshot,
1548
1605
  }
1549
- if client_sessions:
1550
- response["client_sessions"] = client_sessions
1551
- return response
1552
1606
 
1553
1607
 
1554
1608
  class RevertProxmoxInfraHandler(SyncHandler):
@@ -1557,15 +1611,10 @@ class RevertProxmoxInfraHandler(SyncHandler):
1557
1611
  return "revert_proxmox_infra"
1558
1612
 
1559
1613
  def execute(self, message: Dict[str, Any]) -> Dict[str, Any]:
1560
- source_client_session = message.get("source_client_session")
1561
- client_sessions = [source_client_session] if source_client_session else None
1562
1614
  snapshot = revert_infrastructure()
1563
- response = {
1615
+ return {
1564
1616
  "event": "proxmox_infra_reverted",
1565
1617
  "success": True,
1566
1618
  "message": "Proxmox infrastructure configuration reverted",
1567
1619
  "infra": snapshot,
1568
1620
  }
1569
- if client_sessions:
1570
- response["client_sessions"] = client_sessions
1571
- 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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 1.4.13.dev0
3
+ Version: 1.4.13.dev2
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -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=EoCeFU0-D6AjtQLTsyubZ8HHL9Z71w1CLhD054hnlNo,719
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,7 +14,7 @@ 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=7tBYNEY8EBGAPIMT606BqeHnyMOQIZVlQYpH7me26LY,97962
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
19
  portacode/connection/handlers/base.py,sha256=oENFb-Fcfzwk99Qx8gJQriEMiwSxwygwjOiuCH36hM4,10231
20
20
  portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
@@ -22,12 +22,13 @@ 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=QxD5FOQqRLz1Z8nhycdm2O991TWIXPjSC589xASZADQ,57264
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.dev0.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
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.dev0.dist-info/METADATA,sha256=NuHta1wZHQRrXvVF3tcjW6ybf3wz3ivf3ktRZrYrn3c,13051
94
- portacode-1.4.13.dev0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
95
- portacode-1.4.13.dev0.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
96
- portacode-1.4.13.dev0.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
97
- portacode-1.4.13.dev0.dist-info/RECORD,,
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,,