clonebox 1.1.10__py3-none-any.whl → 1.1.12__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 CHANGED
@@ -1825,23 +1825,37 @@ def cmd_test(args):
1825
1825
  # Test 4: Check mounts (if running)
1826
1826
  if not quick and state == "running":
1827
1827
  console.print("[bold]4. Mount Points Check[/]")
1828
- all_paths = config.get("paths", {}).copy()
1829
- all_paths.update(config.get("app_data_paths", {}))
1828
+ paths = config.get("paths", {})
1829
+ app_data_paths = config.get("app_data_paths", {})
1830
1830
 
1831
- if all_paths:
1831
+ if paths or app_data_paths:
1832
1832
  if not _qga_ping(vm_name, conn_uri):
1833
1833
  console.print("[yellow]⚠️ QEMU guest agent not connected - cannot verify mounts[/]")
1834
1834
  else:
1835
- for idx, (host_path, guest_path) in enumerate(all_paths.items()):
1835
+ # Check bind mounts
1836
+ for idx, (host_path, guest_path) in enumerate(paths.items()):
1836
1837
  try:
1837
1838
  # Use the same QGA helper as diagnose/status
1838
1839
  is_accessible = _qga_exec(
1839
1840
  vm_name, conn_uri, f"test -d {guest_path} && echo yes || echo no", timeout=5
1840
1841
  )
1841
1842
  if is_accessible == "yes":
1842
- console.print(f"[green]✅ {guest_path}[/]")
1843
+ console.print(f"[green]✅ {guest_path} (mount)[/]")
1844
+ else:
1845
+ console.print(f"[red]❌ {guest_path} (mount inaccessible)[/]")
1846
+ except Exception:
1847
+ console.print(f"[yellow]⚠️ {guest_path} (could not check)[/]")
1848
+
1849
+ # Check copied paths
1850
+ for idx, (host_path, guest_path) in enumerate(app_data_paths.items()):
1851
+ try:
1852
+ is_accessible = _qga_exec(
1853
+ vm_name, conn_uri, f"test -d {guest_path} && echo yes || echo no", timeout=5
1854
+ )
1855
+ if is_accessible == "yes":
1856
+ console.print(f"[green]✅ {guest_path} (copied)[/]")
1843
1857
  else:
1844
- console.print(f"[red]❌ {guest_path} (not accessible)[/]")
1858
+ console.print(f"[red]❌ {guest_path} (copy missing)[/]")
1845
1859
  except Exception:
1846
1860
  console.print(f"[yellow]⚠️ {guest_path} (could not check)[/]")
1847
1861
  else:
@@ -2521,14 +2535,22 @@ def cmd_clone(args):
2521
2535
  console.print(" [cyan]clonebox-health[/] # Re-run health check")
2522
2536
 
2523
2537
  # Show mount instructions
2524
- all_paths = config.get("paths", {}).copy()
2525
- all_paths.update(config.get("app_data_paths", {}))
2526
- if all_paths:
2527
- console.print("\n[bold]📁 Mounted paths (automatic):[/]")
2528
- for idx, (host, guest) in enumerate(list(all_paths.items())[:5]):
2538
+ paths = config.get("paths", {})
2539
+ app_data_paths = config.get("app_data_paths", {})
2540
+
2541
+ if paths:
2542
+ console.print("\n[bold]📁 Mounted paths (shared live):[/]")
2543
+ for idx, (host, guest) in enumerate(list(paths.items())[:5]):
2544
+ console.print(f" [dim]{host}[/] → [cyan]{guest}[/]")
2545
+ if len(paths) > 5:
2546
+ console.print(f" [dim]... and {len(paths) - 5} more paths[/]")
2547
+
2548
+ if app_data_paths:
2549
+ console.print("\n[bold]📥 Copied paths (one-time import):[/]")
2550
+ for idx, (host, guest) in enumerate(list(app_data_paths.items())[:5]):
2529
2551
  console.print(f" [dim]{host}[/] → [cyan]{guest}[/]")
