portacode 1.4.16.dev9__py3-none-any.whl → 1.4.16.dev11__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/proxmox_infra.py +285 -381
- {portacode-1.4.16.dev9.dist-info → portacode-1.4.16.dev11.dist-info}/METADATA +1 -1
- {portacode-1.4.16.dev9.dist-info → portacode-1.4.16.dev11.dist-info}/RECORD +8 -8
- {portacode-1.4.16.dev9.dist-info → portacode-1.4.16.dev11.dist-info}/WHEEL +0 -0
- {portacode-1.4.16.dev9.dist-info → portacode-1.4.16.dev11.dist-info}/entry_points.txt +0 -0
- {portacode-1.4.16.dev9.dist-info → portacode-1.4.16.dev11.dist-info}/licenses/LICENSE +0 -0
- {portacode-1.4.16.dev9.dist-info → portacode-1.4.16.dev11.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.16.
|
|
32
|
-
__version_tuple__ = version_tuple = (1, 4, 16, '
|
|
31
|
+
__version__ = version = '1.4.16.dev11'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 4, 16, 'dev11')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -19,7 +19,7 @@ import time
|
|
|
19
19
|
import threading
|
|
20
20
|
from datetime import datetime, timezone
|
|
21
21
|
from pathlib import Path
|
|
22
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence,
|
|
22
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
|
|
23
23
|
|
|
24
24
|
import platformdirs
|
|
25
25
|
|
|
@@ -202,72 +202,65 @@ def _current_time_iso() -> str:
|
|
|
202
202
|
return datetime.now(timezone.utc).isoformat()
|
|
203
203
|
|
|
204
204
|
|
|
205
|
-
def
|
|
206
|
-
|
|
207
|
-
return
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
def _to_float(value: Any, default: float = 0.0) -> float:
|
|
205
|
+
def _parse_iso_timestamp(value: str) -> Optional[datetime]:
|
|
206
|
+
if not value:
|
|
207
|
+
return None
|
|
208
|
+
text = value
|
|
209
|
+
if text.endswith("Z"):
|
|
210
|
+
text = text[:-1] + "+00:00"
|
|
213
211
|
try:
|
|
214
|
-
return
|
|
215
|
-
except
|
|
216
|
-
return
|
|
212
|
+
return datetime.fromisoformat(text)
|
|
213
|
+
except ValueError:
|
|
214
|
+
return None
|
|
217
215
|
|
|
218
216
|
|
|
219
|
-
def
|
|
220
|
-
if
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
|
|
217
|
+
def _templates_need_refresh(config: Dict[str, Any]) -> bool:
|
|
218
|
+
if not config or not config.get("token_value"):
|
|
219
|
+
return False
|
|
220
|
+
last = _parse_iso_timestamp(config.get("templates_last_refreshed") or "")
|
|
221
|
+
if not last:
|
|
222
|
+
return True
|
|
223
|
+
return (datetime.now(timezone.utc) - last).total_seconds() >= TEMPLATES_REFRESH_INTERVAL_S
|
|
224
224
|
|
|
225
225
|
|
|
226
|
-
def
|
|
227
|
-
if
|
|
228
|
-
return
|
|
229
|
-
text = str(value or "").strip()
|
|
230
|
-
if not text:
|
|
231
|
-
return 0.0
|
|
232
|
-
match = re.match(r"(?i)^\s*([0-9]*\.?[0-9]+)\s*([kmgtp]?i?b?)?\s*$", text)
|
|
233
|
-
if not match:
|
|
234
|
-
return 0.0
|
|
235
|
-
number = match.group(1)
|
|
236
|
-
unit = (match.group(2) or "").lower()
|
|
226
|
+
def _ensure_templates_refreshed_on_startup(config: Dict[str, Any]) -> None:
|
|
227
|
+
if not _templates_need_refresh(config):
|
|
228
|
+
return
|
|
237
229
|
try:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return value * 1024**4
|
|
249
|
-
if unit.startswith("p"):
|
|
250
|
-
return value * 1024**5
|
|
251
|
-
return value
|
|
230
|
+
client = _build_proxmox_client_from_config(config)
|
|
231
|
+
node = config.get("node") or _pick_node(client)
|
|
232
|
+
storages = client.nodes(node).storage.get()
|
|
233
|
+
templates = _list_templates(client, node, storages)
|
|
234
|
+
if templates:
|
|
235
|
+
config["templates"] = templates
|
|
236
|
+
config["templates_last_refreshed"] = _current_time_iso()
|
|
237
|
+
_save_config(config)
|
|
238
|
+
except Exception as exc:
|
|
239
|
+
logger.warning("Unable to refresh Proxmox templates on startup: %s", exc)
|
|
252
240
|
|
|
253
241
|
|
|
254
|
-
def
|
|
255
|
-
|
|
242
|
+
def _pick_storage(storages: Iterable[Dict[str, Any]]) -> str:
|
|
243
|
+
candidates = [s for s in storages if "rootdir" in s.get("content", "") and s.get("avail", 0) > 0]
|
|
244
|
+
if not candidates:
|
|
245
|
+
candidates = [s for s in storages if "rootdir" in s.get("content", "")]
|
|
246
|
+
if not candidates:
|
|
247
|
+
return ""
|
|
248
|
+
candidates.sort(key=lambda entry: entry.get("avail", 0), reverse=True)
|
|
249
|
+
return candidates[0].get("storage", "")
|
|
256
250
|
|
|
257
251
|
|
|
258
|
-
def _bytes_to_gib(value: Any) ->
|
|
259
|
-
|
|
252
|
+
def _bytes_to_gib(value: Any) -> float:
|
|
253
|
+
try:
|
|
254
|
+
return float(value) / 1024**3
|
|
255
|
+
except (TypeError, ValueError):
|
|
256
|
+
return 0.0
|
|
260
257
|
|
|
261
258
|
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return "unknown"
|
|
268
|
-
primary = text.split(",", 1)[0]
|
|
269
|
-
storage = primary.split(":", 1)[0].strip()
|
|
270
|
-
return storage or "unknown"
|
|
259
|
+
def _bytes_to_mib(value: Any) -> float:
|
|
260
|
+
try:
|
|
261
|
+
return float(value) / 1024**2
|
|
262
|
+
except (TypeError, ValueError):
|
|
263
|
+
return 0.0
|
|
271
264
|
|
|
272
265
|
|
|
273
266
|
def _size_token_to_gib(token: str) -> float:
|
|
@@ -298,21 +291,30 @@ def _extract_size_gib(value: Any) -> float:
|
|
|
298
291
|
return _size_token_to_gib(text)
|
|
299
292
|
|
|
300
293
|
|
|
294
|
+
def _extract_storage_token(value: Any) -> str:
|
|
295
|
+
if not value:
|
|
296
|
+
return "unknown"
|
|
297
|
+
text = str(value)
|
|
298
|
+
if ":" in text:
|
|
299
|
+
return text.split(":", 1)[0].strip() or "unknown"
|
|
300
|
+
return text.strip() or "unknown"
|
|
301
|
+
|
|
302
|
+
|
|
301
303
|
def _storage_from_lxc(cfg: Dict[str, Any], entry: Dict[str, Any]) -> str:
|
|
302
304
|
rootfs = cfg.get("rootfs") or entry.get("rootfs")
|
|
303
|
-
storage =
|
|
305
|
+
storage = _extract_storage_token(rootfs)
|
|
304
306
|
if storage != "unknown":
|
|
305
307
|
return storage
|
|
306
308
|
for idx in range(0, 10):
|
|
307
309
|
mp_value = cfg.get(f"mp{idx}")
|
|
308
|
-
storage =
|
|
310
|
+
storage = _extract_storage_token(mp_value)
|
|
309
311
|
if storage != "unknown":
|
|
310
312
|
return storage
|
|
311
313
|
return "unknown"
|
|
312
314
|
|
|
313
315
|
|
|
314
316
|
def _storage_from_qemu(cfg: Dict[str, Any]) -> str:
|
|
315
|
-
preferred_keys = []
|
|
317
|
+
preferred_keys: List[str] = []
|
|
316
318
|
for prefix in ("scsi", "virtio", "sata", "ide"):
|
|
317
319
|
preferred_keys.extend(f"{prefix}{idx}" for idx in range(0, 6))
|
|
318
320
|
seen = set()
|
|
@@ -324,7 +326,7 @@ def _storage_from_qemu(cfg: Dict[str, Any]) -> str:
|
|
|
324
326
|
text = str(value)
|
|
325
327
|
if "media=cdrom" in text or "cloudinit" in text:
|
|
326
328
|
continue
|
|
327
|
-
storage =
|
|
329
|
+
storage = _extract_storage_token(text)
|
|
328
330
|
if storage != "unknown":
|
|
329
331
|
return storage
|
|
330
332
|
for key in sorted(cfg.keys()):
|
|
@@ -338,11 +340,11 @@ def _storage_from_qemu(cfg: Dict[str, Any]) -> str:
|
|
|
338
340
|
text = str(value)
|
|
339
341
|
if "media=cdrom" in text or "cloudinit" in text:
|
|
340
342
|
continue
|
|
341
|
-
storage =
|
|
343
|
+
storage = _extract_storage_token(text)
|
|
342
344
|
if storage != "unknown":
|
|
343
345
|
return storage
|
|
344
346
|
for key in ("efidisk0", "tpmstate0"):
|
|
345
|
-
storage =
|
|
347
|
+
storage = _extract_storage_token(cfg.get(key))
|
|
346
348
|
if storage != "unknown":
|
|
347
349
|
return storage
|
|
348
350
|
return "unknown"
|
|
@@ -353,7 +355,7 @@ def _primary_lxc_disk(cfg: Dict[str, Any], entry: Dict[str, Any]) -> str:
|
|
|
353
355
|
|
|
354
356
|
|
|
355
357
|
def _primary_qemu_disk(cfg: Dict[str, Any]) -> str:
|
|
356
|
-
preferred_keys = []
|
|
358
|
+
preferred_keys: List[str] = []
|
|
357
359
|
for prefix in ("scsi", "virtio", "sata", "ide"):
|
|
358
360
|
preferred_keys.extend(f"{prefix}{idx}" for idx in range(0, 6))
|
|
359
361
|
seen = set()
|
|
@@ -381,8 +383,8 @@ def _primary_qemu_disk(cfg: Dict[str, Any]) -> str:
|
|
|
381
383
|
return ""
|
|
382
384
|
|
|
383
385
|
|
|
384
|
-
def
|
|
385
|
-
storage =
|
|
386
|
+
def _pick_container_storage(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> str:
|
|
387
|
+
storage = _extract_storage_token(cfg.get("storage") or entry.get("storage"))
|
|
386
388
|
if storage != "unknown":
|
|
387
389
|
return storage
|
|
388
390
|
if kind == "lxc":
|
|
@@ -390,7 +392,7 @@ def _pick_storage(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> str:
|
|
|
390
392
|
return _storage_from_qemu(cfg)
|
|
391
393
|
|
|
392
394
|
|
|
393
|
-
def
|
|
395
|
+
def _pick_container_disk_gib(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> float:
|
|
394
396
|
if kind == "lxc":
|
|
395
397
|
size = _extract_size_gib(_primary_lxc_disk(cfg, entry))
|
|
396
398
|
if size:
|
|
@@ -400,70 +402,62 @@ def _pick_disk_gib(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> flo
|
|
|
400
402
|
if size:
|
|
401
403
|
return size
|
|
402
404
|
for candidate in (entry.get("maxdisk"), entry.get("disk"), cfg.get("disk")):
|
|
403
|
-
if candidate
|
|
405
|
+
if candidate is None or candidate == 0:
|
|
404
406
|
continue
|
|
405
407
|
return _bytes_to_gib(candidate)
|
|
406
408
|
return 0.0
|
|
407
409
|
|
|
408
410
|
|
|
409
|
-
def
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
411
|
+
def _to_mib(value: Any) -> float:
|
|
412
|
+
try:
|
|
413
|
+
val = float(value)
|
|
414
|
+
except (TypeError, ValueError):
|
|
415
|
+
return 0.0
|
|
416
|
+
if val <= 0:
|
|
417
|
+
return 0.0
|
|
418
|
+
# Heuristic: large values are bytes, smaller ones are already MiB.
|
|
419
|
+
return _bytes_to_mib(val) if val > 10000 else val
|
|
413
420
|
|
|
414
421
|
|
|
415
|
-
def
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
422
|
+
def _pick_container_ram_mib(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> float:
|
|
423
|
+
for candidate in (cfg.get("memory"), entry.get("maxmem"), entry.get("mem")):
|
|
424
|
+
ram = _to_mib(candidate)
|
|
425
|
+
if ram:
|
|
426
|
+
return ram
|
|
427
|
+
return 0.0
|
|
420
428
|
|
|
421
429
|
|
|
422
|
-
def
|
|
423
|
-
if not value:
|
|
424
|
-
return None
|
|
425
|
-
text = value
|
|
426
|
-
if text.endswith("Z"):
|
|
427
|
-
text = text[:-1] + "+00:00"
|
|
430
|
+
def _safe_float(value: Any) -> float:
|
|
428
431
|
try:
|
|
429
|
-
return
|
|
430
|
-
except ValueError:
|
|
431
|
-
return
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def _templates_need_refresh(config: Dict[str, Any]) -> bool:
|
|
435
|
-
if not config or not config.get("token_value"):
|
|
436
|
-
return False
|
|
437
|
-
last = _parse_iso_timestamp(config.get("templates_last_refreshed") or "")
|
|
438
|
-
if not last:
|
|
439
|
-
return True
|
|
440
|
-
return (datetime.now(timezone.utc) - last).total_seconds() >= TEMPLATES_REFRESH_INTERVAL_S
|
|
432
|
+
return float(value)
|
|
433
|
+
except (TypeError, ValueError):
|
|
434
|
+
return 0.0
|
|
441
435
|
|
|
442
436
|
|
|
443
|
-
def
|
|
444
|
-
if
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
437
|
+
def _pick_container_cpu_share(kind: str, cfg: Dict[str, Any], entry: Dict[str, Any]) -> float:
|
|
438
|
+
if kind == "lxc":
|
|
439
|
+
for key in ("cpulimit", "cores", "cpus"):
|
|
440
|
+
val = _safe_float(cfg.get(key))
|
|
441
|
+
if val:
|
|
442
|
+
return val
|
|
443
|
+
return _safe_float(entry.get("cpus"))
|
|
444
|
+
|
|
445
|
+
cores = _safe_float(cfg.get("cores"))
|
|
446
|
+
sockets = _safe_float(cfg.get("sockets")) or 1.0
|
|
447
|
+
if cores:
|
|
448
|
+
return cores * sockets
|
|
449
|
+
val = _safe_float(cfg.get("vcpus"))
|
|
450
|
+
if val:
|
|
451
|
+
return val
|
|
452
|
+
val = _safe_float(entry.get("cpus") or entry.get("maxcpu"))
|
|
453
|
+
if val:
|
|
454
|
+
return val
|
|
455
|
+
return 0.0
|
|
457
456
|
|
|
458
457
|
|
|
459
|
-
def
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
candidates = [s for s in storages if "rootdir" in s.get("content", "")]
|
|
463
|
-
if not candidates:
|
|
464
|
-
return ""
|
|
465
|
-
candidates.sort(key=lambda entry: entry.get("avail", 0), reverse=True)
|
|
466
|
-
return candidates[0].get("storage", "")
|
|
458
|
+
def _parse_onboot_flag(value: Any) -> bool:
|
|
459
|
+
text = str(value).strip().lower()
|
|
460
|
+
return text in {"1", "true", "yes", "on"}
|
|
467
461
|
|
|
468
462
|
|
|
469
463
|
def _write_bridge_config(bridge: str) -> None:
|
|
@@ -599,154 +593,35 @@ def _load_managed_container_records() -> List[Dict[str, Any]]:
|
|
|
599
593
|
return records
|
|
600
594
|
|
|
601
595
|
|
|
602
|
-
def
|
|
603
|
-
if not node_status:
|
|
604
|
-
return None, None, None
|
|
605
|
-
memory_total = node_status.get("memory", {}).get("total")
|
|
606
|
-
disk_total = node_status.get("disk", {}).get("total")
|
|
607
|
-
cpu_cores = node_status.get("cpuinfo", {}).get("cores")
|
|
608
|
-
host_ram = _bytes_to_mib(memory_total) if memory_total is not None else None
|
|
609
|
-
host_disk = _bytes_to_gib(disk_total) if disk_total is not None else None
|
|
610
|
-
host_cpu = _to_int(cpu_cores) if cpu_cores is not None else None
|
|
611
|
-
return host_ram, host_disk, host_cpu
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
def _parse_disk_reference(value: Any) -> Tuple[str | None, int]:
|
|
615
|
-
if not value:
|
|
616
|
-
return None, 0
|
|
617
|
-
text = str(value)
|
|
618
|
-
primary = text.split(",", 1)[0]
|
|
619
|
-
if ":" not in primary:
|
|
620
|
-
return primary.strip() or None, 0
|
|
621
|
-
storage_name, size_part = primary.split(":", 1)
|
|
622
|
-
storage_name = storage_name.strip() or None
|
|
623
|
-
size_text = size_part.strip()
|
|
624
|
-
if not size_text:
|
|
625
|
-
return storage_name, 0
|
|
626
|
-
unit = size_text[-1].upper()
|
|
627
|
-
number = size_text[:-1] if unit in {"G", "M"} else size_text
|
|
628
|
-
try:
|
|
629
|
-
value_num = float(number)
|
|
630
|
-
except ValueError:
|
|
631
|
-
return storage_name, 0
|
|
632
|
-
if unit == "M":
|
|
633
|
-
gib = value_num / 1024.0
|
|
634
|
-
else:
|
|
635
|
-
gib = value_num
|
|
636
|
-
return storage_name, int(round(gib))
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
def _build_unmanaged_container_entry(
|
|
640
|
-
ct: Dict[str, Any],
|
|
641
|
-
cfg: Dict[str, Any],
|
|
642
|
-
vmid: str,
|
|
643
|
-
default_storage: str | None,
|
|
644
|
-
*,
|
|
645
|
-
disk_gib_override: int | None = None,
|
|
646
|
-
storage_override: str | None = None,
|
|
647
|
-
entry_type: str = "lxc",
|
|
648
|
-
) -> Dict[str, Any]:
|
|
649
|
-
ram_mib = _to_int(cfg.get("memory")) or _bytes_to_mib(ct.get("maxmem"))
|
|
650
|
-
disk_gib = disk_gib_override if disk_gib_override is not None else _pick_disk_gib(entry_type, cfg, ct)
|
|
651
|
-
cpu_share = _to_float(
|
|
652
|
-
cfg.get("cpulimit")
|
|
653
|
-
or cfg.get("cpus")
|
|
654
|
-
or cfg.get("cores")
|
|
655
|
-
or ct.get("cpus")
|
|
656
|
-
or ct.get("cpu")
|
|
657
|
-
)
|
|
658
|
-
hostname = ct.get("name") or cfg.get("hostname") or f"ct{vmid}"
|
|
659
|
-
storage = storage_override or _pick_storage(entry_type, cfg, ct)
|
|
660
|
-
status = (ct.get("status") or "unknown").lower()
|
|
661
|
-
reserved = _parse_bool_flag(cfg.get("onboot"))
|
|
662
|
-
storage_matches_default = _storage_matches_default(storage, default_storage)
|
|
663
|
-
return {
|
|
664
|
-
"vmid": vmid,
|
|
665
|
-
"hostname": hostname,
|
|
666
|
-
"template": cfg.get("ostemplate"),
|
|
667
|
-
"storage": storage,
|
|
668
|
-
"disk_gib": disk_gib,
|
|
669
|
-
"ram_mib": ram_mib,
|
|
670
|
-
"cpu_share": cpu_share,
|
|
671
|
-
"reserve_on_boot": reserved,
|
|
672
|
-
"matches_default_storage": storage_matches_default,
|
|
673
|
-
"type": entry_type,
|
|
674
|
-
"status": status,
|
|
675
|
-
"managed": False,
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
def _get_storage_snapshot(proxmox: Any, node: str, storage_name: str | None) -> Dict[str, Any] | None:
|
|
680
|
-
if not storage_name:
|
|
681
|
-
return None
|
|
682
|
-
try:
|
|
683
|
-
storage = proxmox.nodes(node).storage(storage_name).status.get()
|
|
684
|
-
except Exception as exc:
|
|
685
|
-
logger.debug("Unable to read storage status %s:%s: %s", node, storage_name, exc)
|
|
686
|
-
return None
|
|
687
|
-
total_bytes = storage.get("total")
|
|
688
|
-
avail_bytes = storage.get("avail")
|
|
689
|
-
used_bytes = storage.get("used")
|
|
690
|
-
return {
|
|
691
|
-
"storage": storage_name,
|
|
692
|
-
"total_gib": _bytes_to_gib(total_bytes) if total_bytes is not None else None,
|
|
693
|
-
"avail_gib": _bytes_to_gib(avail_bytes) if avail_bytes is not None else None,
|
|
694
|
-
"used_gib": _bytes_to_gib(used_bytes) if used_bytes is not None else None,
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
def _extract_qemu_disk_info(cfg: Dict[str, Any]) -> Tuple[str | None, int]:
|
|
699
|
-
disk_keys = sorted(
|
|
700
|
-
key
|
|
701
|
-
for key in cfg.keys()
|
|
702
|
-
if (
|
|
703
|
-
key.startswith("ide")
|
|
704
|
-
or key.startswith("sata")
|
|
705
|
-
or key.startswith("scsi")
|
|
706
|
-
or key.startswith("virtio")
|
|
707
|
-
or key.startswith("efidisk")
|
|
708
|
-
or key.startswith("raid")
|
|
709
|
-
)
|
|
710
|
-
and cfg.get(key)
|
|
711
|
-
)
|
|
712
|
-
for key in disk_keys:
|
|
713
|
-
storage_name, size_gib = _parse_disk_reference(cfg.get(key))
|
|
714
|
-
if storage_name or size_gib:
|
|
715
|
-
return storage_name, size_gib
|
|
716
|
-
return None, 0
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
def _storage_matches_default(storage_name: Any, default_storage: str | None) -> bool:
|
|
720
|
-
if not default_storage:
|
|
721
|
-
return False
|
|
722
|
-
storage = _extract_storage(storage_name)
|
|
723
|
-
default = str(default_storage).strip()
|
|
724
|
-
return storage.lower() == default.lower()
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
def _build_managed_containers_summary(
|
|
728
|
-
records: List[Dict[str, Any]],
|
|
729
|
-
unmanaged_records: List[Dict[str, Any]],
|
|
730
|
-
node_status: Dict[str, Any] | None,
|
|
731
|
-
storage_snapshot: Dict[str, Any] | None,
|
|
732
|
-
default_storage: str | None,
|
|
733
|
-
) -> Dict[str, Any]:
|
|
734
|
-
managed_containers: List[Dict[str, Any]] = []
|
|
596
|
+
def _build_managed_containers_summary(records: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
735
597
|
total_ram = 0
|
|
736
598
|
total_disk = 0
|
|
737
599
|
total_cpu_share = 0.0
|
|
600
|
+
containers: List[Dict[str, Any]] = []
|
|
738
601
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
602
|
+
def _as_int(value: Any) -> int:
|
|
603
|
+
try:
|
|
604
|
+
return int(value)
|
|
605
|
+
except (TypeError, ValueError):
|
|
606
|
+
return 0
|
|
607
|
+
|
|
608
|
+
def _as_float(value: Any) -> float:
|
|
609
|
+
try:
|
|
610
|
+
return float(value)
|
|
611
|
+
except (TypeError, ValueError):
|
|
612
|
+
return 0.0
|
|
613
|
+
|
|
614
|
+
for record in sorted(records, key=lambda entry: _as_int(entry.get("vmid"))):
|
|
615
|
+
ram_mib = _as_int(record.get("ram_mib"))
|
|
616
|
+
disk_gib = _as_int(record.get("disk_gib"))
|
|
617
|
+
cpu_share = _as_float(record.get("cpus"))
|
|
743
618
|
total_ram += ram_mib
|
|
744
619
|
total_disk += disk_gib
|
|
745
620
|
total_cpu_share += cpu_share
|
|
746
621
|
status = (record.get("status") or "unknown").lower()
|
|
747
|
-
|
|
622
|
+
containers.append(
|
|
748
623
|
{
|
|
749
|
-
"vmid": str(
|
|
624
|
+
"vmid": str(_as_int(record.get("vmid"))) if record.get("vmid") is not None else None,
|
|
750
625
|
"device_id": record.get("device_id"),
|
|
751
626
|
"hostname": record.get("hostname"),
|
|
752
627
|
"template": record.get("template"),
|
|
@@ -756,134 +631,177 @@ def _build_managed_containers_summary(
|
|
|
756
631
|
"cpu_share": cpu_share,
|
|
757
632
|
"created_at": record.get("created_at"),
|
|
758
633
|
"status": status,
|
|
759
|
-
"managed": True,
|
|
760
|
-
"matches_default_storage": _storage_matches_default(record.get("storage"), default_storage),
|
|
761
634
|
}
|
|
762
635
|
)
|
|
763
636
|
|
|
764
|
-
unmanaged_total_ram = sum(
|
|
765
|
-
_to_int(entry.get("ram_mib"))
|
|
766
|
-
for entry in unmanaged_records
|
|
767
|
-
if entry.get("reserve_on_boot")
|
|
768
|
-
)
|
|
769
|
-
unmanaged_total_disk = sum(
|
|
770
|
-
_to_int(entry.get("disk_gib"))
|
|
771
|
-
for entry in unmanaged_records
|
|
772
|
-
if entry.get("matches_default_storage")
|
|
773
|
-
)
|
|
774
|
-
unmanaged_total_cpu = sum(
|
|
775
|
-
_to_float(entry.get("cpu_share"))
|
|
776
|
-
for entry in unmanaged_records
|
|
777
|
-
if entry.get("reserve_on_boot")
|
|
778
|
-
)
|
|
779
|
-
|
|
780
|
-
allocated_ram = total_ram + unmanaged_total_ram
|
|
781
|
-
allocated_disk = total_disk + unmanaged_total_disk
|
|
782
|
-
allocated_cpu = total_cpu_share + unmanaged_total_cpu
|
|
783
|
-
|
|
784
|
-
host_ram_node, host_disk_node, host_cpu_node = _extract_host_totals(node_status)
|
|
785
|
-
storage_host_disk = storage_snapshot.get("total_gib") if storage_snapshot else None
|
|
786
|
-
host_ram = host_ram_node
|
|
787
|
-
host_disk = storage_host_disk if storage_host_disk is not None else host_disk_node
|
|
788
|
-
host_cpu = host_cpu_node
|
|
789
|
-
|
|
790
|
-
|
|
791
637
|
return {
|
|
792
638
|
"updated_at": datetime.utcnow().isoformat() + "Z",
|
|
793
|
-
"count": len(
|
|
639
|
+
"count": len(containers),
|
|
794
640
|
"total_ram_mib": total_ram,
|
|
795
641
|
"total_disk_gib": total_disk,
|
|
796
642
|
"total_cpu_share": round(total_cpu_share, 2),
|
|
797
|
-
"containers":
|
|
798
|
-
"unmanaged_containers": unmanaged_records,
|
|
799
|
-
"unmanaged_count": len(unmanaged_records),
|
|
800
|
-
"allocated_ram_mib": allocated_ram,
|
|
801
|
-
"allocated_disk_gib": allocated_disk,
|
|
802
|
-
"allocated_cpu_share": round(allocated_cpu, 2),
|
|
803
|
-
"available_ram_mib": _calculate_available(host_ram, allocated_ram) if host_ram is not None else None,
|
|
804
|
-
"available_disk_gib": _calculate_available(host_disk, allocated_disk) if host_disk is not None else None,
|
|
805
|
-
"available_cpu_share": _calculate_available(host_cpu, allocated_cpu) if host_cpu is not None else None,
|
|
806
|
-
"host_total_ram_mib": host_ram,
|
|
807
|
-
"host_total_disk_gib": host_disk,
|
|
808
|
-
"host_total_cpu_cores": host_cpu,
|
|
809
|
-
"default_storage": default_storage,
|
|
810
|
-
"default_storage_snapshot": storage_snapshot,
|
|
643
|
+
"containers": containers,
|
|
811
644
|
}
|
|
812
645
|
|
|
813
646
|
|
|
647
|
+
def _build_full_container_summary(records: List[Dict[str, Any]], config: Dict[str, Any]) -> Dict[str, Any]:
|
|
648
|
+
base_summary = _build_managed_containers_summary(records)
|
|
649
|
+
if not config or not config.get("token_value"):
|
|
650
|
+
return base_summary
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
proxmox = _connect_proxmox(config)
|
|
654
|
+
node = _get_node_from_config(config)
|
|
655
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
656
|
+
logger.debug("Unable to extend container summary with Proxmox data: %s", exc)
|
|
657
|
+
return base_summary
|
|
658
|
+
|
|
659
|
+
default_storage = (config.get("default_storage") or "").strip()
|
|
660
|
+
record_map: Dict[str, Dict[str, Any]] = {}
|
|
661
|
+
for record in records:
|
|
662
|
+
vmid = record.get("vmid")
|
|
663
|
+
if vmid is None:
|
|
664
|
+
continue
|
|
665
|
+
try:
|
|
666
|
+
vmid_key = str(int(vmid))
|
|
667
|
+
except (ValueError, TypeError):
|
|
668
|
+
continue
|
|
669
|
+
record_map[vmid_key] = record
|
|
670
|
+
|
|
671
|
+
managed_entries: List[Dict[str, Any]] = []
|
|
672
|
+
unmanaged_entries: List[Dict[str, Any]] = []
|
|
673
|
+
allocated_ram = 0.0
|
|
674
|
+
allocated_disk = 0.0
|
|
675
|
+
allocated_cpu = 0.0
|
|
676
|
+
|
|
677
|
+
def _process_entries(kind: str, getter: str) -> None:
|
|
678
|
+
nonlocal allocated_ram, allocated_disk, allocated_cpu
|
|
679
|
+
entries = getattr(proxmox.nodes(node), getter).get()
|
|
680
|
+
for entry in entries:
|
|
681
|
+
vmid = entry.get("vmid")
|
|
682
|
+
if vmid is None:
|
|
683
|
+
continue
|
|
684
|
+
vmid_str = str(vmid)
|
|
685
|
+
cfg: Dict[str, Any] = {}
|
|
686
|
+
try:
|
|
687
|
+
cfg = getattr(proxmox.nodes(node), getter)(vmid_str).config.get() or {}
|
|
688
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
689
|
+
logger.debug("Failed to load %s config for %s: %s", kind, vmid_str, exc)
|
|
690
|
+
cfg = {}
|
|
691
|
+
|
|
692
|
+
record = record_map.get(vmid_str)
|
|
693
|
+
description = cfg.get("description") or ""
|
|
694
|
+
managed = bool(record) or MANAGED_MARKER in description
|
|
695
|
+
hostname = entry.get("name") or cfg.get("hostname") or (record.get("hostname") if record else None)
|
|
696
|
+
storage = _pick_container_storage(kind, cfg, entry)
|
|
697
|
+
disk_gib = _pick_container_disk_gib(kind, cfg, entry)
|
|
698
|
+
ram_mib = _pick_container_ram_mib(kind, cfg, entry)
|
|
699
|
+
cpu_share = _pick_container_cpu_share(kind, cfg, entry)
|
|
700
|
+
reserve_on_boot = _parse_onboot_flag(cfg.get("onboot"))
|
|
701
|
+
matches_default_storage = bool(default_storage and storage and storage.lower() == default_storage.lower())
|
|
702
|
+
|
|
703
|
+
base_entry = {
|
|
704
|
+
"type": kind,
|
|
705
|
+
"vmid": vmid_str,
|
|
706
|
+
"hostname": hostname,
|
|
707
|
+
"status": (entry.get("status") or "unknown").lower(),
|
|
708
|
+
"storage": storage,
|
|
709
|
+
"disk_gib": disk_gib,
|
|
710
|
+
"ram_mib": ram_mib,
|
|
711
|
+
"cpu_share": cpu_share,
|
|
712
|
+
"reserve_on_boot": reserve_on_boot,
|
|
713
|
+
"matches_default_storage": matches_default_storage,
|
|
714
|
+
"managed": managed,
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if managed:
|
|
718
|
+
merged = base_entry | {
|
|
719
|
+
"device_id": record.get("device_id") if record else None,
|
|
720
|
+
"template": record.get("template") if record else None,
|
|
721
|
+
"created_at": record.get("created_at") if record else None,
|
|
722
|
+
}
|
|
723
|
+
managed_entries.append(merged)
|
|
724
|
+
else:
|
|
725
|
+
unmanaged_entries.append(base_entry)
|
|
726
|
+
|
|
727
|
+
if managed or reserve_on_boot:
|
|
728
|
+
allocated_ram += ram_mib
|
|
729
|
+
allocated_cpu += cpu_share
|
|
730
|
+
if managed or matches_default_storage:
|
|
731
|
+
allocated_disk += disk_gib
|
|
732
|
+
|
|
733
|
+
_process_entries("lxc", "lxc")
|
|
734
|
+
_process_entries("qemu", "qemu")
|
|
735
|
+
|
|
736
|
+
memory_info = {}
|
|
737
|
+
cpu_info = {}
|
|
738
|
+
try:
|
|
739
|
+
node_status = proxmox.nodes(node).status.get()
|
|
740
|
+
memory_info = node_status.get("memory") or {}
|
|
741
|
+
cpu_info = node_status.get("cpuinfo") or {}
|
|
742
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
743
|
+
logger.debug("Unable to read node status for resource totals: %s", exc)
|
|
744
|
+
|
|
745
|
+
host_total_ram_mib = _bytes_to_mib(memory_info.get("total"))
|
|
746
|
+
used_ram_mib = _bytes_to_mib(memory_info.get("used"))
|
|
747
|
+
available_ram_mib = max(host_total_ram_mib - used_ram_mib, 0.0) if host_total_ram_mib else None
|
|
748
|
+
host_total_cpu_cores = _safe_float(cpu_info.get("cores"))
|
|
749
|
+
available_cpu_share = max(host_total_cpu_cores - allocated_cpu, 0.0) if host_total_cpu_cores else None
|
|
750
|
+
|
|
751
|
+
host_total_disk_gib = None
|
|
752
|
+
available_disk_gib = None
|
|
753
|
+
if default_storage:
|
|
754
|
+
try:
|
|
755
|
+
storage_status = proxmox.nodes(node).storage(default_storage).status.get()
|
|
756
|
+
host_total_disk_gib = _bytes_to_gib(storage_status.get("total"))
|
|
757
|
+
available_disk_gib = _bytes_to_gib(storage_status.get("avail"))
|
|
758
|
+
except Exception as exc: # pragma: no cover - best effort
|
|
759
|
+
logger.debug("Unable to read storage status for %s: %s", default_storage, exc)
|
|
760
|
+
|
|
761
|
+
summary = base_summary.copy()
|
|
762
|
+
summary["containers"] = managed_entries
|
|
763
|
+
summary["count"] = len(managed_entries)
|
|
764
|
+
summary["total_ram_mib"] = int(sum(entry.get("ram_mib") or 0 for entry in managed_entries))
|
|
765
|
+
summary["total_disk_gib"] = int(sum(entry.get("disk_gib") or 0 for entry in managed_entries))
|
|
766
|
+
summary["total_cpu_share"] = round(sum(entry.get("cpu_share") or 0 for entry in managed_entries), 2)
|
|
767
|
+
summary["unmanaged_containers"] = unmanaged_entries
|
|
768
|
+
summary["allocated_ram_mib"] = round(allocated_ram, 2)
|
|
769
|
+
summary["allocated_disk_gib"] = round(allocated_disk, 2)
|
|
770
|
+
summary["allocated_cpu_share"] = round(allocated_cpu, 2)
|
|
771
|
+
summary["host_total_ram_mib"] = int(host_total_ram_mib) if host_total_ram_mib else None
|
|
772
|
+
summary["host_total_disk_gib"] = host_total_disk_gib
|
|
773
|
+
summary["host_total_cpu_cores"] = host_total_cpu_cores if host_total_cpu_cores else None
|
|
774
|
+
summary["available_ram_mib"] = int(available_ram_mib) if available_ram_mib is not None else None
|
|
775
|
+
summary["available_disk_gib"] = available_disk_gib
|
|
776
|
+
summary["available_cpu_share"] = available_cpu_share if available_cpu_share is not None else None
|
|
777
|
+
return summary
|
|
778
|
+
|
|
779
|
+
|
|
814
780
|
def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
815
|
-
def _refresh_container_statuses(
|
|
816
|
-
records
|
|
817
|
-
|
|
818
|
-
managed_vmids: Set[str],
|
|
819
|
-
default_storage: str | None,
|
|
820
|
-
) -> Tuple[Dict[str, str], List[Dict[str, Any]], Dict[str, Any] | None, Dict[str, Any] | None]:
|
|
821
|
-
statuses: Dict[str, str] = {}
|
|
822
|
-
unmanaged: List[Dict[str, Any]] = []
|
|
823
|
-
node_status: Dict[str, Any] | None = None
|
|
824
|
-
if not config:
|
|
825
|
-
return statuses, unmanaged, node_status, None
|
|
826
|
-
proxmox = None
|
|
827
|
-
node = None
|
|
781
|
+
def _refresh_container_statuses(records: List[Dict[str, Any]], config: Dict[str, Any] | None) -> None:
|
|
782
|
+
if not records or not config:
|
|
783
|
+
return
|
|
828
784
|
try:
|
|
829
785
|
proxmox = _connect_proxmox(config)
|
|
830
786
|
node = _get_node_from_config(config)
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
continue
|
|
836
|
-
vmid_key = str(_to_int(vmid_val))
|
|
837
|
-
statuses[vmid_key] = (ct.get("status") or "unknown").lower()
|
|
838
|
-
if vmid_key in managed_vmids:
|
|
839
|
-
continue
|
|
840
|
-
cfg: Dict[str, Any] = {}
|
|
841
|
-
try:
|
|
842
|
-
cfg = proxmox.nodes(node).lxc(vmid_key).config.get() or {}
|
|
843
|
-
except Exception as exc: # pragma: no cover - best effort
|
|
844
|
-
logger.debug("Failed to read config for container %s: %s", vmid_key, exc)
|
|
845
|
-
description = (cfg.get("description") or "")
|
|
846
|
-
if MANAGED_MARKER in description:
|
|
847
|
-
continue
|
|
848
|
-
unmanaged.append(
|
|
849
|
-
_build_unmanaged_container_entry(ct, cfg, vmid_key, default_storage, entry_type="lxc")
|
|
850
|
-
)
|
|
851
|
-
for vm in proxmox.nodes(node).qemu.get():
|
|
852
|
-
vmid_val = vm.get("vmid")
|
|
853
|
-
if vmid_val is None:
|
|
854
|
-
continue
|
|
855
|
-
vmid_key = str(_to_int(vmid_val))
|
|
856
|
-
statuses[vmid_key] = (vm.get("status") or "unknown").lower()
|
|
857
|
-
if vmid_key in managed_vmids:
|
|
858
|
-
continue
|
|
859
|
-
cfg: Dict[str, Any] = {}
|
|
860
|
-
try:
|
|
861
|
-
cfg = proxmox.nodes(node).qemu(vmid_key).config.get() or {}
|
|
862
|
-
except Exception as exc:
|
|
863
|
-
logger.debug("Failed to read config for VM %s: %s", vmid_key, exc)
|
|
864
|
-
description = (cfg.get("description") or "")
|
|
865
|
-
if MANAGED_MARKER in description:
|
|
866
|
-
continue
|
|
867
|
-
storage_name, disk_gib_override = _extract_qemu_disk_info(cfg)
|
|
868
|
-
unmanaged.append(
|
|
869
|
-
_build_unmanaged_container_entry(
|
|
870
|
-
vm,
|
|
871
|
-
cfg,
|
|
872
|
-
vmid_key,
|
|
873
|
-
default_storage,
|
|
874
|
-
disk_gib_override=disk_gib_override,
|
|
875
|
-
storage_override=storage_name,
|
|
876
|
-
entry_type="qemu",
|
|
877
|
-
)
|
|
878
|
-
)
|
|
787
|
+
statuses = {
|
|
788
|
+
str(ct.get("vmid")): (ct.get("status") or "unknown").lower()
|
|
789
|
+
for ct in proxmox.nodes(node).lxc.get()
|
|
790
|
+
}
|
|
879
791
|
except Exception as exc: # pragma: no cover - best effort
|
|
880
792
|
logger.debug("Failed to refresh container statuses: %s", exc)
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
793
|
+
return
|
|
794
|
+
for record in records:
|
|
795
|
+
vmid = record.get("vmid")
|
|
796
|
+
if vmid is None:
|
|
797
|
+
continue
|
|
798
|
+
try:
|
|
799
|
+
vmid_key = str(int(vmid))
|
|
800
|
+
except (ValueError, TypeError):
|
|
801
|
+
continue
|
|
802
|
+
status = statuses.get(vmid_key)
|
|
803
|
+
if status:
|
|
804
|
+
record["status"] = status
|
|
887
805
|
|
|
888
806
|
now = time.monotonic()
|
|
889
807
|
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
@@ -893,22 +811,8 @@ def _get_managed_containers_summary(force: bool = False) -> Dict[str, Any]:
|
|
|
893
811
|
return cached
|
|
894
812
|
config = _load_config()
|
|
895
813
|
records = _load_managed_container_records()
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
default_storage = config.get("default_storage") if config else None
|
|
900
|
-
statuses, unmanaged, node_status, storage_snapshot = _refresh_container_statuses(
|
|
901
|
-
records, config, managed_vmids, default_storage
|
|
902
|
-
)
|
|
903
|
-
for record in records:
|
|
904
|
-
vmid = record.get("vmid")
|
|
905
|
-
if vmid is None:
|
|
906
|
-
continue
|
|
907
|
-
vmid_key = str(_to_int(vmid))
|
|
908
|
-
status = statuses.get(vmid_key)
|
|
909
|
-
if status:
|
|
910
|
-
record["status"] = status
|
|
911
|
-
summary = _build_managed_containers_summary(records, unmanaged, node_status, storage_snapshot, default_storage)
|
|
814
|
+
_refresh_container_statuses(records, config)
|
|
815
|
+
summary = _build_full_container_summary(records, config)
|
|
912
816
|
with _MANAGED_CONTAINERS_CACHE_LOCK:
|
|
913
817
|
_MANAGED_CONTAINERS_CACHE["timestamp"] = now
|
|
914
818
|
_MANAGED_CONTAINERS_CACHE["summary"] = summary
|
|
@@ -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=ZsUlJUljVebwmjOdkxbUPcwi8iyVjIuyBU9g68awYpA,721
|
|
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
|
|
@@ -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=-zAmaOTCTJaic8sclb5Z0DVCl_pU7XePo86NKohi3gc,85295
|
|
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
|
|
@@ -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.16.
|
|
68
|
+
portacode-1.4.16.dev11.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.16.
|
|
95
|
-
portacode-1.4.16.
|
|
96
|
-
portacode-1.4.16.
|
|
97
|
-
portacode-1.4.16.
|
|
98
|
-
portacode-1.4.16.
|
|
94
|
+
portacode-1.4.16.dev11.dist-info/METADATA,sha256=nEvtzLLbq9J8jG-kubMsNMRQ07YdtEczInIarfWpGhY,13052
|
|
95
|
+
portacode-1.4.16.dev11.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
96
|
+
portacode-1.4.16.dev11.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
|
|
97
|
+
portacode-1.4.16.dev11.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
|
|
98
|
+
portacode-1.4.16.dev11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|