clonebox 1.1.13__py3-none-any.whl → 1.1.15__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/cloner.py CHANGED
@@ -35,6 +35,7 @@ from clonebox.logging import get_logger, log_operation
35
35
  from clonebox.resources import ResourceLimits
36
36
  from clonebox.rollback import vm_creation_transaction
37
37
  from clonebox.secrets import SecretsManager, SSHKeyPair
38
+ from clonebox.audit import get_audit_logger, AuditEventType, AuditOutcome
38
39
 
39
40
  log = get_logger(__name__)
40
41
 
@@ -365,121 +366,131 @@ class SelectiveVMCloner:
365
366
  Returns:
366
367
  UUID of created VM
367
368
  """
368
- with log_operation(
369
- log, "vm.create", vm_name=config.name, ram_mb=config.ram_mb
370
- ):
371
- with vm_creation_transaction(self, config, console) as ctx:
372
- # If VM already exists, optionally replace it
373
- existing_vm = None
374
- try:
375
- candidate_vm = self.conn.lookupByName(config.name)
376
- if candidate_vm is not None:
377
- try:
378
- if hasattr(candidate_vm, "name") and callable(candidate_vm.name):
379
- if candidate_vm.name() == config.name:
369
+ audit = get_audit_logger()
370
+ with audit.operation(
371
+ AuditEventType.VM_CREATE,
372
+ target_type="vm",
373
+ target_name=config.name,
374
+ ) as audit_ctx:
375
+ audit_ctx.add_detail("ram_mb", config.ram_mb)
376
+ audit_ctx.add_detail("vcpus", config.vcpus)
377
+ audit_ctx.add_detail("disk_size_gb", config.disk_size_gb)
378
+
379
+ with log_operation(
380
+ log, "vm.create", vm_name=config.name, ram_mb=config.ram_mb
381
+ ):
382
+ with vm_creation_transaction(self, config, console) as ctx:
383
+ # If VM already exists, optionally replace it
384
+ existing_vm = None
385
+ try:
386
+ candidate_vm = self.conn.lookupByName(config.name)
387
+ if candidate_vm is not None:
388
+ try:
389
+ if hasattr(candidate_vm, "name") and callable(candidate_vm.name):
390
+ if candidate_vm.name() == config.name:
391
+ existing_vm = candidate_vm
392
+ else:
380
393
  existing_vm = candidate_vm
381
- else:
394
+ except Exception:
382
395
  existing_vm = candidate_vm
383
- except Exception:
384
- existing_vm = candidate_vm
385
- except Exception:
386
- existing_vm = None
387
-
388
- if existing_vm is not None:
389
- if not replace:
390
- raise RuntimeError(
391
- f"VM '{config.name}' already exists.\n\n"
396
+ except Exception:
397
+ existing_vm = None
398
+
399
+ if existing_vm is not None:
400
+ if not replace:
401
+ raise RuntimeError(
402
+ f"VM '{config.name}' already exists.\n\n"
403
+ f"🔧 Solutions:\n"
404
+ f" 1. Reuse existing VM: clonebox start {config.name}\n"
405
+ f" 2. Replace it: clonebox clone . --name {config.name} --replace\n"
406
+ f" 3. Delete it: clonebox delete {config.name}\n"
407
+ )
408
+
409
+ log.info(f"VM '{config.name}' already exists - replacing...")
410
+ self.delete_vm(config.name, delete_storage=True, console=console, ignore_not_found=True)
411
+
412
+ # Determine images directory
413
+ images_dir = self.get_images_dir()
414
+ try:
415
+ vm_dir = ctx.add_directory(images_dir / config.name)
416
+ vm_dir.mkdir(parents=True, exist_ok=True)
417
+ except PermissionError as e:
418
+ raise PermissionError(
419
+ f"Cannot create VM directory: {images_dir / config.name}\n\n"
392
420
  f"🔧 Solutions:\n"
393
- f" 1. Reuse existing VM: clonebox start {config.name}\n"
394
- f" 2. Replace it: clonebox clone . --name {config.name} --replace\n"
395
- f" 3. Delete it: clonebox delete {config.name}\n"
396
- )
397
-
398
- log.info(f"VM '{config.name}' already exists - replacing...")
399
- self.delete_vm(config.name, delete_storage=True, console=console, ignore_not_found=True)
400
-
401
- # Determine images directory
402
- images_dir = self.get_images_dir()
403
- try:
404
- vm_dir = ctx.add_directory(images_dir / config.name)
405
- vm_dir.mkdir(parents=True, exist_ok=True)
406
- except PermissionError as e:
407
- raise PermissionError(
408
- f"Cannot create VM directory: {images_dir / config.name}\n\n"
409
- f"🔧 Solutions:\n"
410
- f" 1. Use --user flag to run in user session (recommended):\n"
411
- f" clonebox clone . --user\n\n"
412
- f" 2. Run with sudo (not recommended):\n"
413
- f" sudo clonebox clone .\n\n"
414
- f" 3. Fix directory permissions:\n"
415
- f" sudo mkdir -p {images_dir}\n"
416
- f" sudo chown -R $USER:libvirt {images_dir}\n\n"
417
- f"Original error: {e}"
418
- ) from e
419
-
420
- # Create root disk
421
- root_disk = ctx.add_file(vm_dir / "root.qcow2")
422
-
423
- if not config.base_image:
424
- config.base_image = str(self._ensure_default_base_image(console=console))
425
-
426
- if config.base_image and Path(config.base_image).exists():
427
- # Use backing file for faster creation
428
- log.debug(f"Creating disk with backing file: {config.base_image}")
429
- cmd = [
430
- "qemu-img",
431
- "create",
432
- "-f",
433
- "qcow2",
434
- "-b",
435
- config.base_image,
436
- "-F",
437
- "qcow2",
438
- str(root_disk),
439
- f"{config.disk_size_gb}G",
440
- ]
441
- else:
442
- # Create empty disk
443
- log.debug(f"Creating empty {config.disk_size_gb}GB disk...")
444
- cmd = ["qemu-img", "create", "-f", "qcow2", str(root_disk), f"{config.disk_size_gb}G"]
445
-
446
- subprocess.run(cmd, check=True, capture_output=True)
447
-
448
- # Create cloud-init ISO if packages/services specified
449
- cloudinit_iso = None
450
- if (
451
- config.packages
452
- or config.services
453
- or config.snap_packages
454
- or config.post_commands
455
- or config.gui
456
- ):
457
- cloudinit_iso = ctx.add_file(self._create_cloudinit_iso(vm_dir, config))
458
- log.info(f"Created cloud-init ISO with {len(config.packages)} packages")
459
-
460
- # Generate VM XML
461
- vm_xml = self._generate_vm_xml(config, root_disk, cloudinit_iso)
462
- ctx.add_libvirt_domain(self.conn, config.name)
463
-
464
- # Define VM
465
- log.info(f"Defining VM '{config.name}'...")
466
- try:
467
- vm = self.conn.defineXML(vm_xml)
468
- except Exception as e:
469
- raise RuntimeError(
470
- f"Failed to define VM '{config.name}'.\n"
471
- f"Error: {e}\n\n"
472
- f"If the VM already exists, try: clonebox clone . --name {config.name} --replace\n"
473
- ) from e
421
+ f" 1. Use --user flag to run in user session (recommended):\n"
422
+ f" clonebox clone . --user\n\n"
423
+ f" 2. Run with sudo (not recommended):\n"
424
+ f" sudo clonebox clone .\n\n"
425
+ f" 3. Fix directory permissions:\n"
426
+ f" sudo mkdir -p {images_dir}\n"
427
+ f" sudo chown -R $USER:libvirt {images_dir}\n\n"
428
+ f"Original error: {e}"
429
+ ) from e
430
+
431
+ # Create root disk
432
+ root_disk = ctx.add_file(vm_dir / "root.qcow2")
433
+
434
+ if not config.base_image:
435
+ config.base_image = str(self._ensure_default_base_image(console=console))
436
+
437
+ if config.base_image and Path(config.base_image).exists():
438
+ # Use backing file for faster creation
439
+ log.debug(f"Creating disk with backing file: {config.base_image}")
440
+ cmd = [
441
+ "qemu-img",
442
+ "create",
443
+ "-f",
444
+ "qcow2",
445
+ "-b",
446
+ config.base_image,
447
+ "-F",
448
+ "qcow2",
449
+ str(root_disk),
450
+ f"{config.disk_size_gb}G",
451
+ ]
452
+ else:
453
+ # Create empty disk
454
+ log.debug(f"Creating empty {config.disk_size_gb}GB disk...")
455
+ cmd = ["qemu-img", "create", "-f", "qcow2", str(root_disk), f"{config.disk_size_gb}G"]
456
+
457
+ subprocess.run(cmd, check=True, capture_output=True)
458
+
459
+ # Create cloud-init ISO if packages/services specified
460
+ cloudinit_iso = None
461
+ if (
462
+ config.packages
463
+ or config.services
464
+ or config.snap_packages
465
+ or config.post_commands
466
+ or config.gui
467
+ ):
468
+ cloudinit_iso = ctx.add_file(self._create_cloudinit_iso(vm_dir, config, self.user_session))
469
+ log.info(f"Created cloud-init ISO with {len(config.packages)} packages")
470
+
471
+ # Generate VM XML
472
+ vm_xml = self._generate_vm_xml(config, root_disk, cloudinit_iso)
473
+ ctx.add_libvirt_domain(self.conn, config.name)
474
+
475
+ # Define VM
476
+ log.info(f"Defining VM '{config.name}'...")
477
+ try:
478
+ vm = self.conn.defineXML(vm_xml)
479
+ except Exception as e:
480
+ raise RuntimeError(
481
+ f"Failed to define VM '{config.name}'.\n"
482
+ f"Error: {e}\n\n"
483
+ f"If the VM already exists, try: clonebox clone . --name {config.name} --replace\n"
484
+ ) from e
474
485
 
475
- # Start if autostart requested
476
- if getattr(config, "autostart", False):
477
- self.start_vm(config.name, open_viewer=True)
486
+ # Start if autostart requested
487
+ if getattr(config, "autostart", False):
488
+ self.start_vm(config.name, open_viewer=True)
478
489
 
479
- # All good - commit transaction
480
- ctx.commit()
490
+ # All good - commit transaction
491
+ ctx.commit()
481
492
 
482
- return vm.UUIDString()
493
+ return vm.UUIDString()
483
494
 
484
495
  def _generate_vm_xml(
485
496
  self, config: VMConfig = None, root_disk: Path = None, cloudinit_iso: Optional[Path] = None
@@ -706,6 +717,9 @@ class SelectiveVMCloner:
706
717
  for snap, ifaces in SNAP_INTERFACES.items()
707
718
  )
708
719
 
720
+ mount_points_bash = "\n".join(str(p) for p in (config.paths or {}).values())
721
+ copy_paths_bash = "\n".join(str(p) for p in (config.copy_paths or {}).values())
722
+
709
723
  script = f"""#!/bin/bash