2530
- if len(all_paths) > 5:
2531
- console.print(f" [dim]... and {len(all_paths) - 5} more paths[/]")
2552
+ if len(app_data_paths) > 5:
2553
+ console.print(f" [dim]... and {len(app_data_paths) - 5} more paths[/]")
2532
2554
  except PermissionError as e:
2533
2555
  console.print(f"[red]❌ Permission Error:[/]\n{e}")
2534
2556
  console.print("\n[yellow]💡 Try running with --user flag:[/]")
clonebox/cloner.py CHANGED
@@ -92,6 +92,7 @@ class VMConfig:
92
92
  snap_packages: list = field(default_factory=list) # Snap packages to install
93
93
  services: list = field(default_factory=list)
94
94
  post_commands: list = field(default_factory=list) # Commands to run after setup
95
+ copy_paths: dict = field(default_factory=dict) # Paths to copy (import) instead of bind-mount
95
96
  user_session: bool = field(
96
97
  default_factory=lambda: os.getenv("VM_USER_SESSION", "false").lower() == "true"
97
98
  ) # Use qemu:///session instead of qemu:///system
@@ -183,7 +184,13 @@ class SelectiveVMCloner:
183
184
  )
184
185
 
185
186
  try:
186
- self.conn = libvirt.open(self.conn_uri)
187
+ # Use openAuth to avoid blocking on graphical auth dialogs (polkit)
188
+ # This is more robust for CLI usage
189
+ def auth_cb(creds, opaque):
190
+ return 0
191
+
192
+ auth = [[libvirt.VIR_CRED_AUTHNAME, libvirt.VIR_CRED_NOECHOPROMPT], auth_cb, None]
193
+ self.conn = libvirt.openAuth(self.conn_uri, auth, 0)
187
194
  except libvirt.libvirtError as e:
