clonebox 0.1.2__py3-none-any.whl → 0.1.4__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/__main__.py +7 -0
- clonebox/cli.py +317 -265
- clonebox/cloner.py +177 -119
- clonebox/detector.py +186 -108
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/METADATA +110 -3
- clonebox-0.1.4.dist-info/RECORD +11 -0
- clonebox-0.1.2.dist-info/RECORD +0 -10
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/WHEEL +0 -0
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/entry_points.txt +0 -0
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {clonebox-0.1.2.dist-info → clonebox-0.1.4.dist-info}/top_level.txt +0 -0
clonebox/detector.py
CHANGED
|
@@ -4,11 +4,10 @@ SystemDetector - Detects running services, applications and important paths.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
-
import subprocess
|
|
8
7
|
import pwd
|
|
9
|
-
|
|
8
|
+
import subprocess
|
|
10
9
|
from dataclasses import dataclass, field
|
|
11
|
-
from
|
|
10
|
+
from pathlib import Path
|
|
12
11
|
|
|
13
12
|
import psutil
|
|
14
13
|
|
|
@@ -16,6 +15,7 @@ import psutil
|
|
|
16
15
|
@dataclass
|
|
17
16
|
class DetectedService:
|
|
18
17
|
"""A detected systemd service."""
|
|
18
|
+
|
|
19
19
|
name: str
|
|
20
20
|
status: str # running, stopped, failed
|
|
21
21
|
description: str = ""
|
|
@@ -25,6 +25,7 @@ class DetectedService:
|
|
|
25
25
|
@dataclass
|
|
26
26
|
class DetectedApplication:
|
|
27
27
|
"""A detected running application."""
|
|
28
|
+
|
|
28
29
|
name: str
|
|
29
30
|
pid: int
|
|
30
31
|
cmdline: str
|
|
@@ -36,6 +37,7 @@ class DetectedApplication:
|
|
|
36
37
|
@dataclass
|
|
37
38
|
class DetectedPath:
|
|
38
39
|
"""A detected important path."""
|
|
40
|
+
|
|
39
41
|
path: str
|
|
40
42
|
type: str # config, data, project, home
|
|
41
43
|
size_mb: float = 0.0
|
|
@@ -45,14 +47,15 @@ class DetectedPath:
|
|
|
45
47
|
@dataclass
|
|
46
48
|
class SystemSnapshot:
|
|
47
49
|
"""Complete snapshot of detected system state."""
|
|
50
|
+
|
|
48
51
|
services: list = field(default_factory=list)
|
|
49
52
|
applications: list = field(default_factory=list)
|
|
50
53
|
paths: list = field(default_factory=list)
|
|
51
|
-
|
|
54
|
+
|
|
52
55
|
@property
|
|
53
56
|
def running_services(self) -> list:
|
|
54
57
|
return [s for s in self.services if s.status == "running"]
|
|
55
|
-
|
|
58
|
+
|
|
56
59
|
@property
|
|
57
60
|
def running_apps(self) -> list:
|
|
58
61
|
return self.applications
|
|
@@ -60,43 +63,93 @@ class SystemSnapshot:
|
|
|
60
63
|
|
|
61
64
|
class SystemDetector:
|
|
62
65
|
"""Detects running services, applications and important paths on the system."""
|
|
63
|
-
|
|
66
|
+
|
|
64
67
|
# Common development/server services to look for
|
|
65
68
|
INTERESTING_SERVICES = [
|
|
66
|
-
"docker",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
69
|
+
"docker",
|
|
70
|
+
"containerd",
|
|
71
|
+
"podman",
|
|
72
|
+
"nginx",
|
|
73
|
+
"apache2",
|
|
74
|
+
"httpd",
|
|
75
|
+
"caddy",
|
|
76
|
+
"postgresql",
|
|
77
|
+
"mysql",
|
|
78
|
+
"mariadb",
|
|
79
|
+
"mongodb",
|
|
80
|
+
"redis",
|
|
81
|
+
"memcached",
|
|
82
|
+
"elasticsearch",
|
|
83
|
+
"kibana",
|
|
84
|
+
"grafana",
|
|
85
|
+
"prometheus",
|
|
86
|
+
"jenkins",
|
|
87
|
+
"gitlab-runner",
|
|
88
|
+
"sshd",
|
|
89
|
+
"rsync",
|
|
90
|
+
"rabbitmq-server",
|
|
91
|
+
"kafka",
|
|
92
|
+
"nodejs",
|
|
93
|
+
"pm2",
|
|
94
|
+
"supervisor",
|
|
95
|
+
"systemd-resolved",
|
|
96
|
+
"cups",
|
|
97
|
+
"bluetooth",
|
|
98
|
+
"NetworkManager",
|
|
99
|
+
"libvirtd",
|
|
100
|
+
"virtlogd",
|
|
77
101
|
]
|
|
78
|
-
|
|
102
|
+
|
|
79
103
|
# Interesting process names
|
|
80
104
|
INTERESTING_PROCESSES = [
|
|
81
|
-
"python",
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
105
|
+
"python",
|
|
106
|
+
"python3",
|
|
107
|
+
"node",
|
|
108
|
+
"npm",
|
|
109
|
+
"yarn",
|
|
110
|
+
"pnpm",
|
|
111
|
+
"java",
|
|
112
|
+
"gradle",
|
|
113
|
+
"mvn",
|
|
114
|
+
"go",
|
|
115
|
+
"cargo",
|
|
116
|
+
"rustc",
|
|
117
|
+
"docker",
|
|
118
|
+
"docker-compose",
|
|
119
|
+
"podman",
|
|
120
|
+
"nginx",
|
|
121
|
+
"apache",
|
|
122
|
+
"httpd",
|
|
123
|
+
"postgres",
|
|
124
|
+
"mysql",
|
|
125
|
+
"mongod",
|
|
126
|
+
"redis-server",
|
|
127
|
+
"code",
|
|
128
|
+
"code-server",
|
|
129
|
+
"cursor",
|
|
130
|
+
"vim",
|
|
131
|
+
"nvim",
|
|
132
|
+
"emacs",
|
|
133
|
+
"firefox",
|
|
134
|
+
"chrome",
|
|
135
|
+
"chromium",
|
|
136
|
+
"jupyter",
|
|
137
|
+
"jupyter-lab",
|
|
138
|
+
"gunicorn",
|
|
139
|
+
"uvicorn",
|
|
140
|
+
"flask",
|
|
141
|
+
"django",
|
|
142
|
+
"webpack",
|
|
143
|
+
"vite",
|
|
144
|
+
"esbuild",
|
|
145
|
+
"tmux",
|
|
146
|
+
"screen",
|
|
94
147
|
]
|
|
95
|
-
|
|
148
|
+
|
|
96
149
|
def __init__(self):
|
|
97
150
|
self.user = pwd.getpwuid(os.getuid()).pw_name
|
|
98
151
|
self.home = Path.home()
|
|
99
|
-
|
|
152
|
+
|
|
100
153
|
def detect_all(self) -> SystemSnapshot:
|
|
101
154
|
"""Detect all services, applications and paths."""
|
|
102
155
|
return SystemSnapshot(
|
|
@@ -104,95 +157,104 @@ class SystemDetector:
|
|
|
104
157
|
applications=self.detect_applications(),
|
|
105
158
|
paths=self.detect_paths(),
|
|
106
159
|
)
|
|
107
|
-
|
|
160
|
+
|
|
108
161
|
def detect_services(self) -> list:
|
|
109
162
|
"""Detect systemd services."""
|
|
110
163
|
services = []
|
|
111
|
-
|
|
164
|
+
|
|
112
165
|
try:
|
|
113
166
|
# Get all services
|
|
114
167
|
result = subprocess.run(
|
|
115
168
|
["systemctl", "list-units", "--type=service", "--all", "--no-pager", "--plain"],
|
|
116
|
-
capture_output=True,
|
|
169
|
+
capture_output=True,
|
|
170
|
+
text=True,
|
|
171
|
+
timeout=10,
|
|
117
172
|
)
|
|
118
|
-
|
|
173
|
+
|
|
119
174
|
for line in result.stdout.strip().split("\n")[1:]: # Skip header
|
|
120
175
|
parts = line.split()
|
|
121
176
|
if len(parts) >= 4:
|
|
122
177
|
name = parts[0].replace(".service", "")
|
|
123
|
-
|
|
178
|
+
|
|
124
179
|
# Filter to interesting services
|
|
125
|
-
if any(
|
|
180
|
+
if any(
|
|
181
|
+
interesting in name.lower() for interesting in self.INTERESTING_SERVICES
|
|
182
|
+
):
|
|
126
183
|
status = "running" if parts[3] == "running" else parts[3]
|
|
127
|
-
|
|
184
|
+
|
|
128
185
|
# Get description
|
|
129
186
|
desc = " ".join(parts[4:]) if len(parts) > 4 else ""
|
|
130
|
-
|
|
187
|
+
|
|
131
188
|
# Check if enabled
|
|
132
189
|
enabled = False
|
|
133
190
|
try:
|
|
134
191
|
en_result = subprocess.run(
|
|
135
192
|
["systemctl", "is-enabled", name],
|
|
136
|
-
capture_output=True,
|
|
193
|
+
capture_output=True,
|
|
194
|
+
text=True,
|
|
195
|
+
timeout=5,
|
|
137
196
|
)
|
|
138
197
|
enabled = en_result.stdout.strip() == "enabled"
|
|
139
198
|
except:
|
|
140
199
|
pass
|
|
141
|
-
|
|
142
|
-
services.append(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
))
|
|
200
|
+
|
|
201
|
+
services.append(
|
|
202
|
+
DetectedService(
|
|
203
|
+
name=name, status=status, description=desc, enabled=enabled
|
|
204
|
+
)
|
|
205
|
+
)
|
|
148
206
|
except Exception:
|
|
149
207
|
pass
|
|
150
|
-
|
|
208
|
+
|
|
151
209
|
return services
|
|
152
|
-
|
|
210
|
+
|
|
153
211
|
def detect_applications(self) -> list:
|
|
154
212
|
"""Detect running applications/processes."""
|
|
155
213
|
applications = []
|
|
156
214
|
seen_names = set()
|
|
157
|
-
|
|
158
|
-
for proc in psutil.process_iter([
|
|
215
|
+
|
|
216
|
+
for proc in psutil.process_iter(["pid", "name", "cmdline", "exe", "cwd", "memory_info"]):
|
|
159
217
|
try:
|
|
160
218
|
info = proc.info
|
|
161
|
-
name = info[
|
|
162
|
-
|
|
219
|
+
name = info["name"] or ""
|
|
220
|
+
|
|
163
221
|
# Filter to interesting processes
|
|
164
|
-
if not any(
|
|
222
|
+
if not any(
|
|
223
|
+
interesting in name.lower() for interesting in self.INTERESTING_PROCESSES
|
|
224
|
+
):
|
|
165
225
|
continue
|
|
166
|
-
|
|
226
|
+
|
|
167
227
|
# Skip duplicates by name (keep first)
|
|
168
228
|
if name in seen_names:
|
|
169
229
|
continue
|
|
170
230
|
seen_names.add(name)
|
|
171
|
-
|
|
172
|
-
cmdline = " ".join(info[
|
|
173
|
-
exe = info[
|
|
174
|
-
cwd = info[
|
|
175
|
-
mem = (info[
|
|
176
|
-
|
|
177
|
-
applications.append(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
231
|
+
|
|
232
|
+
cmdline = " ".join(info["cmdline"] or [])[:200]
|
|
233
|
+
exe = info["exe"] or ""
|
|
234
|
+
cwd = info["cwd"] or ""
|
|
235
|
+
mem = (info["memory_info"].rss / 1024 / 1024) if info["memory_info"] else 0
|
|
236
|
+
|
|
237
|
+
applications.append(
|
|
238
|
+
DetectedApplication(
|
|
239
|
+
name=name,
|
|
240
|
+
pid=info["pid"],
|
|
241
|
+
cmdline=cmdline,
|
|
242
|
+
exe=exe,
|
|
243
|
+
working_dir=cwd,
|
|
244
|
+
memory_mb=round(mem, 1),
|
|
245
|
+
)
|
|
246
|
+
)
|
|
185
247
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
|
186
248
|
continue
|
|
187
|
-
|
|
249
|
+
|
|
188
250
|
# Sort by memory usage
|
|
189
251
|
applications.sort(key=lambda x: x.memory_mb, reverse=True)
|
|
190
252
|
return applications
|
|
191
|
-
|
|
253
|
+
|
|
192
254
|
def detect_paths(self) -> list:
|
|
193
255
|
"""Detect important paths (projects, configs, data)."""
|
|
194
256
|
paths = []
|
|
195
|
-
|
|
257
|
+
|
|
196
258
|
# User home subdirectories
|
|
197
259
|
important_home_dirs = [
|
|
198
260
|
("projects", "project"),
|
|
@@ -217,18 +279,20 @@ class SystemDetector:
|
|
|
217
279
|
("Documents", "data"),
|
|
218
280
|
("Downloads", "data"),
|
|
219
281
|
]
|
|
220
|
-
|
|
282
|
+
|
|
221
283
|
for dirname, path_type in important_home_dirs:
|
|
222
284
|
full_path = self.home / dirname
|
|
223
285
|
if full_path.exists() and full_path.is_dir():
|
|
224
286
|
size = self._get_dir_size(full_path, max_depth=1)
|
|
225
|
-
paths.append(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
287
|
+
paths.append(
|
|
288
|
+
DetectedPath(
|
|
289
|
+
path=str(full_path),
|
|
290
|
+
type=path_type,
|
|
291
|
+
size_mb=round(size / 1024 / 1024, 1),
|
|
292
|
+
description=f"User {dirname}",
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
|
|
232
296
|
# System paths that might be interesting
|
|
233
297
|
system_paths = [
|
|
234
298
|
("/var/www", "data", "Web server root"),
|
|
@@ -239,21 +303,35 @@ class SystemDetector:
|
|
|
239
303
|
("/etc/nginx", "config", "Nginx config"),
|
|
240
304
|
("/etc/apache2", "config", "Apache config"),
|
|
241
305
|
]
|
|
242
|
-
|
|
306
|
+
|
|
243
307
|
for path, path_type, desc in system_paths:
|
|
244
308
|
p = Path(path)
|
|
245
309
|
if p.exists():
|
|
246
310
|
size = self._get_dir_size(p, max_depth=1)
|
|
247
|
-
paths.append(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
311
|
+
paths.append(
|
|
312
|
+
DetectedPath(
|
|
313
|
+
path=path,
|
|
314
|
+
type=path_type,
|
|
315
|
+
size_mb=round(size / 1024 / 1024, 1),
|
|
316
|
+
description=desc,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
|
|
254
320
|
# Detect project directories (with .git, package.json, etc.)
|
|
255
|
-
project_markers = [
|
|
256
|
-
|
|
321
|
+
project_markers = [
|
|
322
|
+
".git",
|
|
323
|
+
"package.json",
|
|
324
|
+
"Cargo.toml",
|
|
325
|
+
"go.mod",
|
|
326
|
+
"pyproject.toml",
|
|
327
|
+
"setup.py",
|
|
328
|
+
]
|
|
329
|
+
for search_dir in [
|
|
330
|
+
self.home / "projects",
|
|
331
|
+
self.home / "code",
|
|
332
|
+
self.home / "github",
|
|
333
|
+
self.home,
|
|
334
|
+
]:
|
|
257
335
|
if search_dir.exists():
|
|
258
336
|
for item in search_dir.iterdir():
|
|
259
337
|
if item.is_dir() and not item.name.startswith("."):
|
|
@@ -261,18 +339,20 @@ class SystemDetector:
|
|
|
261
339
|
if (item / marker).exists():
|
|
262
340
|
size = self._get_dir_size(item, max_depth=2)
|
|
263
341
|
if str(item) not in [p.path for p in paths]:
|
|
264
|
-
paths.append(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
342
|
+
paths.append(
|
|
343
|
+
DetectedPath(
|
|
344
|
+
path=str(item),
|
|
345
|
+
type="project",
|
|
346
|
+
size_mb=round(size / 1024 / 1024, 1),
|
|
347
|
+
description=f"Project ({marker})",
|
|
348
|
+
)
|
|
349
|
+
)
|
|
270
350
|
break
|
|
271
|
-
|
|
351
|
+
|
|
272
352
|
# Sort by type then path
|
|
273
353
|
paths.sort(key=lambda x: (x.type, x.path))
|
|
274
354
|
return paths
|
|
275
|
-
|
|
355
|
+
|
|
276
356
|
def _get_dir_size(self, path: Path, max_depth: int = 2) -> int:
|
|
277
357
|
"""Get approximate directory size in bytes."""
|
|
278
358
|
total = 0
|
|
@@ -290,28 +370,26 @@ class SystemDetector:
|
|
|
290
370
|
except (PermissionError, FileNotFoundError, OSError):
|
|
291
371
|
pass
|
|
292
372
|
return total
|
|
293
|
-
|
|
373
|
+
|
|
294
374
|
def detect_docker_containers(self) -> list:
|
|
295
375
|
"""Detect running Docker containers."""
|
|
296
376
|
containers = []
|
|
297
377
|
try:
|
|
298
378
|
result = subprocess.run(
|
|
299
379
|
["docker", "ps", "--format", "{{.Names}}\t{{.Image}}\t{{.Status}}"],
|
|
300
|
-
capture_output=True,
|
|
380
|
+
capture_output=True,
|
|
381
|
+
text=True,
|
|
382
|
+
timeout=10,
|
|
301
383
|
)
|
|
302
384
|
for line in result.stdout.strip().split("\n"):
|
|
303
385
|
if line:
|
|
304
386
|
parts = line.split("\t")
|
|
305
387
|
if len(parts) >= 3:
|
|
306
|
-
containers.append({
|
|
307
|
-
"name": parts[0],
|
|
308
|
-
"image": parts[1],
|
|
309
|
-
"status": parts[2]
|
|
310
|
-
})
|
|
388
|
+
containers.append({"name": parts[0], "image": parts[1], "status": parts[2]})
|
|
311
389
|
except:
|
|
312
390
|
pass
|
|
313
391
|
return containers
|
|
314
|
-
|
|
392
|
+
|
|
315
393
|
def get_system_info(self) -> dict:
|
|
316
394
|
"""Get basic system information."""
|
|
317
395
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clonebox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
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
|
|
@@ -65,7 +65,28 @@ CloneBox lets you create isolated virtual machines with only the applications, d
|
|
|
65
65
|
|
|
66
66
|
## Installation
|
|
67
67
|
|
|
68
|
-
###
|
|
68
|
+
### Quick Setup (Recommended)
|
|
69
|
+
|
|
70
|
+
Run the setup script to automatically install dependencies and configure the environment:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# Clone the repository
|
|
74
|
+
git clone https://github.com/wronai/clonebox.git
|
|
75
|
+
cd clonebox
|
|
76
|
+
|
|
77
|
+
# Run the setup script
|
|
78
|
+
./setup.sh
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The setup script will:
|
|
82
|
+
- Install all required packages (QEMU, libvirt, Python, etc.)
|
|
83
|
+
- Add your user to the necessary groups
|
|
84
|
+
- Configure libvirt networks
|
|
85
|
+
- Install clonebox in development mode
|
|
86
|
+
|
|
87
|
+
### Manual Installation
|
|
88
|
+
|
|
89
|
+
#### Prerequisites
|
|
69
90
|
|
|
70
91
|
```bash
|
|
71
92
|
# Install libvirt and QEMU/KVM
|
|
@@ -82,7 +103,7 @@ newgrp libvirt
|
|
|
82
103
|
sudo apt install genisoimage
|
|
83
104
|
```
|
|
84
105
|
|
|
85
|
-
|
|
106
|
+
#### Install CloneBox
|
|
86
107
|
|
|
87
108
|
```bash
|
|
88
109
|
# From source
|
|
@@ -270,6 +291,29 @@ clonebox detect --yaml --dedupe
|
|
|
270
291
|
clonebox detect --yaml --dedupe -o my-config.yaml
|
|
271
292
|
```
|
|
272
293
|
|
|
294
|
+
### User Session & Networking
|
|
295
|
+
|
|
296
|
+
CloneBox supports creating VMs in user session (no root required) with automatic network fallback:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
# Create VM in user session (uses ~/.local/share/libvirt/images)
|
|
300
|
+
clonebox clone . --user
|
|
301
|
+
|
|
302
|
+
# Explicitly use user-mode networking (slirp) - works without libvirt network
|
|
303
|
+
clonebox clone . --user --network user
|
|
304
|
+
|
|
305
|
+
# Force libvirt default network (may fail in user session)
|
|
306
|
+
clonebox clone . --network default
|
|
307
|
+
|
|
308
|
+
# Auto mode (default): tries libvirt network, falls back to user-mode if unavailable
|
|
309
|
+
clonebox clone . --network auto
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Network modes:**
|
|
313
|
+
- `auto` (default): Uses libvirt default network if available, otherwise falls back to user-mode (slirp)
|
|
314
|
+
- `default`: Forces use of libvirt default network
|
|
315
|
+
- `user`: Uses user-mode networking (slirp) - no bridge setup required
|
|
316
|
+
|
|
273
317
|
## Commands Reference
|
|
274
318
|
|
|
275
319
|
| Command | Description |
|
|
@@ -278,6 +322,9 @@ clonebox detect --yaml --dedupe -o my-config.yaml
|
|
|
278
322
|
| `clonebox clone <path>` | Generate `.clonebox.yaml` from path + running processes |
|
|
279
323
|
| `clonebox clone . --run` | Clone and immediately start VM |
|
|
280
324
|
| `clonebox clone . --edit` | Clone, edit config, then create |
|
|
325
|
+
| `clonebox clone . --user` | Clone in user session (no root) |
|
|
326
|
+
| `clonebox clone . --network user` | Use user-mode networking (slirp) |
|
|
327
|
+
| `clonebox clone . --network auto` | Auto-detect network mode (default) |
|
|
281
328
|
| `clonebox start .` | Start VM from `.clonebox.yaml` in current dir |
|
|
282
329
|
| `clonebox start <name>` | Start existing VM by name |
|
|
283
330
|
| `clonebox stop <name>` | Stop a VM (graceful shutdown) |
|
|
@@ -296,6 +343,66 @@ clonebox detect --yaml --dedupe -o my-config.yaml
|
|
|
296
343
|
- Python 3.8+
|
|
297
344
|
- User in `libvirt` group
|
|
298
345
|
|
|
346
|
+
## Troubleshooting
|
|
347
|
+
|
|
348
|
+
### Network Issues
|
|
349
|
+
|
|
350
|
+
If you encounter "Network not found" or "network 'default' is not active" errors:
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
# Option 1: Use user-mode networking (no setup required)
|
|
354
|
+
clonebox clone . --user --network user
|
|
355
|
+
|
|
356
|
+
# Option 2: Run the network fix script
|
|
357
|
+
./fix-network.sh
|
|
358
|
+
|
|
359
|
+
# Or manually fix:
|
|
360
|
+
virsh --connect qemu:///session net-destroy default 2>/dev/null
|
|
361
|
+
virsh --connect qemu:///session net-undefine default 2>/dev/null
|
|
362
|
+
virsh --connect qemu:///session net-define /tmp/default-network.xml
|
|
363
|
+
virsh --connect qemu:///session net-start default
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Permission Issues
|
|
367
|
+
|
|
368
|
+
If you get permission errors:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# Ensure user is in libvirt and kvm groups
|
|
372
|
+
sudo usermod -aG libvirt $USER
|
|
373
|
+
sudo usermod -aG kvm $USER
|
|
374
|
+
|
|
375
|
+
# Log out and log back in for groups to take effect
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### VM Already Exists
|
|
379
|
+
|
|
380
|
+
If you get "domain already exists" error:
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
# List VMs
|
|
384
|
+
clonebox list
|
|
385
|
+
|
|
386
|
+
# Stop and delete the existing VM
|
|
387
|
+
clonebox delete <vm-name>
|
|
388
|
+
|
|
389
|
+
# Or use virsh directly
|
|
390
|
+
virsh --connect qemu:///session destroy <vm-name>
|
|
391
|
+
virsh --connect qemu:///session undefine <vm-name>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### virt-viewer not found
|
|
395
|
+
|
|
396
|
+
If GUI doesn't open:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
# Install virt-viewer
|
|
400
|
+
sudo apt install virt-viewer
|
|
401
|
+
|
|
402
|
+
# Then connect manually
|
|
403
|
+
virt-viewer --connect qemu:///session <vm-name>
|
|
404
|
+
```
|
|
405
|
+
|
|
299
406
|
## License
|
|
300
407
|
|
|
301
408
|
MIT License - see [LICENSE](LICENSE) file.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
clonebox/__init__.py,sha256=IOk7G0DiSQ33EGbFC0xbnnFB9aou_6yuyFxvycQEvA0,407
|
|
2
|
+
clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
|
|
3
|
+
clonebox/cli.py,sha256=gtnFl0Jibfwuy4uMF9_BPTOvbHzTgpXsy71WwLEtY64,31781
|
|
4
|
+
clonebox/cloner.py,sha256=Uoh9mCUX-3p2tFL_3qlf2R2232JCXO5YhWrgKTpEr0s,19369
|
|
5
|
+
clonebox/detector.py,sha256=jkzENmi4720n5e04k6gM7MNvXbQdYX-z1_O3Id0WK9w,12505
|
|
6
|
+
clonebox-0.1.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
7
|
+
clonebox-0.1.4.dist-info/METADATA,sha256=YQg_Br_7JJcWvhNTKhWokMXftG2Xht_zfPQ_zmJuzSQ,13126
|
|
8
|
+
clonebox-0.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
9
|
+
clonebox-0.1.4.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
|
|
10
|
+
clonebox-0.1.4.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
|
|
11
|
+
clonebox-0.1.4.dist-info/RECORD,,
|
clonebox-0.1.2.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
clonebox/__init__.py,sha256=IOk7G0DiSQ33EGbFC0xbnnFB9aou_6yuyFxvycQEvA0,407
|
|
2
|
-
clonebox/cli.py,sha256=Zk9D99G2Zcaeb0Pw3eNhv0EtLYKPcpE0GyB3QtuhvgQ,31625
|
|
3
|
-
clonebox/cloner.py,sha256=qfMpx7tS5Eozvhi2ZzBc5GY6HLYotncuMakeknHnTwo,18099
|
|
4
|
-
clonebox/detector.py,sha256=Umg4CRJU61yV3a1AvR_0tOfjBMCCIbiQdDAAhlrOL5k,11916
|
|
5
|
-
clonebox-0.1.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
6
|
-
clonebox-0.1.2.dist-info/METADATA,sha256=YtSqXudDO6TrPtAZzbP5fbMvf-PHarUpkY55ZGpHplw,10374
|
|
7
|
-
clonebox-0.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
8
|
-
clonebox-0.1.2.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
|
|
9
|
-
clonebox-0.1.2.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
|
|
10
|
-
clonebox-0.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|