710
724
  set -uo pipefail
711
725
  LOG="/var/log/clonebox-boot.log"
@@ -1000,12 +1014,23 @@ for app in "${{APPS_TO_TEST[@]}}"; do
1000
1014
  fi
1001
1015
  done
1002
1016
 
1003
- section "5/7" "Checking mount points..."
1004
- write_status "checking_mounts" "checking mount points"
1005
- while IFS= read -r line; do
1006
- tag=$(echo "$line" | awk '{{print $1}}')
1007
- mp=$(echo "$line" | awk '{{print $2}}')
1008
- if [[ "$tag" =~ ^mount[0-9]+$ ]] && [[ "$mp" == /* ]]; then
1017
+ section "5/7" "Checking mounts & imported paths..."
1018
+ write_status "checking_mounts" "checking mounts & imported paths"
1019
+
1020
+ MOUNT_POINTS=$(cat <<'EOF'
1021
+ {mount_points_bash}
1022
+ EOF
1023
+ )
1024
+
1025
+ COPIED_PATHS=$(cat <<'EOF'
1026
+ {copy_paths_bash}
1027
+ EOF
1028
+ )
1029
+
1030
+ # Bind mounts (shared live)
1031
+ if [ -n "$(echo "$MOUNT_POINTS" | tr -d '[:space:]')" ]; then
1032
+ while IFS= read -r mp; do
1033
+ [ -z "$mp" ] && continue
1009
1034
  if mountpoint -q "$mp" 2>/dev/null; then
1010
1035
  ok "$mp mounted"
1011
1036
  else
@@ -1018,8 +1043,24 @@ while IFS= read -r line; do
1018
1043
  fail "$mp mount FAILED"
1019
1044
  fi
1020
1045
  fi
1021
- fi
1022
- done < /etc/fstab
1046
+ done <<< "$MOUNT_POINTS"
1047
+ else
1048
+ log " (no bind mounts configured)"
1049
+ fi
1050
+
1051
+ # Imported/copied paths (one-time import)
1052
+ if [ -n "$(echo "$COPIED_PATHS" | tr -d '[:space:]')" ]; then
1053
+ while IFS= read -r p; do
1054
+ [ -z "$p" ] && continue
1055
+ if [ -d "$p" ]; then
1056
+ ok "$p copied"
1057
+ else
1058
+ fail "$p missing (copy)"
1059
+ fi
1060
+ done <<< "$COPIED_PATHS"
1061
+ else
1062
+ log " (no copied paths configured)"
1063
+ fi
1023
1064
 
1024
1065
  section "6/7" "Checking services..."
1025
1066
  write_status "checking_services" "checking services"
@@ -1095,6 +1136,10 @@ REPORT_FILE="/var/log/clonebox-health.log"
1095
1136
  PASSED=0
1096
1137
  FAILED=0
1097
1138
  WARNINGS=0
1139
+ SETUP_IN_PROGRESS=0
1140
+ if [ ! -f /var/lib/cloud/instance/boot-finished ]; then
1141
+ SETUP_IN_PROGRESS=1
1142
+ fi
1098
1143
 
1099
1144
  # Colors for output
1100
1145
  RED='\\033[0;31m'
@@ -1113,22 +1158,36 @@ check_apt_package() {{
1113
1158
  ((PASSED++))
1114
1159
  return 0
1115
1160
  else
1116
- log "[FAIL] APT package '$pkg' is NOT installed"
1117
- ((FAILED++))
1118
- return 1
1161
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1162
+ log "[WARN] APT package '$pkg' is not installed yet"
1163
+ ((WARNINGS++))
1164
+ return 1
1165
+ else
1166
+ log "[FAIL] APT package '$pkg' is NOT installed"
1167
+ ((FAILED++))
1168
+ return 1
1169
+ fi
1119
1170
  fi
1120
1171
  }}
1121
1172
 
1122
1173
  check_snap_package() {{
1123
1174
  local pkg="$1"
1124
- if snap list "$pkg" &>/dev/null; then
1175
+ local out
1176
+ out=$(snap list "$pkg" 2>&1)
1177
+ if [ $? -eq 0 ]; then
1125
1178
  log "[PASS] Snap package '$pkg' is installed"
1126
1179
  ((PASSED++))
1127
1180
  return 0
1128
1181
  else
1129
- log "[FAIL] Snap package '$pkg' is NOT installed"
1130
- ((FAILED++))
1131
- return 1
1182
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1183
+ log "[WARN] Snap package '$pkg' is not installed yet"
1184
+ ((WARNINGS++))
1185
+ return 1
1186
+ else
1187
+ log "[FAIL] Snap package '$pkg' is NOT installed"
1188
+ ((FAILED++))
1189
+ return 1
1190
+ fi
1132
1191
  fi
1133
1192
  }}
1134
1193
 
@@ -1221,20 +1280,30 @@ log "Warnings: $WARNINGS"
1221
1280
  if [ $FAILED -eq 0 ]; then
1222
1281
  log ""
1223
1282
  log "[SUCCESS] All critical checks passed!"
1224
- echo "HEALTH_STATUS=OK" > /var/log/clonebox-health-status
1225
- exit 0
1283
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1284
+ echo "HEALTH_STATUS=PENDING" > /var/log/clonebox-health-status
1285
+ exit 0
1286
+ else
1287
+ echo "HEALTH_STATUS=OK" > /var/log/clonebox-health-status
1288
+ exit 0
1289
+ fi
1226
1290
  else
1227
1291
  log ""
1228
1292
  log "[ERROR] Some checks failed. Review log for details."
1229
- echo "HEALTH_STATUS=FAILED" > /var/log/clonebox-health-status
1230
- exit 1
1293
+ if [ $SETUP_IN_PROGRESS -eq 1 ]; then
1294
+ echo "HEALTH_STATUS=PENDING" > /var/log/clonebox-health-status
1295
+ exit 0
1296
+ else
1297
+ echo "HEALTH_STATUS=FAILED" > /var/log/clonebox-health-status
1298
+ exit 1
1299
+ fi
1231
1300
  fi
1232
1301
  """
1233
1302
  # Encode script to base64 for safe embedding in cloud-init
1234
1303
  encoded = base64.b64encode(script.encode()).decode()
1235
1304
  return encoded
1236
1305
 
1237
- def _create_cloudinit_iso(self, vm_dir: Path, config: VMConfig) -> Path:
1306
+ def _create_cloudinit_iso(self, vm_dir: Path, config: VMConfig, user_session: bool = False) -> Path:
1238
1307
  """Create cloud-init ISO with secure credential handling."""
1239
1308
  secrets_mgr = SecretsManager()
1240
1309
 
@@ -1259,6 +1328,17 @@ fi
1259
1328
  ssh_authorized_keys = [key_pair.public_key]
1260
1329
  log.info(f"SSH key generated and saved to: {ssh_key_path}")
1261
1330
 
1331
+ local_password = getattr(config, "password", None)
1332
+ if getattr(config, "gui", False) and local_password:
1333
+ chpasswd_config = (
1334
+ "chpasswd:\n"
1335
+ " list: |\n"
1336
+ f" {config.username}:{local_password}\n"
1337
+ " expire: False"
1338
+ )
1339
+ lock_passwd = "false"
1340
+ ssh_pwauth = "true"
1341
+
1262
1342
  elif auth_method == "one_time_password":
1263
1343
  otp, chpasswd_raw = SecretsManager.generate_one_time_password()
1264
1344
  chpasswd_config = chpasswd_raw
@@ -1511,7 +1591,7 @@ Comment=CloneBox autostart
1511
1591
  )
1512
1592
  runcmd_lines.append(" - chmod +x /usr/local/bin/clonebox-health")
1513
1593
  runcmd_lines.append(
1514
- " - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1"
1594
+ " - /usr/local/bin/clonebox-health >> /var/log/clonebox-health.log 2>&1 || true"
1515
1595
  )
1516
1596
  runcmd_lines.append(" - echo 'CloneBox VM ready!' > /var/log/clonebox-ready")
1517
1597
 
@@ -2245,19 +2325,25 @@ if __name__ == "__main__":
2245
2325
  # Note: The bash monitor is already installed above, no need to install Python monitor
2246
2326
 
2247
2327
  # Create logs disk for host access
2328
+ # Use different paths based on session type
2329
+ if user_session:
2330
+ logs_disk_path = str(Path.home() / ".local/share/libvirt/images/clonebox-logs.qcow2")
2331
+ else:
2332
+ logs_disk_path = "/var/lib/libvirt/images/clonebox-logs.qcow2"
2333
+
2248
2334
  runcmd_lines.extend(
2249
2335
  [
2250
2336
  " - mkdir -p /mnt/logs",
2251
- " - truncate -s 1G /var/lib/libvirt/images/clonebox-logs.qcow2",
2252
- " - mkfs.ext4 -F /var/lib/libvirt/images/clonebox-logs.qcow2",
2253
- " - echo '/var/lib/libvirt/images/clonebox-logs.qcow2 /mnt/logs ext4 loop,defaults 0 0' >> /etc/fstab",
2337
+ f" - truncate -s 1G {logs_disk_path}",
2338
+ f" - mkfs.ext4 -F {logs_disk_path}",
2339
+ f" - echo '{logs_disk_path} /mnt/logs ext4 loop,defaults 0 0' >> /etc/fstab",
2254
2340
  " - mount -a",
2255
2341
  " - mkdir -p /mnt/logs/var/log",
2256
2342
  " - mkdir -p /mnt/logs/tmp",
2257
2343
  " - cp -r /var/log/clonebox*.log /mnt/logs/var/log/ 2>/dev/null || true",
2258
2344
  " - cp -r /tmp/*-error.log /mnt/logs/tmp/ 2>/dev/null || true",
2259
- " - echo 'Logs disk mounted at /mnt/logs - accessible from host as /var/lib/libvirt/images/clonebox-logs.qcow2'",
2260
- " - \"echo 'To view logs on host: sudo mount -o loop /var/lib/libvirt/images/clonebox-logs.qcow2 /mnt/clonebox-logs'\"",
2345
+ f" - echo 'Logs disk mounted at /mnt/logs - accessible from host as {logs_disk_path}'",
2346
+ f" - \"echo 'To view logs on host: sudo mount -o loop {logs_disk_path} /mnt/clonebox-logs'\"",
2261
2347
  ]
2262
2348
  )
2263
2349