188
195
  raise ConnectionError(
189
196
  f"Cannot connect to {self.conn_uri}\n"
@@ -523,7 +530,8 @@ class SelectiveVMCloner:
523
530
  pass
524
531
 
525
532
  # CPU tuning element
526
- if limits.cpu.shares or limits.cpu.quota or limits.cpu.pin:
533
+ # Only available in system session (requires cgroups)
534
+ if not self.user_session and (limits.cpu.shares or limits.cpu.quota or limits.cpu.pin):
527
535
  cputune = ET.SubElement(root, "cputune")
528
536
  ET.SubElement(cputune, "shares").text = str(limits.cpu.shares)
529
537
  if limits.cpu.quota:
@@ -534,7 +542,8 @@ class SelectiveVMCloner:
534
542
  ET.SubElement(cputune, "vcpupin", vcpu=str(idx), cpuset=str(cpu))
535
543
 
536
544
  # Memory tuning element
537
- if limits.memory.soft_limit or limits.memory.swap:
545
+ # Only available in system session (requires cgroups)
546
+ if not self.user_session and (limits.memory.soft_limit or limits.memory.swap):
538
547
  memtune = ET.SubElement(root, "memtune")
539
548
  ET.SubElement(memtune, "hard_limit", unit="KiB").text = str(limit_kib)
540
549
  if limits.memory.soft_limit_bytes:
@@ -558,7 +567,8 @@ class SelectiveVMCloner:
558
567
  ET.SubElement(disk, "target", dev="vda", bus="virtio")
559
568
 
560
569
  # Disk I/O tuning
561
- if limits.disk.read_bps or limits.disk.write_bps or limits.disk.read_iops or limits.disk.write_iops:
570
+ # Only available in system session (requires cgroups)
571
+ if not self.user_session and (limits.disk.read_bps or limits.disk.write_bps or limits.disk.read_iops or limits.disk.write_iops):
562
572
  iotune = ET.SubElement(disk, "iotune")
563
573
  if limits.disk.read_bps_bytes:
564
574
  ET.SubElement(iotune, "read_bytes_sec").text = str(limits.disk.read_bps_bytes)
@@ -588,6 +598,16 @@ class SelectiveVMCloner:
588
598
  tag = f"mount{idx}"
589
599
  ET.SubElement(fs, "target", dir=tag)
590
600
 
601
+ # 9p filesystem mounts for COPY paths (mounted to temp location for import)
602
+ for idx, (host_path, guest_path) in enumerate(config.copy_paths.items()):
603
+ if Path(host_path).exists():
604
+ fs = ET.SubElement(devices, "filesystem", type="mount", accessmode="mapped")
605
+ ET.SubElement(fs, "driver", type="path", wrpolicy="immediate")
606
+ ET.SubElement(fs, "source", dir=host_path)
607
+ # Use import tag names for copy mounts
608
+ tag = f"import{idx}"
609
+ ET.SubElement(fs, "target", dir=tag)
610
+
591
611
  # Network interface
592
612
  network_mode = self.resolve_network_mode(config)
593
613
  if network_mode == "user":
@@ -655,7 +675,8 @@ class SelectiveVMCloner:
655
675
  import base64
656
676
 
657
677
  wants_google_chrome = any(
658
- p == "/home/ubuntu/.config/google-chrome" for p in (config.paths or {}).values()
678
+ p == "/home/ubuntu/.config/google-chrome"
679
+ for p in list((config.paths or {}).values()) + list((config.copy_paths or {}).values())
659
680
  )
660
681
 
661
682
  apt_pkg_list = list(config.packages or [])
@@ -1298,6 +1319,40 @@ fi
1298
1319
  # Add fstab entry for persistence after reboot
1299
1320
  fstab_entries.append(f"{tag} {guest_path} 9p {mount_opts},nofail 0 0")
1300
1321
 
1322
+ # Handle copy_paths (import then copy)
1323
+ all_copy_paths = dict(config.copy_paths) if config.copy_paths else {}
1324
+ for idx, (host_path, guest_path) in enumerate(all_copy_paths.items()):
1325
+ if Path(host_path).exists():
1326
+ tag = f"import{idx}"
1327
+ temp_mount_point = f"/mnt/import{idx}"
1328
+ # Use regular mount options
1329
+ mount_opts = "trans=virtio,version=9p2000.L,mmap,uid=1000,gid=1000"
1330
+
1331
+ # 1. Create temp mount point
1332
+ mount_commands.append(f" - mkdir -p {temp_mount_point}")
1333
+
1334
+ # 2. Mount the 9p share
1335
+ mount_commands.append(f" - mount -t 9p -o {mount_opts} {tag} {temp_mount_point} || true")
1336
+
1337
+ # 3. Ensure target directory exists and permissions are prepared
1338
+ if str(guest_path).startswith("/home/ubuntu/"):
1339
+ mount_commands.append(f" - mkdir -p {guest_path}")
1340
+ mount_commands.append(f" - chown 1000:1000 {guest_path}")
1341
+ else:
1342
+ mount_commands.append(f" - mkdir -p {guest_path}")
1343
+
1344
+ # 4. Copy contents (cp -rT to copy contents of source to target)
1345
+ # We use || true to ensure boot continues even if copy fails
1346
+ mount_commands.append(f" - echo 'Importing {host_path} to {guest_path}...'")
1347
+ mount_commands.append(f" - cp -rT {temp_mount_point} {guest_path} || true")
1348
+
1349
+ # 5. Fix ownership recursively
1350
+ mount_commands.append(f" - chown -R 1000:1000 {guest_path}")
1351
+
1352
+ # 6. Unmount and cleanup
1353
+ mount_commands.append(f" - umount {temp_mount_point} || true")
1354
+ mount_commands.append(f" - rmdir {temp_mount_point} || true")
1355
+
1301
1356
  # User-data
1302
1357
  # Add desktop environment if GUI is enabled
1303
1358
  base_packages = ["qemu-guest-agent", "cloud-guest-utils"]
@@ -1876,8 +1931,14 @@ esac
1876
1931
  }
