clonebox 0.1.14__tar.gz → 0.1.15__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.
Files changed (25) hide show
  1. {clonebox-0.1.14 → clonebox-0.1.15}/PKG-INFO +114 -14
  2. clonebox-0.1.14/src/clonebox.egg-info/PKG-INFO → clonebox-0.1.15/README.md +107 -52
  3. {clonebox-0.1.14 → clonebox-0.1.15}/pyproject.toml +36 -1
  4. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/cli.py +50 -6
  5. clonebox-0.1.15/src/clonebox/models.py +128 -0
  6. clonebox-0.1.14/README.md → clonebox-0.1.15/src/clonebox.egg-info/PKG-INFO +152 -13
  7. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/SOURCES.txt +4 -1
  8. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/requires.txt +7 -0
  9. clonebox-0.1.15/tests/test_cli.py +405 -0
  10. {clonebox-0.1.14 → clonebox-0.1.15}/tests/test_cloner.py +23 -32
  11. {clonebox-0.1.14 → clonebox-0.1.15}/tests/test_detector.py +48 -28
  12. clonebox-0.1.15/tests/test_models.py +214 -0
  13. clonebox-0.1.15/tests/test_validator.py +286 -0
  14. clonebox-0.1.14/tests/test_cli.py +0 -207
  15. {clonebox-0.1.14 → clonebox-0.1.15}/LICENSE +0 -0
  16. {clonebox-0.1.14 → clonebox-0.1.15}/setup.cfg +0 -0
  17. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/__init__.py +0 -0
  18. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/__main__.py +0 -0
  19. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/cloner.py +0 -0
  20. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/detector.py +0 -0
  21. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/validator.py +0 -0
  22. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/dependency_links.txt +0 -0
  23. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/entry_points.txt +0 -0
  24. {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/top_level.txt +0 -0
  25. {clonebox-0.1.14 → clonebox-0.1.15}/tests/test_network.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 0.1.14
3
+ Version: 0.1.15
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
@@ -30,14 +30,26 @@ Requires-Dist: rich>=13.0.0
30
30
  Requires-Dist: questionary>=2.0.0
31
31
  Requires-Dist: psutil>=5.9.0
32
32
  Requires-Dist: pyyaml>=6.0
33
+ Requires-Dist: pydantic>=2.0.0
33
34
  Provides-Extra: dev
34
35
  Requires-Dist: pytest>=7.0.0; extra == "dev"
35
36
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
36
37
  Requires-Dist: black>=23.0.0; extra == "dev"
37
38
  Requires-Dist: ruff>=0.1.0; extra == "dev"
39
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
40
+ Provides-Extra: test
41
+ Requires-Dist: pytest>=7.0.0; extra == "test"
42
+ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
43
+ Requires-Dist: pytest-timeout>=2.0.0; extra == "test"
38
44
  Dynamic: license-file
39
45
 
40
46
  # CloneBox 📦
47
+
48
+ [![CI](https://github.com/wronai/clonebox/workflows/CI/badge.svg)](https://github.com/wronai/clonebox/actions)
49
+ [![PyPI version](https://badge.fury.io/py/clonebox.svg)](https://pypi.org/project/clonebox/)
50
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
51
+ [![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](LICENSE)
52
+
41
53
  ![img.png](img.png)
42
54
 
43
55
  ```commandline
@@ -51,7 +63,8 @@ Dynamic: license-file
51
63
  ║ Clone your workstation to an isolated VM ║
52
64
  ╚═══════════════════════════════════════════════════════╝
53
65
  ```
54
- **Clone your workstation environment to an isolated VM with selective apps, paths and services.**
66
+
67
+ > **Clone your workstation environment to an isolated VM in 60 seconds using bind mounts instead of disk cloning.**
55
68
 
56
69
  CloneBox lets you create isolated virtual machines with only the applications, directories and services you need - using bind mounts instead of full disk cloning. Perfect for development, testing, or creating reproducible environments.
57
70
 
@@ -168,6 +181,61 @@ clonebox stop <name> # Zatrzymaj VM
168
181
  clonebox delete <name> # Usuń VM
169
182
  ```
170
183
 
184
+ ## Development and Testing
185
+
186
+ ### Running Tests
187
+
188
+ CloneBox has comprehensive test coverage with unit tests and end-to-end tests:
189
+
190
+ ```bash
191
+ # Run unit tests only (fast, no libvirt required)
192
+ make test
193
+
194
+ # Run fast unit tests (excludes slow tests)
195
+ make test-unit
196
+
197
+ # Run end-to-end tests (requires libvirt/KVM)
198
+ make test-e2e
199
+
200
+ # Run all tests including e2e
201
+ make test-all
202
+
203
+ # Run tests with coverage
204
+ make test-cov
205
+
206
+ # Run tests with verbose output
207
+ make test-verbose
208
+ ```
209
+
210
+ ### Test Categories
211
+
212
+ Tests are organized with pytest markers:
213
+
214
+ - **Unit tests**: Fast tests that mock libvirt/system calls (default)
215
+ - **E2E tests**: End-to-end tests requiring actual VM creation (marked with `@pytest.mark.e2e`)
216
+ - **Slow tests**: Tests that take longer to run (marked with `@pytest.mark.slow`)
217
+
218
+ E2E tests are automatically skipped when:
219
+ - libvirt is not installed
220
+ - `/dev/kvm` is not available
221
+ - Running in CI environment (`CI=true` or `GITHUB_ACTIONS=true`)
222
+
223
+ ### Manual Test Execution
224
+
225
+ ```bash
226
+ # Run only unit tests (exclude e2e)
227
+ pytest tests/ -m "not e2e"
228
+
229
+ # Run only e2e tests
230
+ pytest tests/e2e/ -m "e2e" -v
231
+
232
+ # Run specific test file
233
+ pytest tests/test_cloner.py -v
234
+
235
+ # Run with coverage
236
+ pytest tests/ -m "not e2e" --cov=clonebox --cov-report=html
237
+ ```
238
+
171
239
  ## Quick Start
172
240
 
173
241
  ### Interactive Mode (Recommended)
@@ -258,22 +326,53 @@ ls ~/.mozilla/firefox # Firefox profile
258
326
  ls ~/.config/JetBrains # PyCharm settings
259
327
  ```
260
328
 
261
- ### Testing VM Configuration
329
+ ### Testing and Validating VM Configuration
262
330
 
263
331
  ```bash
264
332
  # Quick test - basic checks
265
333
  clonebox test . --user --quick
266
334
 
267
- # Full test with verbose output
268
- clonebox test . --user --verbose
269
-
270
- # Test output shows:
271
- # ✅ VM is defined in libvirt
272
- # ✅ VM is running
273
- # ✅ VM has network access (IP: 192.168.122.89)
274
- # ✅ Cloud-init completed
275
- # ✅ All mount points accessible
276
- # ✅ Health check triggered
335
+ # Full validation - checks EVERYTHING against YAML config
336
+ clonebox test . --user --validate
337
+
338
+ # Validation checks:
339
+ # ✅ All mount points (paths + app_data_paths) are mounted and accessible
340
+ # ✅ All APT packages are installed
341
+ # ✅ All snap packages are installed
342
+ # ✅ All services are enabled and running
343
+ # ✅ Reports file counts for each mount
344
+ # ✅ Shows package versions
345
+ # ✅ Comprehensive summary table
346
+
347
+ # Example output:
348
+ # 💾 Validating Mount Points...
349
+ # ┌─────────────────────────┬─────────┬────────────┬────────┐
350
+ # │ Guest Path │ Mounted │ Accessible │ Files │
351
+ # ├─────────────────────────┼─────────┼────────────┼────────┤
352
+ # │ /home/ubuntu/Downloads │ ✅ │ ✅ │ 199 │
353
+ # │ ~/.config/JetBrains │ ✅ │ ✅ │ 45 │
354
+ # └─────────────────────────┴─────────┴────────────┴────────┘
355
+ # 12/14 mounts working
356
+ #
357
+ # 📦 Validating APT Packages...
358
+ # ┌─────────────────┬──────────────┬────────────┐
359
+ # │ Package │ Status │ Version │
360
+ # ├─────────────────┼──────────────┼────────────┤
361
+ # │ firefox │ ✅ Installed │ 122.0+b... │
362
+ # │ docker.io │ ✅ Installed │ 24.0.7-... │
363
+ # └─────────────────┴──────────────┴────────────┘
364
+ # 8/8 packages installed
365
+ #
366
+ # 📊 Validation Summary
367
+ # ┌────────────────┬────────┬────────┬───────┐
368
+ # │ Category │ Passed │ Failed │ Total │
369
+ # ├────────────────┼────────┼────────┼───────┤
370
+ # │ Mounts │ 12 │ 2 │ 14 │
371
+ # │ APT Packages │ 8 │ 0 │ 8 │
372
+ # │ Snap Packages │ 2 │ 0 │ 2 │
373
+ # │ Services │ 5 │ 1 │ 6 │
374
+ # │ TOTAL │ 27 │ 3 │ 30 │
375
+ # └────────────────┴────────┴────────┴───────┘
277
376
  ```
278
377
 
279
378
  ### VM Health Monitoring and Mount Validation
@@ -601,7 +700,8 @@ clonebox clone . --network auto
601
700
  | `clonebox detect --json` | Output as JSON |
602
701
  | `clonebox status . --user` | Check VM health, cloud-init, IP, and mount status |
603
702
  | `clonebox status . --user --health` | Check VM status and run full health check |
604
- | `clonebox test . --user` | Test VM configuration and validate all settings |
703
+ | `clonebox test . --user` | Test VM configuration (basic checks) |
704
+ | `clonebox test . --user --validate` | Full validation: mounts, packages, services vs YAML |
605
705
  | `clonebox export . --user` | Export VM for migration to another workstation |
606
706
  | `clonebox export . --user --include-data` | Export VM with browser profiles and configs |
607
707
  | `clonebox import archive.tar.gz --user` | Import VM from export archive |
@@ -1,43 +1,10 @@
1
- Metadata-Version: 2.4
2
- Name: clonebox
3
- Version: 0.1.14
4
- Summary: Clone your workstation environment to an isolated VM with selective apps, paths and services
5
- Author: CloneBox Team
6
- License: Apache-2.0
7
- Project-URL: Homepage, https://github.com/wronai/clonebox
8
- Project-URL: Repository, https://github.com/wronai/clonebox
9
- Project-URL: Issues, https://github.com/wronai/clonebox/issues
10
- Keywords: vm,virtualization,libvirt,clone,workstation,qemu,kvm
11
- Classifier: Development Status :: 4 - Beta
12
- Classifier: Environment :: Console
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Intended Audience :: System Administrators
15
- Classifier: License :: OSI Approved :: Apache Software License
16
- Classifier: Operating System :: POSIX :: Linux
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.8
19
- Classifier: Programming Language :: Python :: 3.9
20
- Classifier: Programming Language :: Python :: 3.10
21
- Classifier: Programming Language :: Python :: 3.11
22
- Classifier: Programming Language :: Python :: 3.12
23
- Classifier: Topic :: System :: Systems Administration
24
- Classifier: Topic :: Utilities
25
- Requires-Python: >=3.8
26
- Description-Content-Type: text/markdown
27
- License-File: LICENSE
28
- Requires-Dist: libvirt-python>=9.0.0
29
- Requires-Dist: rich>=13.0.0
30
- Requires-Dist: questionary>=2.0.0
31
- Requires-Dist: psutil>=5.9.0
32
- Requires-Dist: pyyaml>=6.0
33
- Provides-Extra: dev
34
- Requires-Dist: pytest>=7.0.0; extra == "dev"
35
- Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
36
- Requires-Dist: black>=23.0.0; extra == "dev"
37
- Requires-Dist: ruff>=0.1.0; extra == "dev"
38
- Dynamic: license-file
39
-
40
1
  # CloneBox 📦
2
+
3
+ [![CI](https://github.com/wronai/clonebox/workflows/CI/badge.svg)](https://github.com/wronai/clonebox/actions)
4
+ [![PyPI version](https://badge.fury.io/py/clonebox.svg)](https://pypi.org/project/clonebox/)
5
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
6
+ [![License](https://img.shields.io/badge/license-Apache%202.0-green.svg)](LICENSE)
7
+
41
8
  ![img.png](img.png)
42
9
 
43
10
  ```commandline
@@ -51,7 +18,8 @@ Dynamic: license-file
51
18
  ║ Clone your workstation to an isolated VM ║
52
19
  ╚═══════════════════════════════════════════════════════╝
53
20
  ```
54
- **Clone your workstation environment to an isolated VM with selective apps, paths and services.**
21
+
22
+ > **Clone your workstation environment to an isolated VM in 60 seconds using bind mounts instead of disk cloning.**
55
23
 
56
24
  CloneBox lets you create isolated virtual machines with only the applications, directories and services you need - using bind mounts instead of full disk cloning. Perfect for development, testing, or creating reproducible environments.
57
25
 
@@ -168,6 +136,61 @@ clonebox stop <name> # Zatrzymaj VM
168
136
  clonebox delete <name> # Usuń VM
169
137
  ```
170
138
 
139
+ ## Development and Testing
140
+
141
+ ### Running Tests
142
+
143
+ CloneBox has comprehensive test coverage with unit tests and end-to-end tests:
144
+
145
+ ```bash
146
+ # Run unit tests only (fast, no libvirt required)
147
+ make test
148
+
149
+ # Run fast unit tests (excludes slow tests)
150
+ make test-unit
151
+
152
+ # Run end-to-end tests (requires libvirt/KVM)
153
+ make test-e2e
154
+
155
+ # Run all tests including e2e
156
+ make test-all
157
+
158
+ # Run tests with coverage
159
+ make test-cov
160
+
161
+ # Run tests with verbose output
162
+ make test-verbose
163
+ ```
164
+
165
+ ### Test Categories
166
+
167
+ Tests are organized with pytest markers:
168
+
169
+ - **Unit tests**: Fast tests that mock libvirt/system calls (default)
170
+ - **E2E tests**: End-to-end tests requiring actual VM creation (marked with `@pytest.mark.e2e`)
171
+ - **Slow tests**: Tests that take longer to run (marked with `@pytest.mark.slow`)
172
+
173
+ E2E tests are automatically skipped when:
174
+ - libvirt is not installed
175
+ - `/dev/kvm` is not available
176
+ - Running in CI environment (`CI=true` or `GITHUB_ACTIONS=true`)
177
+
178
+ ### Manual Test Execution
179
+
180
+ ```bash
181
+ # Run only unit tests (exclude e2e)
182
+ pytest tests/ -m "not e2e"
183
+
184
+ # Run only e2e tests
185
+ pytest tests/e2e/ -m "e2e" -v
186
+
187
+ # Run specific test file
188
+ pytest tests/test_cloner.py -v
189
+
190
+ # Run with coverage
191
+ pytest tests/ -m "not e2e" --cov=clonebox --cov-report=html
192
+ ```
193
+
171
194
  ## Quick Start
172
195
 
173
196
  ### Interactive Mode (Recommended)
@@ -258,22 +281,53 @@ ls ~/.mozilla/firefox # Firefox profile
258
281
  ls ~/.config/JetBrains # PyCharm settings
259
282
  ```
260
283
 
261
- ### Testing VM Configuration
284
+ ### Testing and Validating VM Configuration
262
285
 
263
286
  ```bash
264
287
  # Quick test - basic checks
265
288
  clonebox test . --user --quick
266
289
 
267
- # Full test with verbose output
268
- clonebox test . --user --verbose
269
-
270
- # Test output shows:
271
- # ✅ VM is defined in libvirt
272
- # ✅ VM is running
273
- # ✅ VM has network access (IP: 192.168.122.89)
274
- # ✅ Cloud-init completed
275
- # ✅ All mount points accessible
276
- # ✅ Health check triggered
290
+ # Full validation - checks EVERYTHING against YAML config
291
+ clonebox test . --user --validate
292
+
293
+ # Validation checks:
294
+ # ✅ All mount points (paths + app_data_paths) are mounted and accessible
295
+ # ✅ All APT packages are installed
296
+ # ✅ All snap packages are installed
297
+ # ✅ All services are enabled and running
298
+ # ✅ Reports file counts for each mount
299
+ # ✅ Shows package versions
300
+ # ✅ Comprehensive summary table
301
+
302
+ # Example output:
303
+ # 💾 Validating Mount Points...
304
+ # ┌─────────────────────────┬─────────┬────────────┬────────┐
305
+ # │ Guest Path │ Mounted │ Accessible │ Files │
306
+ # ├─────────────────────────┼─────────┼────────────┼────────┤
307
+ # │ /home/ubuntu/Downloads │ ✅ │ ✅ │ 199 │
308
+ # │ ~/.config/JetBrains │ ✅ │ ✅ │ 45 │
309
+ # └─────────────────────────┴─────────┴────────────┴────────┘
310
+ # 12/14 mounts working
311
+ #
312
+ # 📦 Validating APT Packages...
313
+ # ┌─────────────────┬──────────────┬────────────┐
314
+ # │ Package │ Status │ Version │
315
+ # ├─────────────────┼──────────────┼────────────┤
316
+ # │ firefox │ ✅ Installed │ 122.0+b... │
317
+ # │ docker.io │ ✅ Installed │ 24.0.7-... │
318
+ # └─────────────────┴──────────────┴────────────┘
319
+ # 8/8 packages installed
320
+ #
321
+ # 📊 Validation Summary
322
+ # ┌────────────────┬────────┬────────┬───────┐
323
+ # │ Category │ Passed │ Failed │ Total │
324
+ # ├────────────────┼────────┼────────┼───────┤
325
+ # │ Mounts │ 12 │ 2 │ 14 │
326
+ # │ APT Packages │ 8 │ 0 │ 8 │
327
+ # │ Snap Packages │ 2 │ 0 │ 2 │
328
+ # │ Services │ 5 │ 1 │ 6 │
329
+ # │ TOTAL │ 27 │ 3 │ 30 │
330
+ # └────────────────┴────────┴────────┴───────┘
277
331
  ```
278
332
 
279
333
  ### VM Health Monitoring and Mount Validation
@@ -601,7 +655,8 @@ clonebox clone . --network auto
601
655
  | `clonebox detect --json` | Output as JSON |
602
656
  | `clonebox status . --user` | Check VM health, cloud-init, IP, and mount status |
603
657
  | `clonebox status . --user --health` | Check VM status and run full health check |
604
- | `clonebox test . --user` | Test VM configuration and validate all settings |
658
+ | `clonebox test . --user` | Test VM configuration (basic checks) |
659
+ | `clonebox test . --user --validate` | Full validation: mounts, packages, services vs YAML |
605
660
  | `clonebox export . --user` | Export VM for migration to another workstation |
606
661
  | `clonebox export . --user --include-data` | Export VM with browser profiles and configs |
607
662
  | `clonebox import archive.tar.gz --user` | Import VM from export archive |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "clonebox"
7
- version = "0.1.14"
7
+ version = "0.1.15"
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"}
@@ -36,6 +36,7 @@ dependencies = [
36
36
  "questionary>=2.0.0",
37
37
  "psutil>=5.9.0",
38
38
  "pyyaml>=6.0",
39
+ "pydantic>=2.0.0",
39
40
  ]
40
41
 
41
42
  [project.optional-dependencies]
@@ -44,6 +45,12 @@ dev = [
44
45
  "pytest-cov>=4.0.0",
45
46
  "black>=23.0.0",
46
47
  "ruff>=0.1.0",
48
+ "mypy>=1.0.0",
49
+ ]
50
+ test = [
51
+ "pytest>=7.0.0",
52
+ "pytest-cov>=4.0.0",
53
+ "pytest-timeout>=2.0.0",
47
54
  ]
48
55
 
49
56
  [project.scripts]
@@ -64,3 +71,31 @@ target-version = ["py38", "py39", "py310", "py311", "py312"]
64
71
  [tool.ruff]
65
72
  line-length = 100
66
73
  select = ["E", "F", "W", "I", "N", "UP", "B", "C4"]
74
+
75
+ [tool.pytest.ini_options]
76
+ minversion = "7.0"
77
+ testpaths = ["tests"]
78
+ python_files = ["test_*.py"]
79
+ python_classes = ["Test*"]
80
+ python_functions = ["test_*"]
81
+ markers = [
82
+ "e2e: end-to-end tests requiring libvirt/KVM (deselect with '-m \"not e2e\"')",
83
+ "slow: slow tests (deselect with '-m \"not slow\"')",
84
+ "integration: integration tests",
85
+ "requires_kvm: tests requiring /dev/kvm access",
86
+ ]
87
+ addopts = [
88
+ "-ra",
89
+ "--strict-markers",
90
+ "--strict-config",
91
+ "--showlocals",
92
+ "--cov=src/clonebox",
93
+ "--cov-report=term-missing",
94
+ "--cov-fail-under=30",
95
+ ]
96
+ filterwarnings = [
97
+ "error",
98
+ "ignore::DeprecationWarning",
99
+ "ignore::PendingDeprecationWarning",
100
+ "ignore::UserWarning",
101
+ ]
@@ -1347,11 +1347,24 @@ def cmd_test(args):
1347
1347
 
1348
1348
  console.print()
1349
1349
 
1350
- # Summary
1351
- console.print("[bold]Test Summary[/]")
1352
- console.print("VM configuration is valid and VM is accessible.")
1353
- console.print("\n[dim]For detailed health report, run in VM:[/]")
1354
- console.print("[dim] cat /var/log/clonebox-health.log[/]")
1350
+ # Run full validation if requested
1351
+ if validate_all and state == "running":
1352
+ validator = VMValidator(config, vm_name, conn_uri, console)
1353
+ results = validator.validate_all()
1354
+
1355
+ # Exit with error code if validations failed
1356
+ if results["overall"] == "partial":
1357
+ return 1
1358
+ else:
1359
+ # Summary
1360
+ console.print("[bold]Test Summary[/]")
1361
+ console.print("VM configuration is valid and VM is accessible.")
1362
+ console.print("\n[dim]For full validation including packages, services, and mounts:[/]")
1363
+ console.print("[dim] clonebox test . --user --validate[/]")
1364
+ console.print("\n[dim]For detailed health report, run in VM:[/]")
1365
+ console.print("[dim] cat /var/log/clonebox-health.log[/]")
1366
+
1367
+ return 0
1355
1368
 
1356
1369
 
1357
1370
  CLONEBOX_CONFIG_FILE = ".clonebox.yaml"
@@ -1721,12 +1734,16 @@ def create_vm_from_config(
1721
1734
  def cmd_clone(args):
1722
1735
  """Generate clone config from path and optionally create VM."""
1723
1736
  target_path = Path(args.path).resolve()
1737
+ dry_run = getattr(args, "dry_run", False)
1724
1738
 
1725
1739
  if not target_path.exists():
1726
1740
  console.print(f"[red]❌ Path does not exist: {target_path}[/]")
1727
1741
  return
1728
1742
 
1729
- console.print(f"[bold cyan]📦 Generating clone config for: {target_path}[/]\n")
1743
+ if dry_run:
1744
+ console.print(f"[bold cyan]🔍 DRY RUN - Analyzing: {target_path}[/]\n")
1745
+ else:
1746
+ console.print(f"[bold cyan]📦 Generating clone config for: {target_path}[/]\n")
1730
1747
 
1731
1748
  # Detect system state
1732
1749
  with Progress(
@@ -1751,6 +1768,25 @@ def cmd_clone(args):
1751
1768
  base_image=getattr(args, "base_image", None),
1752
1769
  )
1753
1770
 
1771
+ # Dry run - show what would be created and exit
1772
+ if dry_run:
1773
+ config = yaml.safe_load(yaml_content)
1774
+ console.print(Panel(
1775
+ f"[bold]VM Name:[/] {config['vm']['name']}\n"
1776
+ f"[bold]RAM:[/] {config['vm'].get('ram_mb', 4096)} MB\n"
1777
+ f"[bold]vCPUs:[/] {config['vm'].get('vcpus', 4)}\n"
1778
+ f"[bold]Network:[/] {config['vm'].get('network_mode', 'auto')}\n"
1779
+ f"[bold]Paths:[/] {len(config.get('paths', {}))} mounts\n"
1780
+ f"[bold]Packages:[/] {len(config.get('packages', []))} packages\n"
1781
+ f"[bold]Services:[/] {len(config.get('services', []))} services",
1782
+ title="[bold cyan]Would create VM[/]",
1783
+ border_style="cyan",
1784
+ ))
1785
+ console.print("\n[dim]Config preview:[/]")
1786
+ console.print(Panel(yaml_content, title="[bold].clonebox.yaml[/]", border_style="dim"))
1787
+ console.print("\n[yellow]ℹ️ Dry run complete. No changes made.[/]")
1788
+ return
1789
+
1754
1790
  # Save config file
1755
1791
  config_file = (
1756
1792
  target_path / CLONEBOX_CONFIG_FILE
@@ -2058,6 +2094,11 @@ def main():
2058
2094
  action="store_true",
2059
2095
  help="If VM already exists, stop+undefine it and recreate (also deletes its storage)",
2060
2096
  )
2097
+ clone_parser.add_argument(
2098
+ "--dry-run",
2099
+ action="store_true",
2100
+ help="Show what would be created without making any changes",
2101
+ )
2061
2102
  clone_parser.set_defaults(func=cmd_clone)
2062
2103
 
2063
2104
  # Status command - check VM health from workstation
@@ -2118,6 +2159,9 @@ def main():
2118
2159
  test_parser.add_argument(
2119
2160
  "--verbose", "-v", action="store_true", help="Verbose output"
2120
2161
  )
2162
+ test_parser.add_argument(
2163
+ "--validate", action="store_true", help="Run full validation (mounts, packages, services)"
2164
+ )
2121
2165
  test_parser.set_defaults(func=cmd_test)
2122
2166
 
2123
2167
  args = parser.parse_args()
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Pydantic models for CloneBox configuration validation.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from pydantic import BaseModel, Field, field_validator, model_validator
10
+
11
+
12
+ class VMSettings(BaseModel):
13
+ """VM-specific settings."""
14
+
15
+ name: str = Field(default="clonebox-vm", description="VM name")
16
+ ram_mb: int = Field(default=4096, ge=512, le=131072, description="RAM in MB")
17
+ vcpus: int = Field(default=4, ge=1, le=128, description="Number of vCPUs")
18
+ disk_size_gb: int = Field(default=10, ge=1, le=2048, description="Disk size in GB")
19
+ gui: bool = Field(default=True, description="Enable SPICE graphics")
20
+ base_image: Optional[str] = Field(default=None, description="Path to base qcow2 image")
21
+ network_mode: str = Field(default="auto", description="Network mode: auto|default|user")
22
+ username: str = Field(default="ubuntu", description="VM default username")
23
+ password: str = Field(default="ubuntu", description="VM default password")
24
+
25
+ @field_validator("name")
26
+ @classmethod
27
+ def name_must_be_valid(cls, v: str) -> str:
28
+ if not v or not v.strip():
29
+ raise ValueError("VM name cannot be empty")
30
+ if len(v) > 64:
31
+ raise ValueError("VM name must be <= 64 characters")
32
+ return v.strip()
33
+
34
+ @field_validator("network_mode")
35
+ @classmethod
36
+ def network_mode_must_be_valid(cls, v: str) -> str:
37
+ valid_modes = {"auto", "default", "user"}
38
+ if v not in valid_modes:
39
+ raise ValueError(f"network_mode must be one of: {valid_modes}")
40
+ return v
41
+
42
+
43
+ class CloneBoxConfig(BaseModel):
44
+ """Complete CloneBox configuration with validation."""
45
+
46
+ version: str = Field(default="1", description="Config version")
47
+ generated: Optional[str] = Field(default=None, description="Generation timestamp")
48
+ vm: VMSettings = Field(default_factory=VMSettings, description="VM settings")
49
+ paths: Dict[str, str] = Field(default_factory=dict, description="Host:Guest path mappings")
50
+ app_data_paths: Dict[str, str] = Field(
51
+ default_factory=dict, description="Application data paths"
52
+ )
53
+ packages: List[str] = Field(default_factory=list, description="APT packages to install")
54
+ snap_packages: List[str] = Field(default_factory=list, description="Snap packages to install")
55
+ services: List[str] = Field(default_factory=list, description="Services to enable")
56
+ post_commands: List[str] = Field(default_factory=list, description="Post-setup commands")
57
+ detected: Optional[Dict[str, Any]] = Field(
58
+ default=None, description="Auto-detected system info"
59
+ )
60
+
61
+ @field_validator("paths", "app_data_paths")
62
+ @classmethod
63
+ def paths_must_be_absolute(cls, v: Dict[str, str]) -> Dict[str, str]:
64
+ for host_path, guest_path in v.items():
65
+ if not host_path.startswith("/"):
66
+ raise ValueError(f"Host path must be absolute: {host_path}")
67
+ if not guest_path.startswith("/"):
68
+ raise ValueError(f"Guest path must be absolute: {guest_path}")
69
+ return v
70
+
71
+ @model_validator(mode="before")
72
+ @classmethod
73
+ def handle_nested_vm(cls, data: Any) -> Any:
74
+ """Handle both dict and nested vm structures."""
75
+ if isinstance(data, dict):
76
+ if "vm" in data and isinstance(data["vm"], dict):
77
+ return data
78
+ vm_fields = {"name", "ram_mb", "vcpus", "disk_size_gb", "gui", "base_image",
79
+ "network_mode", "username", "password"}
80
+ vm_data = {k: v for k, v in data.items() if k in vm_fields}
81
+ if vm_data:
82
+ data = {k: v for k, v in data.items() if k not in vm_fields}
83
+ data["vm"] = vm_data
84
+ return data
85
+
86
+ def save(self, path: Path) -> None:
87
+ """Save configuration to YAML file."""
88
+ import yaml
89
+
90
+ config_dict = self.model_dump(exclude_none=True)
91
+ path.write_text(yaml.dump(config_dict, default_flow_style=False, sort_keys=False))
92
+
93
+ @classmethod
94
+ def load(cls, path: Path) -> "CloneBoxConfig":
95
+ """Load configuration from YAML file."""
96
+ import yaml
97
+
98
+ if path.is_dir():
99
+ path = path / ".clonebox.yaml"
100
+ if not path.exists():
101
+ raise FileNotFoundError(f"Config file not found: {path}")
102
+ data = yaml.safe_load(path.read_text())
103
+ return cls.model_validate(data)
104
+
105
+ def to_vm_config(self) -> "VMConfigDataclass":
106
+ """Convert to legacy VMConfig dataclass for compatibility."""
107
+ from clonebox.cloner import VMConfig as VMConfigDataclass
108
+
109
+ return VMConfigDataclass(
110
+ name=self.vm.name,
111
+ ram_mb=self.vm.ram_mb,
112
+ vcpus=self.vm.vcpus,
113
+ disk_size_gb=self.vm.disk_size_gb,
114
+ gui=self.vm.gui,
115
+ base_image=self.vm.base_image,
116
+ paths=self.paths,
117
+ packages=self.packages,
118
+ snap_packages=self.snap_packages,
119
+ services=self.services,
120
+ post_commands=self.post_commands,
121
+ network_mode=self.vm.network_mode,
122
+ username=self.vm.username,
123
+ password=self.vm.password,
124
+ )
125
+
126
+
127
+ # Backwards compatibility alias
128
+ VMConfigModel = CloneBoxConfig