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.
- {clonebox-0.1.14 → clonebox-0.1.15}/PKG-INFO +114 -14
- clonebox-0.1.14/src/clonebox.egg-info/PKG-INFO → clonebox-0.1.15/README.md +107 -52
- {clonebox-0.1.14 → clonebox-0.1.15}/pyproject.toml +36 -1
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/cli.py +50 -6
- clonebox-0.1.15/src/clonebox/models.py +128 -0
- clonebox-0.1.14/README.md → clonebox-0.1.15/src/clonebox.egg-info/PKG-INFO +152 -13
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/SOURCES.txt +4 -1
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/requires.txt +7 -0
- clonebox-0.1.15/tests/test_cli.py +405 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/tests/test_cloner.py +23 -32
- {clonebox-0.1.14 → clonebox-0.1.15}/tests/test_detector.py +48 -28
- clonebox-0.1.15/tests/test_models.py +214 -0
- clonebox-0.1.15/tests/test_validator.py +286 -0
- clonebox-0.1.14/tests/test_cli.py +0 -207
- {clonebox-0.1.14 → clonebox-0.1.15}/LICENSE +0 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/setup.cfg +0 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/__init__.py +0 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/__main__.py +0 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/cloner.py +0 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/detector.py +0 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox/validator.py +0 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/dependency_links.txt +0 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/entry_points.txt +0 -0
- {clonebox-0.1.14 → clonebox-0.1.15}/src/clonebox.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
+
[](https://github.com/wronai/clonebox/actions)
|
|
49
|
+
[](https://pypi.org/project/clonebox/)
|
|
50
|
+
[](https://www.python.org/downloads/)
|
|
51
|
+
[](LICENSE)
|
|
52
|
+
|
|
41
53
|

|
|
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
|
-
|
|
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
|
|
268
|
-
clonebox test . --user --
|
|
269
|
-
|
|
270
|
-
#
|
|
271
|
-
# ✅
|
|
272
|
-
# ✅
|
|
273
|
-
# ✅
|
|
274
|
-
# ✅
|
|
275
|
-
# ✅
|
|
276
|
-
# ✅
|
|
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
|
|
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
|
+
[](https://github.com/wronai/clonebox/actions)
|
|
4
|
+
[](https://pypi.org/project/clonebox/)
|
|
5
|
+
[](https://www.python.org/downloads/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
41
8
|

|
|
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
|
-
|
|
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
|
|
268
|
-
clonebox test . --user --
|
|
269
|
-
|
|
270
|
-
#
|
|
271
|
-
# ✅
|
|
272
|
-
# ✅
|
|
273
|
-
# ✅
|
|
274
|
-
# ✅
|
|
275
|
-
# ✅
|
|
276
|
-
# ✅
|
|
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
|
|
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.
|
|
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
|
-
#
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
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
|