1877
1932
  )
1878
1933
 
1879
- # Check for google-chrome from app_data_paths
1880
- for host_path, guest_path in (config.paths or {}).items():
1934
+ # Check for google-chrome from app_data_paths (now in copy_paths or paths)
1935
+ all_paths_to_check = {}
1936
+ if config.paths:
1937
+ all_paths_to_check.update(config.paths)
1938
+ if config.copy_paths:
1939
+ all_paths_to_check.update(config.copy_paths)
1940
+
1941
+ for host_path, guest_path in all_paths_to_check.items():
1881
1942
  if guest_path == "/home/ubuntu/.config/google-chrome":
1882
1943
  autostart_apps.append(
1883
1944
  {
@@ -2330,7 +2391,31 @@ final_message: "CloneBox VM is ready after $UPTIME seconds"
2330
2391
  vm.destroy()
2331
2392
  else:
2332
2393
  log(f"[cyan]🛑 Shutting down VM '{vm_name}'...[/]")
2333
- vm.shutdown()
2394
+ try:
2395
+ vm.shutdown()
2396
+ except libvirt.libvirtError as e:
2397
+ log(f"[red]❌ Failed to request shutdown: {e}[/]")
2398
+ return False
2399
+
2400
+ # Wait for shutdown
2401
+ import time
2402
+ waiting = True
2403
+ for i in range(30):
2404
+ try:
2405
+ if not vm.isActive():
2406
+ waiting = False
2407
+ break
2408
+ except libvirt.libvirtError:
2409
+ # Domain might be gone
2410
+ waiting = False
2411
+ break
2412
+ time.sleep(1)
2413
+
2414
+ if waiting:
2415
+ log(f"[red]❌ Shutdown timed out. VM is still running.[/]")
2416
+ log(f"[dim]The guest OS is not responding to ACPI shutdown signal.[/]")
2417
+ log(f"[dim]Try using: clonebox stop {vm_name} --force[/]")
2418
+ return False
2334
2419
 
2335
2420
  log("[green]✅ VM stopped![/]")
2336
2421
  return True
clonebox/di.py CHANGED
@@ -34,7 +34,7 @@ class DependencyContainer:
34
34
 
35
35
  def __init__(self):
36
36
  self._registrations: Dict[Type, ServiceRegistration] = {}
37
- self._lock = threading.Lock()
37
+ self._lock = threading.RLock()
38
38
 
39
39
  def register(
40
40
  self,
clonebox/models.py CHANGED
@@ -116,10 +116,6 @@ class CloneBoxConfig(BaseModel):
116
116
  """Convert to legacy VMConfig dataclass for compatibility."""
117
117
  from clonebox.cloner import VMConfig as VMConfigDataclass
118
118
 
119
- # Merge paths and app_data_paths
120
- all_paths = dict(self.paths)
121
- all_paths.update(self.app_data_paths)
122
-
123
119
  return VMConfigDataclass(
124
120
  name=self.vm.name,
125
121
  ram_mb=self.vm.ram_mb,
@@ -127,7 +123,8 @@ class CloneBoxConfig(BaseModel):
127
123
  disk_size_gb=self.vm.disk_size_gb,
128
124
  gui=self.vm.gui,
129
125
  base_image=self.vm.base_image,
130
- paths=all_paths,
126
+ paths=self.paths,
127
+ copy_paths=self.app_data_paths, # Map app_data_paths to copy_paths
131
128
  packages=self.packages,
132
129
  snap_packages=self.snap_packages,
133
130
  services=self.services,
clonebox/validator.py CHANGED
@@ -106,14 +106,14 @@ class VMValidator:
106
106
  return None
107
107
 
108
108
  def validate_mounts(self) -> Dict:
109
- """Validate all mount points are accessible and contain data."""
110
- self.console.print("\n[bold]💾 Validating Mount Points...[/]")
109
+ """Validate all mount points and copied data paths."""
110
+ self.console.print("\n[bold]💾 Validating Mounts & Data...[/]")
111
111
 
112
- all_paths = self.config.get("paths", {}).copy()
113
- all_paths.update(self.config.get("app_data_paths", {}))
112
+ paths = self.config.get("paths", {})
113
+ app_data_paths = self.config.get("app_data_paths", {})
114
114
 
115
- if not all_paths:
116
- self.console.print("[dim]No mount points configured[/]")
115
+ if not paths and not app_data_paths:
116
+ self.console.print("[dim]No mounts or data paths configured[/]")
117
117
  return self.results["mounts"]
118
118
 
119
119
  # Get mounted filesystems
@@ -122,64 +122,95 @@ class VMValidator:
122
122
  if mount_output:
123
123
  mounted_paths = [line.split()[2] for line in mount_output.split("\n") if line.strip()]
124
124
 
125
- mount_table = Table(title="Mount Validation", border_style="cyan")
125
+ mount_table = Table(title="Data Validation", border_style="cyan")
126
126
  mount_table.add_column("Guest Path", style="bold")
127
- mount_table.add_column("Mounted", justify="center")
128
- mount_table.add_column("Accessible", justify="center")
127
+ mount_table.add_column("Type", justify="center")
128
+ mount_table.add_column("Status", justify="center")
129
129
  mount_table.add_column("Files", justify="right")
130
130
 
131
- for host_path, guest_path in all_paths.items():
131
+ # Validate bind mounts (paths)
132
+ for host_path, guest_path in paths.items():
132
133
  self.results["mounts"]["total"] += 1
133
-
134
+
134
135
  # Check if mounted
135
136
  is_mounted = any(guest_path in mp for mp in mounted_paths)
136
-
137
+
137
138
  # Check if accessible
138
139
  accessible = False
139
140
  file_count = "?"
140
-
141
+
141
142
  if is_mounted:
142
143
  test_result = self._exec_in_vm(f"test -d {guest_path} && echo 'yes' || echo 'no'")
143
144
  accessible = test_result == "yes"
144
-
145
+
145
146
  if accessible:
146
- # Get file count
147
147
  count_str = self._exec_in_vm(f"ls -A {guest_path} 2>/dev/null | wc -l")
148
148
  if count_str and count_str.isdigit():
149
149
  file_count = count_str
150
150
 
151
- # Determine status
152
151
  if is_mounted and accessible:
153
- mount_status = "[green]✅[/]"
154
- access_status = "[green]✅[/]"
152
+ status_icon = "[green]✅ Mounted[/]"
155
153
  self.results["mounts"]["passed"] += 1
156
154
  status = "pass"
157
155
  elif is_mounted:
158
- mount_status = "[green][/]"
159
- access_status = "[red]❌[/]"
156
+ status_icon = "[red]❌ Inaccessible[/]"
160
157
  self.results["mounts"]["failed"] += 1
161
158
  status = "mounted_but_inaccessible"
162
159
  else:
163
- mount_status = "[red]❌[/]"
164
- access_status = "[dim]N/A[/]"
160
+ status_icon = "[red]❌ Not Mounted[/]"
165
161
  self.results["mounts"]["failed"] += 1
166
162
  status = "not_mounted"
167
163
 
168
- mount_table.add_row(guest_path, mount_status, access_status, str(file_count))
169
-
170
- self.results["mounts"]["details"].append(
171
- {
172
- "path": guest_path,
173
- "mounted": is_mounted,
174
- "accessible": accessible,
175
- "files": file_count,
176
- "status": status,
177
- }
178
- )
164
+ mount_table.add_row(guest_path, "Bind Mount", status_icon, str(file_count))
165
+ self.results["mounts"]["details"].append({
166
+ "path": guest_path,
167
+ "type": "mount",
168
+ "mounted": is_mounted,
169
+ "accessible": accessible,
170
+ "files": file_count,
171
+ "status": status
172
+ })
173
+
174
+ # Validate copied paths (app_data_paths)
175
+ for host_path, guest_path in app_data_paths.items():
176
+ self.results["mounts"]["total"] += 1
177
+
178
+ # Check if exists and has content
179
+ exists = False
180
+ file_count = "?"
181
+
182
+ test_result = self._exec_in_vm(f"test -d {guest_path} && echo 'yes' || echo 'no'")
183
+ exists = test_result == "yes"
184
+
185
+ if exists:
186
+ count_str = self._exec_in_vm(f"ls -A {guest_path} 2>/dev/null | wc -l")
187
+ if count_str and count_str.isdigit():
188
+ file_count = count_str
189
+
190
+ # For copied paths, we just check existence and content
191
+ if exists:
192
+ # Warning if empty? Maybe, but strictly it passed existence check
193
+ status_icon = "[green]✅ Copied[/]"
194
+ self.results["mounts"]["passed"] += 1
195
+ status = "pass"
196
+ else:
197
+ status_icon = "[red]❌ Missing[/]"
198
+ self.results["mounts"]["failed"] += 1
199
+ status = "missing"
200
+
201
+ mount_table.add_row(guest_path, "Imported", status_icon, str(file_count))
202
+ self.results["mounts"]["details"].append({
203
+ "path": guest_path,
204
+ "type": "copy",
205
+ "mounted": False, # Expected false for copies
206
+ "accessible": exists,
207
+ "files": file_count,
208
+ "status": status
209
+ })
179
210
 
180
211
  self.console.print(mount_table)
181
212
  self.console.print(
182
- f"[dim]{self.results['mounts']['passed']}/{self.results['mounts']['total']} mounts working[/]"
213
+ f"[dim]{self.results['mounts']['passed']}/{self.results['mounts']['total']} paths valid[/]"
183
214
  )
184
215
 
185
216
  return self.results["mounts"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.10
3
+ Version: 1.1.12
4
4
  Summary: Clone your workstation environment to an isolated VM with selective apps, paths and services
5
5
  Author: CloneBox Team
6
6
  License: Apache-2.0
@@ -1,15 +1,15 @@
1
1
  clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
2
2
  clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
3
- clonebox/cli.py,sha256=6aaIMefXxoRQgzlQDOqKFHq7e8U0CIH5TuYKrsobhH4,137848
4
- clonebox/cloner.py,sha256=gUpv0VIqpkmEZPhafyYZOttpd8ZWh9_kTTfk0cCULEg,91937
3
+ clonebox/cli.py,sha256=CcqJgVH-hSGm4FXTuJCWniocKnAeQayhPPO17nxK578,139009
4
+ clonebox/cloner.py,sha256=sCUeGLY64rXVoEmstS5oFLbCmwwFyR8LhIzkmplMemI,96242
5
5
  clonebox/container.py,sha256=tiYK1ZB-DhdD6A2FuMA0h_sRNkUI7KfYcJ0tFOcdyeM,6105
6
6
  clonebox/dashboard.py,sha256=dMY6odvPq3j6FronhRRsX7aY3qdCwznB-aCWKEmHDNw,5768
7
7
  clonebox/detector.py,sha256=vS65cvFNPmUBCX1Y_TMTnSRljw6r1Ae9dlVtACs5XFc,23075
8
- clonebox/di.py,sha256=feFMXP5ff-7gwrIqgnoCpk1ivaiZA_lv2wcpkCSKiew,5648
8
+ clonebox/di.py,sha256=TrPNNtyDcD_J6K8QMDsxnLVi1E6Qsj47CmOg4560tHs,5649
9
9
  clonebox/exporter.py,sha256=WIzVvmA0z_jjrpyXxvnXoLp9oaW6fKS7k0PGwzx_PIM,5629
10
10
  clonebox/importer.py,sha256=Q9Uk1IOA41mgGhU4ynW2k-h9GEoGxRKI3c9wWE4uxcA,7097
11
11
  clonebox/logging.py,sha256=jD_WgkHmt_h99EmX82QWK7D0OKWyxqcLwgT4UDQFp2g,3675
12
- clonebox/models.py,sha256=l9e5U4ZEEcgCTt2c47GpqFwppwVeifoCoNFi57lFGFo,8307
12
+ clonebox/models.py,sha256=13B0lVAuaGnpY4h4iYUPigIvfxYx2pWaAEM1cYF_Hbo,8263
13
13
  clonebox/monitor.py,sha256=zlIarNf8w_i34XI8hZGxxrg5PVZK_Yxm6FQnkhLavRI,9181
14
14
  clonebox/p2p.py,sha256=6o0JnscKqF9-BftQhW5fF1W6YY1wXshY9LEklNcHGJc,5913
15
15
  clonebox/profiles.py,sha256=UP37fX_rhrG_O9ehNFJBUcULPmUtN1A8KsJ6cM44oK0,1986
@@ -17,7 +17,7 @@ clonebox/resource_monitor.py,sha256=lDR9KyPbVtImeeOkOBPPVP-5yCgoL5hsVFPZ_UqsY0w,
17
17
  clonebox/resources.py,sha256=IkuM4OdSDV4qhyc0eIynwbAHBTv0aVSxxW-gghsnCAs,6815
18
18
  clonebox/rollback.py,sha256=hpwO-8Ehe1pW0wHuZvJkC_qxZ6yEo9otCJRhGIUArCo,5711
19
19
  clonebox/secrets.py,sha256=QtGMIjhTKq14qQRGy-pX0TU3vKLiZt7CMloAlF2GmzQ,10515
20
- clonebox/validator.py,sha256=pxW1Np3s-T2TFvJ64BiO-N8lcKnJGzwInu5cPC9lBK0,39528
20
+ clonebox/validator.py,sha256=qKbT5dXka1Pts9GYqSt3ZEqF83gqdR0gFtFwOODWKvg,40918
21
21
  clonebox/backends/libvirt_backend.py,sha256=sIHFIvFO1hIOXEFR_foSkOGBgIzaJVQs-njOU8GdafA,7170
22
22
  clonebox/backends/qemu_disk.py,sha256=YsGjYX5sbEf35Y4yjTpNkZat73a4RGBxY-KTVzJhqIs,1687
23
23
  clonebox/backends/subprocess_runner.py,sha256=c-IyaMxM1cmUu64h654oAvulm83K5Mu-VQxXJ_0BOds,1506
@@ -34,9 +34,9 @@ clonebox/snapshots/manager.py,sha256=hGzM8V6ZJPXjTqj47c4Kr8idlE-c1Q3gPUvuw1HvS1A
34
34
  clonebox/snapshots/models.py,sha256=sRnn3OZE8JG9FZJlRuA3ihO-JXoPCQ3nD3SQytflAao,6206
35
35
  clonebox/templates/profiles/ml-dev.yaml,sha256=w07MToGh31xtxpjbeXTBk9BkpAN8A3gv8HeA3ESKG9M,461
36
36
  clonebox/templates/profiles/web-stack.yaml,sha256=EBnnGMzML5vAjXmIUbCpbTCwmRaNJiuWd3EcL43DOK8,485
37
- clonebox-1.1.10.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
38
- clonebox-1.1.10.dist-info/METADATA,sha256=nRh_SZ7TDLcmP5gJPPBXwdyq_VRpaI_nSfbsDeYjL9E,48916
39
- clonebox-1.1.10.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
40
- clonebox-1.1.10.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
41
- clonebox-1.1.10.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
42
- clonebox-1.1.10.dist-info/RECORD,,
37
+ clonebox-1.1.12.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
38
+ clonebox-1.1.12.dist-info/METADATA,sha256=VwCiMx2lcWX9XB9w89M16zRtQPuPhu1RW1KHdIxeJrA,48916
39
+ clonebox-1.1.12.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
40
+ clonebox-1.1.12.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
41
+ clonebox-1.1.12.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
42
+ clonebox-1.1.12.dist-info/RECORD,,