clonebox 1.1.20__tar.gz → 1.1.21__tar.gz
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.
Potentially problematic release.
This version of clonebox might be problematic. Click here for more details.
- {clonebox-1.1.20/src/clonebox.egg-info → clonebox-1.1.21}/PKG-INFO +1 -1
- {clonebox-1.1.20 → clonebox-1.1.21}/pyproject.toml +1 -1
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/cli.py +36 -6
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/cloner.py +35 -6
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/validator.py +77 -12
- {clonebox-1.1.20 → clonebox-1.1.21/src/clonebox.egg-info}/PKG-INFO +1 -1
- {clonebox-1.1.20 → clonebox-1.1.21}/LICENSE +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/README.md +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/setup.cfg +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/__init__.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/__main__.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/audit.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/backends/libvirt_backend.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/backends/qemu_disk.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/backends/subprocess_runner.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/container.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/dashboard.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/detector.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/di.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/exporter.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/health/__init__.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/health/manager.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/health/models.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/health/probes.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/importer.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/interfaces/disk.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/interfaces/hypervisor.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/interfaces/network.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/interfaces/process.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/logging.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/models.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/monitor.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/orchestrator.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/p2p.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/plugins/__init__.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/plugins/base.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/plugins/manager.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/policies/__init__.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/policies/engine.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/policies/models.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/policies/validators.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/profiles.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/remote.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/resource_monitor.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/resources.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/rollback.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/secrets.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/snapshots/__init__.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/snapshots/manager.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/snapshots/models.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/templates/profiles/ml-dev.yaml +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox/templates/profiles/web-stack.yaml +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/SOURCES.txt +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/dependency_links.txt +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/entry_points.txt +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/requires.txt +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/src/clonebox.egg-info/top_level.txt +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_audit.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_cli.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_cli_new_commands.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_cloner.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_cloner_simple.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_container.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_coverage_additional.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_coverage_boost_final.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_dashboard_coverage.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_detector.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_models.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_network.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_orchestrator.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_plugins.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_profiles.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_remote.py +0 -0
- {clonebox-1.1.20 → clonebox-1.1.21}/tests/test_validator.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "clonebox"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.21"
|
|
8
8
|
description = "Clone your workstation environment to an isolated VM with selective apps, paths and services"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -945,10 +945,27 @@ def interactive_mode():
|
|
|
945
945
|
cloner = SelectiveVMCloner(user_session=getattr(args, "user", False))
|
|
946
946
|
|
|
947
947
|
# Check prerequisites
|
|
948
|
-
checks = cloner.check_prerequisites()
|
|
949
|
-
|
|
948
|
+
checks = cloner.check_prerequisites(config)
|
|
949
|
+
required_keys = [
|
|
950
|
+
"libvirt_connected",
|
|
951
|
+
"kvm_available",
|
|
952
|
+
"default_network",
|
|
953
|
+
"images_dir_writable",
|
|
954
|
+
"genisoimage_installed",
|
|
955
|
+
"qemu_img_installed",
|
|
956
|
+
]
|
|
957
|
+
if getattr(config, "gui", False):
|
|
958
|
+
required_keys.append("virt_viewer_installed")
|
|
959
|
+
|
|
960
|
+
required_checks = {
|
|
961
|
+
k: checks.get(k)
|
|
962
|
+
for k in required_keys
|
|
963
|
+
if isinstance(checks.get(k), bool)
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if required_checks and not all(required_checks.values()):
|
|
950
967
|
console.print("[yellow]⚠️ Prerequisites check:[/]")
|
|
951
|
-
for check, passed in
|
|
968
|
+
for check, passed in required_checks.items():
|
|
952
969
|
icon = "✅" if passed else "❌"
|
|
953
970
|
console.print(f" {icon} {check}")
|
|
954
971
|
|
|
@@ -2624,10 +2641,23 @@ def create_vm_from_config(config, start=False, user_session=False, replace=False
|
|
|
2624
2641
|
cloner = SelectiveVMCloner(user_session=user_session)
|
|
2625
2642
|
|
|
2626
2643
|
# Check prerequisites
|
|
2627
|
-
checks = cloner.check_prerequisites()
|
|
2628
|
-
|
|
2644
|
+
checks = cloner.check_prerequisites(vm_config)
|
|
2645
|
+
required_keys = [
|
|
2646
|
+
"libvirt_connected",
|
|
2647
|
+
"kvm_available",
|
|
2648
|
+
"default_network",
|
|
2649
|
+
"images_dir_writable",
|
|
2650
|
+
"genisoimage_installed",
|
|
2651
|
+
"qemu_img_installed",
|
|
2652
|
+
]
|
|
2653
|
+
if getattr(vm_config, "gui", False):
|
|
2654
|
+
required_keys.append("virt_viewer_installed")
|
|
2655
|
+
|
|
2656
|
+
required_checks = {k: checks.get(k) for k in required_keys if isinstance(checks.get(k), bool)}
|
|
2657
|
+
|
|
2658
|
+
if required_checks and not all(required_checks.values()):
|
|
2629
2659
|
console.print("[yellow]⚠️ Prerequisites check:[/]")
|
|
2630
|
-
for check, passed in
|
|
2660
|
+
for check, passed in required_checks.items():
|
|
2631
2661
|
icon = "✅" if passed else "❌"
|
|
2632
2662
|
console.print(f" {icon} {check}")
|
|
2633
2663
|
|
|
@@ -15,6 +15,7 @@ import tempfile
|
|
|
15
15
|
import time
|
|
16
16
|
import urllib.request
|
|
17
17
|
import uuid
|
|
18
|
+
import zlib
|
|
18
19
|
import xml.etree.ElementTree as ET
|
|
19
20
|
from dataclasses import dataclass, field
|
|
20
21
|
from datetime import datetime
|
|
@@ -299,20 +300,29 @@ class SelectiveVMCloner:
|
|
|
299
300
|
return mode
|
|
300
301
|
return "default"
|
|
301
302
|
|
|
302
|
-
def check_prerequisites(self) -> dict:
|
|
303
|
+
def check_prerequisites(self, config: Optional[VMConfig] = None) -> dict:
|
|
303
304
|
"""Check system prerequisites for VM creation."""
|
|
304
305
|
images_dir = self.get_images_dir()
|
|
305
306
|
|
|
307
|
+
resolved_network_mode: Optional[str] = None
|
|
308
|
+
if config is not None:
|
|
309
|
+
try:
|
|
310
|
+
resolved_network_mode = self.resolve_network_mode(config)
|
|
311
|
+
except Exception:
|
|
312
|
+
resolved_network_mode = None
|
|
313
|
+
|
|
306
314
|
checks = {
|
|
307
315
|
"libvirt_connected": False,
|
|
308
316
|
"kvm_available": False,
|
|
309
317
|
"default_network": False,
|
|
318
|
+
"default_network_required": True,
|
|
310
319
|
"images_dir_writable": False,
|
|
311
320
|
"images_dir": str(images_dir),
|
|
312
321
|
"session_type": "user" if self.user_session else "system",
|
|
313
322
|
"genisoimage_installed": False,
|
|
314
323
|
"virt_viewer_installed": False,
|
|
315
324
|
"qemu_img_installed": False,
|
|
325
|
+
"passt_installed": shutil.which("passt") is not None,
|
|
316
326
|
}
|
|
317
327
|
|
|
318
328
|
# Check for genisoimage
|
|
@@ -340,8 +350,14 @@ class SelectiveVMCloner:
|
|
|
340
350
|
|
|
341
351
|
# Check default network
|
|
342
352
|
default_net_state = self._default_network_state()
|
|
343
|
-
checks["
|
|
344
|
-
if
|
|
353
|
+
checks["default_network_required"] = (resolved_network_mode or "default") != "user"
|
|
354
|
+
if checks["default_network_required"]:
|
|
355
|
+
checks["default_network"] = default_net_state == "active"
|
|
356
|
+
else:
|
|
357
|
+
# For user-mode networking (slirp/passt), libvirt's default network is not required.
|
|
358
|
+
checks["default_network"] = True
|
|
359
|
+
|
|
360
|
+
if checks["default_network_required"] and default_net_state in {"inactive", "missing", "unknown"}:
|
|
345
361
|
checks["network_error"] = (
|
|
346
362
|
"Default network not found or inactive.\n"
|
|
347
363
|
" For user session, CloneBox can use user-mode networking (slirp) automatically.\n"
|
|
@@ -351,6 +367,9 @@ class SelectiveVMCloner:
|
|
|
351
367
|
" Or use system session: clonebox clone . (without --user)\n"
|
|
352
368
|
)
|
|
353
369
|
|
|
370
|
+
if resolved_network_mode is not None:
|
|
371
|
+
checks["network_mode"] = resolved_network_mode
|
|
372
|
+
|
|
354
373
|
# Check images directory
|
|
355
374
|
if images_dir.exists():
|
|
356
375
|
checks["images_dir_writable"] = os.access(images_dir, os.W_OK)
|
|
@@ -1358,6 +1377,13 @@ fi
|
|
|
1358
1377
|
if network_mode == "user":
|
|
1359
1378
|
iface = ET.SubElement(devices, "interface", type="user")
|
|
1360
1379
|
ET.SubElement(iface, "model", type="virtio")
|
|
1380
|
+
|
|
1381
|
+
if shutil.which("passt"):
|
|
1382
|
+
ET.SubElement(iface, "backend", type="passt")
|
|
1383
|
+
|
|
1384
|
+
ssh_port = 22000 + (zlib.crc32(config.name.encode("utf-8")) % 1000)
|
|
1385
|
+
pf = ET.SubElement(iface, "portForward", proto="tcp", address="127.0.0.1")
|
|
1386
|
+
ET.SubElement(pf, "range", start=str(ssh_port), to="22")
|
|
1361
1387
|
else:
|
|
1362
1388
|
iface = ET.SubElement(devices, "interface", type="network")
|
|
1363
1389
|
ET.SubElement(iface, "source", network="default")
|
|
@@ -1398,6 +1424,7 @@ fi
|
|
|
1398
1424
|
|
|
1399
1425
|
# Channel for guest agent
|
|
1400
1426
|
channel = ET.SubElement(devices, "channel", type="unix")
|
|
1427
|
+
# For both user and system sessions, let libvirt handle the path
|
|
1401
1428
|
ET.SubElement(channel, "source", mode="bind")
|
|
1402
1429
|
ET.SubElement(channel, "target", type="virtio", name="org.qemu.guest_agent.0")
|
|
1403
1430
|
|
|
@@ -1563,7 +1590,7 @@ fi
|
|
|
1563
1590
|
|
|
1564
1591
|
# User-data
|
|
1565
1592
|
# Add desktop environment if GUI is enabled
|
|
1566
|
-
base_packages = ["qemu-guest-agent", "cloud-guest-utils"]
|
|
1593
|
+
base_packages = ["qemu-guest-agent", "cloud-guest-utils", "openssh-server"]
|
|
1567
1594
|
if config.gui:
|
|
1568
1595
|
base_packages.extend(
|
|
1569
1596
|
[
|
|
@@ -1619,9 +1646,11 @@ fi
|
|
|
1619
1646
|
|
|
1620
1647
|
# Phase 3: Core services
|
|
1621
1648
|
runcmd_lines.append(" - echo '[3/10] 🔧 Enabling core services...'")
|
|
1622
|
-
runcmd_lines.append(" - echo ' → [1/
|
|
1649
|
+
runcmd_lines.append(" - echo ' → [1/3] Enabling qemu-guest-agent'")
|
|
1623
1650
|
runcmd_lines.append(" - systemctl enable --now qemu-guest-agent || echo ' → ❌ Failed to enable qemu-guest-agent'")
|
|
1624
|
-
runcmd_lines.append(" - echo ' → [2/
|
|
1651
|
+
runcmd_lines.append(" - echo ' → [2/3] Enabling SSH server'")
|
|
1652
|
+
runcmd_lines.append(" - systemctl enable --now ssh || systemctl enable --now sshd || echo ' → ❌ Failed to enable ssh'")
|
|
1653
|
+
runcmd_lines.append(" - echo ' → [3/3] Enabling snapd'")
|
|
1625
1654
|
runcmd_lines.append(" - systemctl enable --now snapd || echo ' → ❌ Failed to enable snapd'")
|
|
1626
1655
|
runcmd_lines.append(" - echo ' → Waiting for snap system seed...'")
|
|
1627
1656
|
runcmd_lines.append(" - timeout 300 snap wait system seed.loaded || true")
|
|
@@ -6,6 +6,7 @@ import subprocess
|
|
|
6
6
|
import json
|
|
7
7
|
import base64
|
|
8
8
|
import time
|
|
9
|
+
import zlib
|
|
9
10
|
from typing import Dict, List, Tuple, Optional
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from rich.console import Console
|
|
@@ -49,9 +50,66 @@ class VMValidator:
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
self._setup_in_progress_cache: Optional[bool] = None
|
|
53
|
+
self._exec_transport: str = "qga" # qga|ssh
|
|
54
|
+
|
|
55
|
+
def _get_ssh_key_path(self) -> Optional[Path]:
|
|
56
|
+
"""Return path to the SSH key generated for this VM (if present)."""
|
|
57
|
+
try:
|
|
58
|
+
if self.conn_uri.endswith("/session"):
|
|
59
|
+
images_dir = Path.home() / ".local/share/libvirt/images"
|
|
60
|
+
else:
|
|
61
|
+
images_dir = Path("/var/lib/libvirt/images")
|
|
62
|
+
key_path = images_dir / self.vm_name / "ssh_key"
|
|
63
|
+
return key_path if key_path.exists() else None
|
|
64
|
+
except Exception:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def _get_ssh_port(self) -> int:
|
|
68
|
+
"""Deterministic host-side SSH port for passt port forwarding."""
|
|
69
|
+
return 22000 + (zlib.crc32(self.vm_name.encode("utf-8")) % 1000)
|
|
70
|
+
|
|
71
|
+
def _ssh_exec(self, command: str, timeout: int = 10) -> Optional[str]:
|
|
72
|
+
key_path = self._get_ssh_key_path()
|
|
73
|
+
if key_path is None:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
ssh_port = self._get_ssh_port()
|
|
77
|
+
user = (self.config.get("vm") or {}).get("username") or "ubuntu"
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
result = subprocess.run(
|
|
81
|
+
[
|
|
82
|
+
"ssh",
|
|
83
|
+
"-i",
|
|
84
|
+
str(key_path),
|
|
85
|
+
"-p",
|
|
86
|
+
str(ssh_port),
|
|
87
|
+
"-o",
|
|
88
|
+
"StrictHostKeyChecking=no",
|
|
89
|
+
"-o",
|
|
90
|
+
"UserKnownHostsFile=/dev/null",
|
|
91
|
+
"-o",
|
|
92
|
+
"ConnectTimeout=5",
|
|
93
|
+
"-o",
|
|
94
|
+
"BatchMode=yes",
|
|
95
|
+
f"{user}@127.0.0.1",
|
|
96
|
+
command,
|
|
97
|
+
],
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
timeout=timeout,
|
|
101
|
+
)
|
|
102
|
+
if result.returncode != 0:
|
|
103
|
+
return None
|
|
104
|
+
return (result.stdout or "").strip()
|
|
105
|
+
except Exception:
|
|
106
|
+
return None
|
|
52
107
|
|
|
53
108
|
def _exec_in_vm(self, command: str, timeout: int = 10) -> Optional[str]:
|
|
54
|
-
"""Execute command in VM using QEMU guest agent."""
|
|
109
|
+
"""Execute command in VM using QEMU guest agent, with SSH fallback."""
|
|
110
|
+
if self._exec_transport == "ssh":
|
|
111
|
+
return self._ssh_exec(command, timeout=timeout)
|
|
112
|
+
|
|
55
113
|
try:
|
|
56
114
|
# Execute command
|
|
57
115
|
result = subprocess.run(
|
|
@@ -1170,17 +1228,24 @@ class VMValidator:
|
|
|
1170
1228
|
last_log = elapsed
|
|
1171
1229
|
|
|
1172
1230
|
if not self._check_qga_ready():
|
|
1173
|
-
|
|
1174
|
-
self.console.print("
|
|
1175
|
-
self.
|
|
1176
|
-
self.
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1231
|
+
# SSH fallback (primarily for --user networking, where passt can forward ports)
|
|
1232
|
+
self.console.print("[yellow]⚠️ QEMU Guest Agent not responding - trying SSH fallback...[/]")
|
|
1233
|
+
self._exec_transport = "ssh"
|
|
1234
|
+
smoke = self._ssh_exec("echo ok", timeout=10)
|
|
1235
|
+
if smoke != "ok":
|
|
1236
|
+
self.console.print("[red]❌ SSH fallback failed[/]")
|
|
1237
|
+
self.console.print("\n[bold]🔧 Troubleshooting QGA:[/]")
|
|
1238
|
+
self.console.print(" 1. The VM might still be booting. Wait 30-60 seconds.")
|
|
1239
|
+
self.console.print(" 2. Ensure the agent is installed and running inside the VM:")
|
|
1240
|
+
self.console.print(" [dim]virsh console " + self.vm_name + "[/]")
|
|
1241
|
+
self.console.print(" [dim]sudo systemctl status qemu-guest-agent[/]")
|
|
1242
|
+
self.console.print(" 3. If newly created, cloud-init might still be running.")
|
|
1243
|
+
self.console.print(" 4. Check VM logs: [dim]clonebox logs " + self.vm_name + "[/]")
|
|
1244
|
+
self.console.print(f"\n[yellow]⚠️ Skipping deep validation as it requires a working Guest Agent or SSH access.[/]")
|
|
1245
|
+
self.results["overall"] = "qga_not_ready"
|
|
1246
|
+
return self.results
|
|
1247
|
+
|
|
1248
|
+
self.console.print("[green]✅ SSH fallback connected (executing validations over SSH)[/]")
|
|
1184
1249
|
|
|
1185
1250
|
ci_status = self._exec_in_vm("cloud-init status --long 2>/dev/null || cloud-init status 2>/dev/null || true", timeout=20)
|
|
1186
1251
|
if ci_status:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|