clonebox 0.1.26__py3-none-any.whl → 0.1.28__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.
- clonebox/cli.py +391 -230
- clonebox/cloner.py +335 -206
- clonebox/dashboard.py +4 -4
- clonebox/detector.py +19 -31
- clonebox/models.py +19 -2
- clonebox/profiles.py +1 -5
- clonebox/validator.py +275 -145
- {clonebox-0.1.26.dist-info → clonebox-0.1.28.dist-info}/METADATA +4 -1
- clonebox-0.1.28.dist-info/RECORD +17 -0
- clonebox-0.1.26.dist-info/RECORD +0 -17
- {clonebox-0.1.26.dist-info → clonebox-0.1.28.dist-info}/WHEEL +0 -0
- {clonebox-0.1.26.dist-info → clonebox-0.1.28.dist-info}/entry_points.txt +0 -0
- {clonebox-0.1.26.dist-info → clonebox-0.1.28.dist-info}/licenses/LICENSE +0 -0
- {clonebox-0.1.26.dist-info → clonebox-0.1.28.dist-info}/top_level.txt +0 -0
clonebox/cloner.py
CHANGED
|
@@ -16,6 +16,7 @@ from typing import Optional
|
|
|
16
16
|
|
|
17
17
|
try:
|
|
18
18
|
from dotenv import load_dotenv
|
|
19
|
+
|
|
19
20
|
load_dotenv()
|
|
20
21
|
except ImportError:
|
|
21
22
|
pass # dotenv is optional
|
|
@@ -26,14 +27,42 @@ except ImportError:
|
|
|
26
27
|
libvirt = None
|
|
27
28
|
|
|
28
29
|
SNAP_INTERFACES = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
"pycharm-community": [
|
|
31
|
+
"desktop",
|
|
32
|
+
"desktop-legacy",
|
|
33
|
+
"x11",
|
|
34
|
+
"wayland",
|
|
35
|
+
"home",
|
|
36
|
+
"network",
|
|
37
|
+
"network-bind",
|
|
38
|
+
"cups-control",
|
|
39
|
+
"removable-media",
|
|
40
|
+
],
|
|
41
|
+
"chromium": [
|
|
42
|
+
"desktop",
|
|
43
|
+
"desktop-legacy",
|
|
44
|
+
"x11",
|
|
45
|
+
"wayland",
|
|
46
|
+
"home",
|
|
47
|
+
"network",
|
|
48
|
+
"audio-playback",
|
|
49
|
+
"camera",
|
|
50
|
+
],
|
|
51
|
+
"firefox": [
|
|
52
|
+
"desktop",
|
|
53
|
+
"desktop-legacy",
|
|
54
|
+
"x11",
|
|
55
|
+
"wayland",
|
|
56
|
+
"home",
|
|
57
|
+
"network",
|
|
58
|
+
"audio-playback",
|
|
59
|
+
"removable-media",
|
|
60
|
+
],
|
|
61
|
+
"code": ["desktop", "desktop-legacy", "x11", "wayland", "home", "network", "ssh-keys"],
|
|
62
|
+
"slack": ["desktop", "desktop-legacy", "x11", "wayland", "home", "network", "audio-playback"],
|
|
63
|
+
"spotify": ["desktop", "x11", "wayland", "home", "network", "audio-playback"],
|
|
35
64
|
}
|
|
36
|
-
DEFAULT_SNAP_INTERFACES = [
|
|
65
|
+
DEFAULT_SNAP_INTERFACES = ["desktop", "desktop-legacy", "x11", "home", "network"]
|
|
37
66
|
|
|
38
67
|
|
|
39
68
|
@dataclass
|
|
@@ -51,11 +80,21 @@ class VMConfig:
|
|
|
51
80
|
snap_packages: list = field(default_factory=list) # Snap packages to install
|
|
52
81
|
services: list = field(default_factory=list)
|
|
53
82
|
post_commands: list = field(default_factory=list) # Commands to run after setup
|
|
54
|
-
user_session: bool = field(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
user_session: bool = field(
|
|
84
|
+
default_factory=lambda: os.getenv("VM_USER_SESSION", "false").lower() == "true"
|
|
85
|
+
) # Use qemu:///session instead of qemu:///system
|
|
86
|
+
network_mode: str = field(
|
|
87
|
+
default_factory=lambda: os.getenv("VM_NETWORK_MODE", "auto")
|
|
88
|
+
) # auto|default|user
|
|
89
|
+
username: str = field(
|
|
90
|
+
default_factory=lambda: os.getenv("VM_USERNAME", "ubuntu")
|
|
91
|
+
) # VM default username
|
|
92
|
+
password: str = field(
|
|
93
|
+
default_factory=lambda: os.getenv("VM_PASSWORD", "ubuntu")
|
|
94
|
+
) # VM default password
|
|
95
|
+
autostart_apps: bool = field(
|
|
96
|
+
default_factory=lambda: os.getenv("VM_AUTOSTART_APPS", "true").lower() == "true"
|
|
97
|
+
) # Auto-start GUI apps after login (desktop autostart)
|
|
59
98
|
web_services: list = field(default_factory=list) # Web services to start (uvicorn, etc.)
|
|
60
99
|
|
|
61
100
|
def to_dict(self) -> dict:
|
|
@@ -87,21 +126,20 @@ class SelectiveVMCloner:
|
|
|
87
126
|
|
|
88
127
|
@property
|
|
89
128
|
def USER_IMAGES_DIR(self) -> Path:
|
|
90
|
-
return Path(
|
|
129
|
+
return Path(
|
|
130
|
+
os.getenv("CLONEBOX_USER_IMAGES_DIR", str(Path.home() / ".local/share/libvirt/images"))
|
|
131
|
+
).expanduser()
|
|
91
132
|
|
|
92
133
|
@property
|
|
93
134
|
def DEFAULT_BASE_IMAGE_URL(self) -> str:
|
|
94
135
|
return os.getenv(
|
|
95
136
|
"CLONEBOX_BASE_IMAGE_URL",
|
|
96
|
-
"https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img"
|
|
137
|
+
"https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img",
|
|
97
138
|
)
|
|
98
139
|
|
|
99
140
|
@property
|
|
100
141
|
def DEFAULT_BASE_IMAGE_FILENAME(self) -> str:
|
|
101
|
-
return os.getenv(
|
|
102
|
-
"CLONEBOX_BASE_IMAGE_FILENAME",
|
|
103
|
-
"clonebox-ubuntu-jammy-amd64.qcow2"
|
|
104
|
-
)
|
|
142
|
+
return os.getenv("CLONEBOX_BASE_IMAGE_FILENAME", "clonebox-ubuntu-jammy-amd64.qcow2")
|
|
105
143
|
|
|
106
144
|
def _connect(self):
|
|
107
145
|
"""Connect to libvirt."""
|
|
@@ -402,10 +440,20 @@ class SelectiveVMCloner:
|
|
|
402
440
|
return vm.UUIDString()
|
|
403
441
|
|
|
404
442
|
def _generate_vm_xml(
|
|
405
|
-
self, config: VMConfig, root_disk: Path, cloudinit_iso: Optional[Path]
|
|
443
|
+
self, config: VMConfig = None, root_disk: Path = None, cloudinit_iso: Optional[Path] = None
|
|
406
444
|
) -> str:
|
|
407
445
|
"""Generate libvirt XML for the VM."""
|
|
408
446
|
|
|
447
|
+
# Backward compatibility: if called without args, try to derive defaults
|
|
448
|
+
if config is None:
|
|
449
|
+
# Create a default config for backward compatibility
|
|
450
|
+
config = VMConfig()
|
|
451
|
+
if root_disk is None:
|
|
452
|
+
# Use a default path for backward compatibility
|
|
453
|
+
root_disk = Path("/var/lib/libvirt/images/default-disk.qcow2")
|
|
454
|
+
if cloudinit_iso is None:
|
|
455
|
+
cloudinit_iso = None
|
|
456
|
+
|
|
409
457
|
root = ET.Element("domain", type="kvm")
|
|
410
458
|
|
|
411
459
|
# Basic metadata
|
|
@@ -527,15 +575,17 @@ class SelectiveVMCloner:
|
|
|
527
575
|
apt_pkg_list.append(gui_pkg)
|
|
528
576
|
|
|
529
577
|
apt_packages = " ".join(f'"{p}"' for p in apt_pkg_list) if apt_pkg_list else ""
|
|
530
|
-
snap_packages =
|
|
578
|
+
snap_packages = (
|
|
579
|
+
" ".join(f'"{p}"' for p in config.snap_packages) if config.snap_packages else ""
|
|
580
|
+
)
|
|
531
581
|
services = " ".join(f'"{s}"' for s in config.services) if config.services else ""
|
|
532
|
-
|
|
582
|
+
|
|
533
583
|
snap_ifaces_bash = "\n".join(
|
|
534
584
|
f'SNAP_INTERFACES["{snap}"]="{" ".join(ifaces)}"'
|
|
535
585
|
for snap, ifaces in SNAP_INTERFACES.items()
|
|
536
586
|
)
|
|
537
|
-
|
|
538
|
-
script = f
|
|
587
|
+
|
|
588
|
+
script = f"""#!/bin/bash
|
|
539
589
|
set -uo pipefail
|
|
540
590
|
LOG="/var/log/clonebox-boot.log"
|
|
541
591
|
STATUS_KV="/var/run/clonebox-status"
|
|
@@ -877,36 +927,40 @@ else
|
|
|
877
927
|
log "${{RED}}${{BOLD}}═══════════════════════════════════════════════════════════${{NC}}"
|
|
878
928
|
exit 1
|
|
879
929
|
fi
|
|
880
|
-
|
|
930
|
+
"""
|
|
881
931
|
return base64.b64encode(script.encode()).decode()
|
|
882
932
|
|
|
883
933
|
def _generate_health_check_script(self, config: VMConfig) -> str:
|
|
884
934
|
"""Generate a health check script that validates all installed components."""
|
|
885
935
|
import base64
|
|
886
|
-
|
|
936
|
+
|
|
887
937
|
# Build package check commands
|
|
888
938
|
apt_checks = []
|
|
889
939
|
for pkg in config.packages:
|
|
890
940
|
apt_checks.append(f'check_apt_package "{pkg}"')
|
|
891
|
-
|
|
941
|
+
|
|
892
942
|
snap_checks = []
|
|
893
943
|
for pkg in config.snap_packages:
|
|
894
944
|
snap_checks.append(f'check_snap_package "{pkg}"')
|
|
895
|
-
|
|
945
|
+
|
|
896
946
|
service_checks = []
|
|
897
947
|
for svc in config.services:
|
|
898
948
|
service_checks.append(f'check_service "{svc}"')
|
|
899
|
-
|
|
949
|
+
|
|
900
950
|
mount_checks = []
|
|
901
951
|
for idx, (host_path, guest_path) in enumerate(config.paths.items()):
|
|
902
952
|
mount_checks.append(f'check_mount "{guest_path}" "mount{idx}"')
|
|
903
|
-
|
|
953
|
+
|
|
904
954
|
apt_checks_str = "\n".join(apt_checks) if apt_checks else "echo 'No apt packages to check'"
|
|
905
|
-
snap_checks_str =
|
|
906
|
-
|
|
955
|
+
snap_checks_str = (
|
|
956
|
+
"\n".join(snap_checks) if snap_checks else "echo 'No snap packages to check'"
|
|
957
|
+
)
|
|
958
|
+
service_checks_str = (
|
|
959
|
+
"\n".join(service_checks) if service_checks else "echo 'No services to check'"
|
|
960
|
+
)
|
|
907
961
|
mount_checks_str = "\n".join(mount_checks) if mount_checks else "echo 'No mounts to check'"
|
|
908
|
-
|
|
909
|
-
script = f
|
|
962
|
+
|
|
963
|
+
script = f"""#!/bin/bash
|
|
910
964
|
# CloneBox Health Check Script
|
|
911
965
|
# Generated automatically - validates all installed components
|
|
912
966
|
|
|
@@ -1048,7 +1102,7 @@ else
|
|
|
1048
1102
|
echo "HEALTH_STATUS=FAILED" > /var/log/clonebox-health-status
|
|
1049
1103
|
exit 1
|
|
1050
1104
|
fi
|
|
1051
|
-
|
|
1105
|
+
"""
|
|
1052
1106
|
# Encode script to base64 for safe embedding in cloud-init
|
|
1053
1107
|
encoded = base64.b64encode(script.encode()).decode()
|
|
1054
1108
|
return encoded
|
|
@@ -1075,9 +1129,7 @@ fi
|
|
|
1075
1129
|
mount_opts = "trans=virtio,version=9p2000.L,mmap,uid=1000,gid=1000,users"
|
|
1076
1130
|
mount_commands.append(f" - mkdir -p {guest_path}")
|
|
1077
1131
|
mount_commands.append(f" - chown 1000:1000 {guest_path}")
|
|
1078
|
-
mount_commands.append(
|
|
1079
|
-
f" - mount -t 9p -o {mount_opts} {tag} {guest_path} || true"
|
|
1080
|
-
)
|
|
1132
|
+
mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {guest_path} || true")
|
|
1081
1133
|
# Add fstab entry for persistence after reboot
|
|
1082
1134
|
fstab_entries.append(f"{tag} {guest_path} 9p {mount_opts},nofail 0 0")
|
|
1083
1135
|
|
|
@@ -1085,79 +1137,93 @@ fi
|
|
|
1085
1137
|
# Add desktop environment if GUI is enabled
|
|
1086
1138
|
base_packages = ["qemu-guest-agent", "cloud-guest-utils"]
|
|
1087
1139
|
if config.gui:
|
|
1088
|
-
base_packages.extend(
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1140
|
+
base_packages.extend(
|
|
1141
|
+
[
|
|
1142
|
+
"ubuntu-desktop-minimal",
|
|
1143
|
+
"firefox",
|
|
1144
|
+
]
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1093
1147
|
all_packages = base_packages + list(config.packages)
|
|
1094
|
-
packages_yaml = (
|
|
1095
|
-
|
|
1096
|
-
)
|
|
1097
|
-
|
|
1148
|
+
packages_yaml = "\n".join(f" - {pkg}" for pkg in all_packages) if all_packages else ""
|
|
1149
|
+
|
|
1098
1150
|
# Build runcmd - services, mounts, snaps, post_commands
|
|
1099
1151
|
runcmd_lines = []
|
|
1100
1152
|
|
|
1101
1153
|
runcmd_lines.append(" - systemctl enable --now qemu-guest-agent || true")
|
|
1102
1154
|
runcmd_lines.append(" - systemctl enable --now snapd || true")
|
|
1103
1155
|
runcmd_lines.append(" - timeout 300 snap wait system seed.loaded || true")
|
|
1104
|
-
|
|
1156
|
+
|
|
1105
1157
|
# Add service enablement
|
|
1106
1158
|
for svc in config.services:
|
|
1107
1159
|
runcmd_lines.append(f" - systemctl enable --now {svc} || true")
|
|
1108
|
-
|
|
1160
|
+
|
|
1109
1161
|
# Add fstab entries for persistent mounts after reboot
|
|
1110
1162
|
if fstab_entries:
|
|
1111
|
-
runcmd_lines.append(
|
|
1163
|
+
runcmd_lines.append(
|
|
1164
|
+
" - grep -q '^# CloneBox 9p mounts' /etc/fstab || echo '# CloneBox 9p mounts' >> /etc/fstab"
|
|
1165
|
+
)
|
|
1112
1166
|
for entry in fstab_entries:
|
|
1113
|
-
runcmd_lines.append(
|
|
1167
|
+
runcmd_lines.append(
|
|
1168
|
+
f" - grep -qF \"{entry}\" /etc/fstab || echo '{entry}' >> /etc/fstab"
|
|
1169
|
+
)
|
|
1114
1170
|
runcmd_lines.append(" - mount -a || true")
|
|
1115
|
-
|
|
1171
|
+
|
|
1116
1172
|
# Add mounts (immediate, before reboot)
|
|
1117
1173
|
for cmd in mount_commands:
|
|
1118
1174
|
runcmd_lines.append(cmd)
|
|
1119
|
-
|
|
1175
|
+
|
|
1120
1176
|
# Install snap packages
|
|
1121
1177
|
if config.snap_packages:
|
|
1122
1178
|
runcmd_lines.append(" - echo 'Installing snap packages...'")
|
|
1123
1179
|
for snap_pkg in config.snap_packages:
|
|
1124
|
-
runcmd_lines.append(
|
|
1125
|
-
|
|
1180
|
+
runcmd_lines.append(
|
|
1181
|
+
f" - snap install {snap_pkg} --classic || snap install {snap_pkg} || true"
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1126
1184
|
# Connect snap interfaces for GUI apps (not auto-connected via cloud-init)
|
|
1127
1185
|
runcmd_lines.append(" - echo 'Connecting snap interfaces...'")
|
|
1128
1186
|
for snap_pkg in config.snap_packages:
|
|
1129
1187
|
interfaces = SNAP_INTERFACES.get(snap_pkg, DEFAULT_SNAP_INTERFACES)
|
|
1130
1188
|
for iface in interfaces:
|
|
1131
|
-
runcmd_lines.append(
|
|
1189
|
+
runcmd_lines.append(
|
|
1190
|
+
f" - snap connect {snap_pkg}:{iface} :{iface} 2>/dev/null || true"
|
|
1191
|
+
)
|
|
1132
1192
|
|
|
1133
1193
|
runcmd_lines.append(" - systemctl restart snapd || true")
|
|
1134
|
-
|
|
1194
|
+
|
|
1135
1195
|
# Add GUI setup if enabled - runs AFTER package installation completes
|
|
1136
1196
|
if config.gui:
|
|
1137
1197
|
# Create directories that GNOME services need BEFORE GUI starts
|
|
1138
1198
|
# These may conflict with mounted host directories, so ensure they exist with correct perms
|
|
1139
|
-
runcmd_lines.extend(
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1199
|
+
runcmd_lines.extend(
|
|
1200
|
+
[
|
|
1201
|
+
" - mkdir -p /home/ubuntu/.config/pulse /home/ubuntu/.cache/ibus /home/ubuntu/.local/share",
|
|
1202
|
+
" - mkdir -p /home/ubuntu/.config/dconf /home/ubuntu/.cache/tracker3",
|
|
1203
|
+
" - mkdir -p /home/ubuntu/.config/autostart",
|
|
1204
|
+
" - chown -R 1000:1000 /home/ubuntu/.config /home/ubuntu/.cache /home/ubuntu/.local",
|
|
1205
|
+
" - chmod 700 /home/ubuntu/.config /home/ubuntu/.cache",
|
|
1206
|
+
" - systemctl set-default graphical.target",
|
|
1207
|
+
" - systemctl enable gdm3 || systemctl enable gdm || true",
|
|
1208
|
+
]
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1149
1211
|
# Create autostart entries for GUI apps
|
|
1150
1212
|
autostart_apps = {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1213
|
+
"pycharm-community": (
|
|
1214
|
+
"PyCharm Community",
|
|
1215
|
+
"/snap/bin/pycharm-community",
|
|
1216
|
+
"pycharm-community",
|
|
1217
|
+
),
|
|
1218
|
+
"firefox": ("Firefox", "/snap/bin/firefox", "firefox"),
|
|
1219
|
+
"chromium": ("Chromium", "/snap/bin/chromium", "chromium"),
|
|
1220
|
+
"google-chrome": ("Google Chrome", "google-chrome-stable", "google-chrome"),
|
|
1155
1221
|
}
|
|
1156
|
-
|
|
1222
|
+
|
|
1157
1223
|
for snap_pkg in config.snap_packages:
|
|
1158
1224
|
if snap_pkg in autostart_apps:
|
|
1159
1225
|
name, exec_cmd, icon = autostart_apps[snap_pkg]
|
|
1160
|
-
desktop_entry = f
|
|
1226
|
+
desktop_entry = f"""[Desktop Entry]
|
|
1161
1227
|
Type=Application
|
|
1162
1228
|
Name={name}
|
|
1163
1229
|
Exec={exec_cmd}
|
|
@@ -1165,16 +1231,19 @@ Icon={icon}
|
|
|
1165
1231
|
X-GNOME-Autostart-enabled=true
|
|
1166
1232
|
X-GNOME-Autostart-Delay=5
|
|
1167
1233
|
Comment=CloneBox autostart
|
|
1168
|
-
|
|
1234
|
+
"""
|
|
1169
1235
|
import base64
|
|
1236
|
+
|
|
1170
1237
|
desktop_b64 = base64.b64encode(desktop_entry.encode()).decode()
|
|
1171
|
-
runcmd_lines.append(
|
|
1172
|
-
|
|
1238
|
+
runcmd_lines.append(
|
|
1239
|
+
f" - echo '{desktop_b64}' | base64 -d > /home/ubuntu/.config/autostart/{snap_pkg}.desktop"
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1173
1242
|
# Check if google-chrome is in paths (app_data_paths)
|
|
1174
|
-
wants_chrome = any(
|
|
1243
|
+
wants_chrome = any("/google-chrome" in str(p) for p in (config.paths or {}).values())
|
|
1175
1244
|
if wants_chrome:
|
|
1176
|
-
name, exec_cmd, icon = autostart_apps[
|
|
1177
|
-
desktop_entry = f
|
|
1245
|
+
name, exec_cmd, icon = autostart_apps["google-chrome"]
|
|
1246
|
+
desktop_entry = f"""[Desktop Entry]
|
|
1178
1247
|
Type=Application
|
|
1179
1248
|
Name={name}
|
|
1180
1249
|
Exec={exec_cmd}
|
|
@@ -1182,33 +1251,41 @@ Icon={icon}
|
|
|
1182
1251
|
X-GNOME-Autostart-enabled=true
|
|
1183
1252
|
X-GNOME-Autostart-Delay=5
|
|
1184
1253
|
Comment=CloneBox autostart
|
|
1185
|
-
|
|
1254
|
+
"""
|
|
1186
1255
|
desktop_b64 = base64.b64encode(desktop_entry.encode()).decode()
|
|
1187
|
-
runcmd_lines.append(
|
|
1188
|
-
|
|
1256
|
+
runcmd_lines.append(
|
|
1257
|
+
f" - echo '{desktop_b64}' | base64 -d > /home/ubuntu/.config/autostart/google-chrome.desktop"
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1189
1260
|
# Fix ownership of autostart directory
|
|
1190
1261
|
runcmd_lines.append(" - chown -R 1000:1000 /home/ubuntu/.config/autostart")
|
|
1191
|
-
|
|
1262
|
+
|
|
1192
1263
|
# Run user-defined post commands
|
|
1193
1264
|
if config.post_commands:
|
|
1194
1265
|
runcmd_lines.append(" - echo 'Running post-setup commands...'")
|
|
1195
1266
|
for cmd in config.post_commands:
|
|
1196
1267
|
runcmd_lines.append(f" - {cmd}")
|
|
1197
|
-
|
|
1268
|
+
|
|
1198
1269
|
# Generate health check script
|
|
1199
1270
|
health_script = self._generate_health_check_script(config)
|
|
1200
|
-
runcmd_lines.append(
|
|
1271
|
+
runcmd_lines.append(
|
|
1272
|
+
f" - echo '{health_script}' | base64 -d > /usr/local/bin/clonebox-health"
|
|
1273
|
+
)
|
|
1201
1274
|
runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-health")
|
|
1202
|
-
runcmd_lines.append(
|
|
1275
|
+
runcmd_lines.append(
|
|
1276
|
+
" - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1"
|
|
1277
|
+
)
|
|
1203
1278
|
runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
|
|
1204
|
-
|
|
1279
|
+
|
|
1205
1280
|
# Generate boot diagnostic script (self-healing)
|
|
1206
1281
|
boot_diag_script = self._generate_boot_diagnostic_script(config)
|
|
1207
|
-
runcmd_lines.append(
|
|
1282
|
+
runcmd_lines.append(
|
|
1283
|
+
f" - echo '{boot_diag_script}' | base64 -d > /usr/local/bin/clonebox-boot-diagnostic"
|
|
1284
|
+
)
|
|
1208
1285
|
runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-boot-diagnostic")
|
|
1209
|
-
|
|
1286
|
+
|
|
1210
1287
|
# Create systemd service for boot diagnostic (runs before GDM on subsequent boots)
|
|
1211
|
-
systemd_service =
|
|
1288
|
+
systemd_service = """[Unit]
|
|
1212
1289
|
Description=CloneBox Boot Diagnostic
|
|
1213
1290
|
After=network-online.target snapd.service
|
|
1214
1291
|
Before=gdm.service display-manager.service
|
|
@@ -1226,14 +1303,17 @@ RemainAfterExit=yes
|
|
|
1226
1303
|
TimeoutStartSec=600
|
|
1227
1304
|
|
|
1228
1305
|
[Install]
|
|
1229
|
-
WantedBy=multi-user.target
|
|
1306
|
+
WantedBy=multi-user.target"""
|
|
1230
1307
|
import base64
|
|
1308
|
+
|
|
1231
1309
|
systemd_b64 = base64.b64encode(systemd_service.encode()).decode()
|
|
1232
|
-
runcmd_lines.append(
|
|
1310
|
+
runcmd_lines.append(
|
|
1311
|
+
f" - echo '{systemd_b64}' | base64 -d > /etc/systemd/system/clonebox-diagnostic.service"
|
|
1312
|
+
)
|
|
1233
1313
|
runcmd_lines.append(" - systemctl daemon-reload")
|
|
1234
1314
|
runcmd_lines.append(" - systemctl enable clonebox-diagnostic.service")
|
|
1235
1315
|
runcmd_lines.append(" - systemctl start clonebox-diagnostic.service || true")
|
|
1236
|
-
|
|
1316
|
+
|
|
1237
1317
|
# Create MOTD banner
|
|
1238
1318
|
motd_banner = '''#!/bin/bash
|
|
1239
1319
|
S="/var/run/clonebox-status"
|
|
@@ -1256,9 +1336,9 @@ echo ""'''
|
|
|
1256
1336
|
motd_b64 = base64.b64encode(motd_banner.encode()).decode()
|
|
1257
1337
|
runcmd_lines.append(f" - echo '{motd_b64}' | base64 -d > /etc/update-motd.d/99-clonebox")
|
|
1258
1338
|
runcmd_lines.append(" - chmod +x /etc/update-motd.d/99-clonebox")
|
|
1259
|
-
|
|
1339
|
+
|
|
1260
1340
|
# Create user-friendly clonebox-repair script
|
|
1261
|
-
repair_script = r
|
|
1341
|
+
repair_script = r"""#!/bin/bash
|
|
1262
1342
|
# CloneBox Repair - User-friendly repair utility for CloneBox VMs
|
|
1263
1343
|
# Usage: clonebox-repair [--auto|--status|--logs|--help]
|
|
1264
1344
|
|
|
@@ -1536,96 +1616,110 @@ case "${1:-}" in
|
|
|
1536
1616
|
"") interactive_menu ;;
|
|
1537
1617
|
*) show_help; exit 1 ;;
|
|
1538
1618
|
esac
|
|
1539
|
-
|
|
1619
|
+
"""
|
|
1540
1620
|
repair_b64 = base64.b64encode(repair_script.encode()).decode()
|
|
1541
1621
|
runcmd_lines.append(f" - echo '{repair_b64}' | base64 -d > /usr/local/bin/clonebox-repair")
|
|
1542
1622
|
runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-repair")
|
|
1543
1623
|
runcmd_lines.append(" - ln -sf /usr/local/bin/clonebox-repair /usr/local/bin/cb-repair")
|
|
1544
|
-
|
|
1624
|
+
|
|
1545
1625
|
# === AUTOSTART: Systemd user services + Desktop autostart files ===
|
|
1546
1626
|
# Create directories for user systemd services and autostart
|
|
1547
1627
|
runcmd_lines.append(f" - mkdir -p /home/{config.username}/.config/systemd/user")
|
|
1548
1628
|
runcmd_lines.append(f" - mkdir -p /home/{config.username}/.config/autostart")
|
|
1549
|
-
|
|
1629
|
+
|
|
1550
1630
|
# Enable lingering for the user (allows user services to run without login)
|
|
1551
1631
|
runcmd_lines.append(f" - loginctl enable-linger {config.username}")
|
|
1552
|
-
|
|
1632
|
+
|
|
1553
1633
|
# Add environment variables for monitoring
|
|
1554
|
-
runcmd_lines.extend(
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1634
|
+
runcmd_lines.extend(
|
|
1635
|
+
[
|
|
1636
|
+
" - echo 'CLONEBOX_ENABLE_MONITORING=true' >> /etc/environment",
|
|
1637
|
+
" - echo 'CLONEBOX_MONITOR_INTERVAL=30' >> /etc/environment",
|
|
1638
|
+
" - echo 'CLONEBOX_AUTO_REPAIR=true' >> /etc/environment",
|
|
1639
|
+
" - echo 'CLONEBOX_WATCH_APPS=true' >> /etc/environment",
|
|
1640
|
+
" - echo 'CLONEBOX_WATCH_SERVICES=true' >> /etc/environment",
|
|
1641
|
+
]
|
|
1642
|
+
)
|
|
1643
|
+
|
|
1562
1644
|
# Generate autostart configurations based on installed apps (if enabled)
|
|
1563
1645
|
autostart_apps = []
|
|
1564
|
-
|
|
1565
|
-
if getattr(config,
|
|
1646
|
+
|
|
1647
|
+
if getattr(config, "autostart_apps", True):
|
|
1566
1648
|
# Detect apps from snap_packages
|
|
1567
|
-
for snap_pkg in
|
|
1649
|
+
for snap_pkg in config.snap_packages or []:
|
|
1568
1650
|
if snap_pkg == "pycharm-community":
|
|
1569
|
-
autostart_apps.append(
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1651
|
+
autostart_apps.append(
|
|
1652
|
+
{
|
|
1653
|
+
"name": "pycharm-community",
|
|
1654
|
+
"display_name": "PyCharm Community",
|
|
1655
|
+
"exec": "/snap/bin/pycharm-community %U",
|
|
1656
|
+
"type": "snap",
|
|
1657
|
+
"after": "graphical-session.target",
|
|
1658
|
+
}
|
|
1659
|
+
)
|
|
1576
1660
|
elif snap_pkg == "chromium":
|
|
1577
|
-
autostart_apps.append(
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1661
|
+
autostart_apps.append(
|
|
1662
|
+
{
|
|
1663
|
+
"name": "chromium",
|
|
1664
|
+
"display_name": "Chromium Browser",
|
|
1665
|
+
"exec": "/snap/bin/chromium %U",
|
|
1666
|
+
"type": "snap",
|
|
1667
|
+
"after": "graphical-session.target",
|
|
1668
|
+
}
|
|
1669
|
+
)
|
|
1584
1670
|
elif snap_pkg == "firefox":
|
|
1585
|
-
autostart_apps.append(
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1671
|
+
autostart_apps.append(
|
|
1672
|
+
{
|
|
1673
|
+
"name": "firefox",
|
|
1674
|
+
"display_name": "Firefox",
|
|
1675
|
+
"exec": "/snap/bin/firefox %U",
|
|
1676
|
+
"type": "snap",
|
|
1677
|
+
"after": "graphical-session.target",
|
|
1678
|
+
}
|
|
1679
|
+
)
|
|
1592
1680
|
elif snap_pkg == "code":
|
|
1593
|
-
autostart_apps.append(
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1681
|
+
autostart_apps.append(
|
|
1682
|
+
{
|
|
1683
|
+
"name": "code",
|
|
1684
|
+
"display_name": "Visual Studio Code",
|
|
1685
|
+
"exec": "/snap/bin/code --new-window",
|
|
1686
|
+
"type": "snap",
|
|
1687
|
+
"after": "graphical-session.target",
|
|
1688
|
+
}
|
|
1689
|
+
)
|
|
1690
|
+
|
|
1601
1691
|
# Detect apps from packages (APT)
|
|
1602
|
-
for apt_pkg in
|
|
1692
|
+
for apt_pkg in config.packages or []:
|
|
1603
1693
|
if apt_pkg == "firefox":
|
|
1604
1694
|
# Only add if not already added from snap
|
|
1605
1695
|
if not any(a["name"] == "firefox" for a in autostart_apps):
|
|
1606
|
-
autostart_apps.append(
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1696
|
+
autostart_apps.append(
|
|
1697
|
+
{
|
|
1698
|
+
"name": "firefox",
|
|
1699
|
+
"display_name": "Firefox",
|
|
1700
|
+
"exec": "/usr/bin/firefox %U",
|
|
1701
|
+
"type": "apt",
|
|
1702
|
+
"after": "graphical-session.target",
|
|
1703
|
+
}
|
|
1704
|
+
)
|
|
1705
|
+
|
|
1614
1706
|
# Check for google-chrome from app_data_paths
|
|
1615
1707
|
for host_path, guest_path in (config.paths or {}).items():
|
|
1616
1708
|
if guest_path == "/home/ubuntu/.config/google-chrome":
|
|
1617
|
-
autostart_apps.append(
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1709
|
+
autostart_apps.append(
|
|
1710
|
+
{
|
|
1711
|
+
"name": "google-chrome",
|
|
1712
|
+
"display_name": "Google Chrome",
|
|
1713
|
+
"exec": "/usr/bin/google-chrome-stable %U",
|
|
1714
|
+
"type": "deb",
|
|
1715
|
+
"after": "graphical-session.target",
|
|
1716
|
+
}
|
|
1717
|
+
)
|
|
1624
1718
|
break
|
|
1625
|
-
|
|
1719
|
+
|
|
1626
1720
|
# Generate systemd user services for each app
|
|
1627
1721
|
for app in autostart_apps:
|
|
1628
|
-
service_content = f
|
|
1722
|
+
service_content = f"""[Unit]
|
|
1629
1723
|
Description={app["display_name"]} Autostart
|
|
1630
1724
|
After={app["after"]}
|
|
1631
1725
|
|
|
@@ -1639,14 +1733,14 @@ RestartSec=5
|
|
|
1639
1733
|
|
|
1640
1734
|
[Install]
|
|
1641
1735
|
WantedBy=default.target
|
|
1642
|
-
|
|
1736
|
+
"""
|
|
1643
1737
|
service_b64 = base64.b64encode(service_content.encode()).decode()
|
|
1644
1738
|
service_path = f"/home/{config.username}/.config/systemd/user/{app['name']}.service"
|
|
1645
1739
|
runcmd_lines.append(f" - echo '{service_b64}' | base64 -d > {service_path}")
|
|
1646
|
-
|
|
1740
|
+
|
|
1647
1741
|
# Generate desktop autostart files for GUI apps (alternative to systemd user services)
|
|
1648
1742
|
for app in autostart_apps:
|
|
1649
|
-
desktop_content = f
|
|
1743
|
+
desktop_content = f"""[Desktop Entry]
|
|
1650
1744
|
Type=Application
|
|
1651
1745
|
Name={app["display_name"]}
|
|
1652
1746
|
Exec={app["exec"]}
|
|
@@ -1654,24 +1748,26 @@ Hidden=false
|
|
|
1654
1748
|
NoDisplay=false
|
|
1655
1749
|
X-GNOME-Autostart-enabled=true
|
|
1656
1750
|
X-GNOME-Autostart-Delay=5
|
|
1657
|
-
|
|
1751
|
+
"""
|
|
1658
1752
|
desktop_b64 = base64.b64encode(desktop_content.encode()).decode()
|
|
1659
1753
|
desktop_path = f"/home/{config.username}/.config/autostart/{app['name']}.desktop"
|
|
1660
1754
|
runcmd_lines.append(f" - echo '{desktop_b64}' | base64 -d > {desktop_path}")
|
|
1661
|
-
|
|
1755
|
+
|
|
1662
1756
|
# Fix ownership of all autostart files
|
|
1663
1757
|
runcmd_lines.append(f" - chown -R 1000:1000 /home/{config.username}/.config/systemd")
|
|
1664
1758
|
runcmd_lines.append(f" - chown -R 1000:1000 /home/{config.username}/.config/autostart")
|
|
1665
|
-
|
|
1759
|
+
|
|
1666
1760
|
# Enable systemd user services (must run as user)
|
|
1667
1761
|
if autostart_apps:
|
|
1668
1762
|
services_to_enable = " ".join(f"{app['name']}.service" for app in autostart_apps)
|
|
1669
|
-
runcmd_lines.append(
|
|
1763
|
+
runcmd_lines.append(
|
|
1764
|
+
f" - sudo -u {config.username} XDG_RUNTIME_DIR=/run/user/1000 systemctl --user daemon-reload || true"
|
|
1765
|
+
)
|
|
1670
1766
|
# Note: We don't enable services by default as desktop autostart is more reliable for GUI apps
|
|
1671
1767
|
# User can enable them manually with: systemctl --user enable <service>
|
|
1672
|
-
|
|
1768
|
+
|
|
1673
1769
|
# === WEB SERVICES: System-wide services for uvicorn, nginx, etc. ===
|
|
1674
|
-
web_services = getattr(config,
|
|
1770
|
+
web_services = getattr(config, "web_services", []) or []
|
|
1675
1771
|
for svc in web_services:
|
|
1676
1772
|
svc_name = svc.get("name", "clonebox-web")
|
|
1677
1773
|
svc_desc = svc.get("description", f"CloneBox {svc_name}")
|
|
@@ -1680,10 +1776,10 @@ X-GNOME-Autostart-Delay=5
|
|
|
1680
1776
|
svc_user = svc.get("user", config.username)
|
|
1681
1777
|
svc_after = svc.get("after", "network.target")
|
|
1682
1778
|
svc_env = svc.get("environment", [])
|
|
1683
|
-
|
|
1779
|
+
|
|
1684
1780
|
env_lines = "\n".join(f"Environment={e}" for e in svc_env) if svc_env else ""
|
|
1685
|
-
|
|
1686
|
-
web_service_content = f
|
|
1781
|
+
|
|
1782
|
+
web_service_content = f"""[Unit]
|
|
1687
1783
|
Description={svc_desc}
|
|
1688
1784
|
After={svc_after}
|
|
1689
1785
|
|
|
@@ -1698,13 +1794,15 @@ RestartSec=10
|
|
|
1698
1794
|
|
|
1699
1795
|
[Install]
|
|
1700
1796
|
WantedBy=multi-user.target
|
|
1701
|
-
|
|
1797
|
+
"""
|
|
1702
1798
|
web_svc_b64 = base64.b64encode(web_service_content.encode()).decode()
|
|
1703
|
-
runcmd_lines.append(
|
|
1799
|
+
runcmd_lines.append(
|
|
1800
|
+
f" - echo '{web_svc_b64}' | base64 -d > /etc/systemd/system/{svc_name}.service"
|
|
1801
|
+
)
|
|
1704
1802
|
runcmd_lines.append(" - systemctl daemon-reload")
|
|
1705
1803
|
runcmd_lines.append(f" - systemctl enable {svc_name}.service")
|
|
1706
1804
|
runcmd_lines.append(f" - systemctl start {svc_name}.service || true")
|
|
1707
|
-
|
|
1805
|
+
|
|
1708
1806
|
# Install CloneBox Monitor for continuous monitoring and self-healing
|
|
1709
1807
|
scripts_dir = Path(__file__).resolve().parent.parent.parent / "scripts"
|
|
1710
1808
|
try:
|
|
@@ -1716,7 +1814,7 @@ WantedBy=multi-user.target
|
|
|
1716
1814
|
monitor_config = f.read()
|
|
1717
1815
|
except (FileNotFoundError, OSError):
|
|
1718
1816
|
# Fallback to embedded scripts if files not found
|
|
1719
|
-
monitor_script =
|
|
1817
|
+
monitor_script = """#!/bin/bash
|
|
1720
1818
|
# CloneBox Monitor - Fallback embedded version
|
|
1721
1819
|
set -euo pipefail
|
|
1722
1820
|
LOG_FILE="/var/log/clonebox-monitor.log"
|
|
@@ -1729,8 +1827,8 @@ while true; do
|
|
|
1729
1827
|
log_info "CloneBox Monitor running..."
|
|
1730
1828
|
sleep 60
|
|
1731
1829
|
done
|
|
1732
|
-
|
|
1733
|
-
monitor_service =
|
|
1830
|
+
"""
|
|
1831
|
+
monitor_service = """[Unit]
|
|
1734
1832
|
Description=CloneBox Monitor
|
|
1735
1833
|
After=graphical-session.target
|
|
1736
1834
|
[Service]
|
|
@@ -1740,33 +1838,39 @@ ExecStart=/usr/local/bin/clonebox-monitor
|
|
|
1740
1838
|
Restart=always
|
|
1741
1839
|
[Install]
|
|
1742
1840
|
WantedBy=default.target
|
|
1743
|
-
|
|
1744
|
-
monitor_config =
|
|
1841
|
+
"""
|
|
1842
|
+
monitor_config = """# CloneBox Monitor Configuration
|
|
1745
1843
|
CLONEBOX_MONITOR_INTERVAL=30
|
|
1746
1844
|
CLONEBOX_AUTO_REPAIR=true
|
|
1747
|
-
|
|
1748
|
-
|
|
1845
|
+
"""
|
|
1846
|
+
|
|
1749
1847
|
# Install monitor script
|
|
1750
1848
|
monitor_b64 = base64.b64encode(monitor_script.encode()).decode()
|
|
1751
|
-
runcmd_lines.append(
|
|
1849
|
+
runcmd_lines.append(
|
|
1850
|
+
f" - echo '{monitor_b64}' | base64 -d > /usr/local/bin/clonebox-monitor"
|
|
1851
|
+
)
|
|
1752
1852
|
runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-monitor")
|
|
1753
|
-
|
|
1853
|
+
|
|
1754
1854
|
# Install monitor configuration
|
|
1755
1855
|
config_b64 = base64.b64encode(monitor_config.encode()).decode()
|
|
1756
1856
|
runcmd_lines.append(f" - echo '{config_b64}' | base64 -d > /etc/default/clonebox-monitor")
|
|
1757
|
-
|
|
1857
|
+
|
|
1758
1858
|
# Install systemd user service
|
|
1759
1859
|
service_b64 = base64.b64encode(monitor_service.encode()).decode()
|
|
1760
|
-
runcmd_lines.append(
|
|
1761
|
-
|
|
1860
|
+
runcmd_lines.append(
|
|
1861
|
+
f" - echo '{service_b64}' | base64 -d > /etc/systemd/user/clonebox-monitor.service"
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1762
1864
|
# Enable lingering and start monitor
|
|
1763
|
-
runcmd_lines.extend(
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1865
|
+
runcmd_lines.extend(
|
|
1866
|
+
[
|
|
1867
|
+
" - loginctl enable-linger ubuntu",
|
|
1868
|
+
" - sudo -u ubuntu systemctl --user daemon-reload",
|
|
1869
|
+
" - sudo -u ubuntu systemctl --user enable clonebox-monitor.service",
|
|
1870
|
+
" - sudo -u ubuntu systemctl --user start clonebox-monitor.service || true",
|
|
1871
|
+
]
|
|
1872
|
+
)
|
|
1873
|
+
|
|
1770
1874
|
# Create Python monitor service for continuous diagnostics (legacy)
|
|
1771
1875
|
monitor_script = f'''#!/usr/bin/env python3
|
|
1772
1876
|
"""CloneBox Monitor - Continuous diagnostics and app restart service."""
|
|
@@ -1870,27 +1974,29 @@ if __name__ == "__main__":
|
|
|
1870
1974
|
main()
|
|
1871
1975
|
'''
|
|
1872
1976
|
# Note: The bash monitor is already installed above, no need to install Python monitor
|
|
1873
|
-
|
|
1977
|
+
|
|
1874
1978
|
# Create logs disk for host access
|
|
1875
|
-
runcmd_lines.extend(
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1979
|
+
runcmd_lines.extend(
|
|
1980
|
+
[
|
|
1981
|
+
" - mkdir -p /mnt/logs",
|
|
1982
|
+
" - truncate -s 1G /var/lib/libvirt/images/clonebox-logs.qcow2",
|
|
1983
|
+
" - mkfs.ext4 -F /var/lib/libvirt/images/clonebox-logs.qcow2",
|
|
1984
|
+
" - echo '/var/lib/libvirt/images/clonebox-logs.qcow2 /mnt/logs ext4 loop,defaults 0 0' >> /etc/fstab",
|
|
1985
|
+
" - mount -a",
|
|
1986
|
+
" - mkdir -p /mnt/logs/var/log",
|
|
1987
|
+
" - mkdir -p /mnt/logs/tmp",
|
|
1988
|
+
" - cp -r /var/log/clonebox*.log /mnt/logs/var/log/ 2>/dev/null || true",
|
|
1989
|
+
" - cp -r /tmp/*-error.log /mnt/logs/tmp/ 2>/dev/null || true",
|
|
1990
|
+
" - echo 'Logs disk mounted at /mnt/logs - accessible from host as /var/lib/libvirt/images/clonebox-logs.qcow2'",
|
|
1991
|
+
" - echo 'To view logs on host: sudo mount -o loop /var/lib/libvirt/images/clonebox-logs.qcow2 /mnt/clonebox-logs'",
|
|
1992
|
+
]
|
|
1993
|
+
)
|
|
1994
|
+
|
|
1889
1995
|
# Add reboot command at the end if GUI is enabled
|
|
1890
1996
|
if config.gui:
|
|
1891
1997
|
runcmd_lines.append(" - echo 'Rebooting in 10 seconds to start GUI...'")
|
|
1892
1998
|
runcmd_lines.append(" - sleep 10 && reboot")
|
|
1893
|
-
|
|
1999
|
+
|
|
1894
2000
|
runcmd_yaml = "\n".join(runcmd_lines) if runcmd_lines else ""
|
|
1895
2001
|
bootcmd_yaml = "\n".join(mount_commands) if mount_commands else ""
|
|
1896
2002
|
bootcmd_block = f"\nbootcmd:\n{bootcmd_yaml}\n" if bootcmd_yaml else ""
|
|
@@ -2079,3 +2185,26 @@ final_message: "CloneBox VM is ready after $UPTIME seconds"
|
|
|
2079
2185
|
"""Close libvirt connection."""
|
|
2080
2186
|
if self.conn:
|
|
2081
2187
|
self.conn.close()
|
|
2188
|
+
|
|
2189
|
+
# Backward compatibility methods for tests
|
|
2190
|
+
def _get_base_image_info(self, image_path: str) -> dict:
|
|
2191
|
+
"""Get base image information - backward compatibility shim."""
|
|
2192
|
+
if hasattr(self, "get_base_image_info"):
|
|
2193
|
+
return self.get_base_image_info(image_path)
|
|
2194
|
+
# Return empty dict if method doesn't exist
|
|
2195
|
+
return {}
|
|
2196
|
+
|
|
2197
|
+
def get_vm_info(self, vm_name: str) -> dict:
|
|
2198
|
+
"""Get VM information - backward compatibility shim."""
|
|
2199
|
+
if hasattr(self, "_get_vm_info"):
|
|
2200
|
+
return self._get_vm_info(vm_name)
|
|
2201
|
+
# Try to get basic info from libvirt
|
|
2202
|
+
try:
|
|
2203
|
+
vm = self.conn.lookupByName(vm_name)
|
|
2204
|
+
return {
|
|
2205
|
+
"name": vm.name(),
|
|
2206
|
+
"state": "running" if vm.isActive() else "stopped",
|
|
2207
|
+
"uuid": vm.UUIDString(),
|
|
2208
|
+
}
|
|
2209
|
+
except Exception:
|
|
2210
|
+
return {}
|