clonebox 1.1.4__py3-none-any.whl → 1.1.5__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/backends/libvirt_backend.py +217 -0
- clonebox/backends/qemu_disk.py +52 -0
- clonebox/backends/subprocess_runner.py +56 -0
- clonebox/cli.py +209 -30
- clonebox/cloner.py +327 -189
- clonebox/di.py +176 -0
- clonebox/health/__init__.py +2 -1
- clonebox/health/manager.py +328 -0
- clonebox/health/probes.py +337 -0
- clonebox/interfaces/disk.py +40 -0
- clonebox/interfaces/hypervisor.py +89 -0
- clonebox/interfaces/network.py +33 -0
- clonebox/interfaces/process.py +46 -0
- clonebox/logging.py +125 -0
- clonebox/monitor.py +1 -3
- clonebox/p2p.py +4 -2
- clonebox/resource_monitor.py +162 -0
- clonebox/resources.py +222 -0
- clonebox/rollback.py +172 -0
- clonebox/secrets.py +331 -0
- clonebox/snapshots/manager.py +3 -9
- clonebox/snapshots/models.py +2 -6
- {clonebox-1.1.4.dist-info → clonebox-1.1.5.dist-info}/METADATA +51 -2
- clonebox-1.1.5.dist-info/RECORD +42 -0
- clonebox-1.1.4.dist-info/RECORD +0 -27
- {clonebox-1.1.4.dist-info → clonebox-1.1.5.dist-info}/WHEEL +0 -0
- {clonebox-1.1.4.dist-info → clonebox-1.1.5.dist-info}/entry_points.txt +0 -0
- {clonebox-1.1.4.dist-info → clonebox-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {clonebox-1.1.4.dist-info → clonebox-1.1.5.dist-info}/top_level.txt +0 -0
clonebox/secrets.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Secrets management for CloneBox.
|
|
3
|
+
Supports multiple backends: env, vault, sops, age.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import secrets
|
|
9
|
+
import string
|
|
10
|
+
import subprocess
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SecretValue:
|
|
19
|
+
"""Represents a secret value with metadata."""
|
|
20
|
+
|
|
21
|
+
key: str
|
|
22
|
+
value: str
|
|
23
|
+
source: str # 'env', 'vault', 'sops', 'generated'
|
|
24
|
+
expires_at: Optional[str] = None
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return f"SecretValue(key={self.key}, source={self.source}, value=***)"
|
|
28
|
+
|
|
29
|
+
def redacted(self) -> str:
|
|
30
|
+
"""Return redacted version for logging."""
|
|
31
|
+
return f"{self.value[:2]}***{self.value[-2:]}" if len(self.value) > 4 else "***"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SecretsProvider(ABC):
|
|
35
|
+
"""Abstract base class for secrets providers."""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_secret(self, key: str) -> Optional[SecretValue]:
|
|
39
|
+
"""Retrieve a secret by key."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def list_secrets(self) -> List[str]:
|
|
44
|
+
"""List available secret keys."""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def is_available(self) -> bool:
|
|
49
|
+
"""Check if this provider is configured and available."""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EnvSecretsProvider(SecretsProvider):
|
|
54
|
+
"""Load secrets from environment variables and .env files."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, env_file: Optional[Path] = None):
|
|
57
|
+
self.env_file = env_file or Path(".env")
|
|
58
|
+
self._cache: Dict[str, str] = {}
|
|
59
|
+
self._load_env_file()
|
|
60
|
+
|
|
61
|
+
def _load_env_file(self) -> None:
|
|
62
|
+
if self.env_file.exists():
|
|
63
|
+
with open(self.env_file) as f:
|
|
64
|
+
for line in f:
|
|
65
|
+
line = line.strip()
|
|
66
|
+
if line and not line.startswith("#") and "=" in line:
|
|
67
|
+
key, _, value = line.partition("=")
|
|
68
|
+
self._cache[key.strip()] = value.strip().strip("'\"")
|
|
69
|
+
|
|
70
|
+
def get_secret(self, key: str) -> Optional[SecretValue]:
|
|
71
|
+
# Check environment first, then cache from file
|
|
72
|
+
value = os.environ.get(key) or self._cache.get(key)
|
|
73
|
+
if value:
|
|
74
|
+
return SecretValue(key=key, value=value, source="env")
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def list_secrets(self) -> List[str]:
|
|
78
|
+
return list(
|
|
79
|
+
set(
|
|
80
|
+
list(self._cache.keys())
|
|
81
|
+
+ [
|
|
82
|
+
k
|
|
83
|
+
for k in os.environ.keys()
|
|
84
|
+
if k.startswith("VM_") or k.startswith("CLONEBOX_")
|
|
85
|
+
]
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def is_available(self) -> bool:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class VaultSecretsProvider(SecretsProvider):
|
|
94
|
+
"""Load secrets from HashiCorp Vault."""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
addr: Optional[str] = None,
|
|
99
|
+
token: Optional[str] = None,
|
|
100
|
+
path_prefix: str = "secret/clonebox",
|
|
101
|
+
):
|
|
102
|
+
self.addr = addr or os.environ.get("VAULT_ADDR", "http://127.0.0.1:8200")
|
|
103
|
+
self.token = token or os.environ.get("VAULT_TOKEN")
|
|
104
|
+
self.path_prefix = path_prefix
|
|
105
|
+
self._client = None
|
|
106
|
+
|
|
107
|
+
def _get_client(self):
|
|
108
|
+
if self._client is None:
|
|
109
|
+
try:
|
|
110
|
+
import hvac
|
|
111
|
+
|
|
112
|
+
self._client = hvac.Client(url=self.addr, token=self.token)
|
|
113
|
+
except ImportError:
|
|
114
|
+
raise RuntimeError(
|
|
115
|
+
"hvac package required for Vault support: pip install hvac"
|
|
116
|
+
)
|
|
117
|
+
return self._client
|
|
118
|
+
|
|
119
|
+
def get_secret(self, key: str) -> Optional[SecretValue]:
|
|
120
|
+
try:
|
|
121
|
+
client = self._get_client()
|
|
122
|
+
path = f"{self.path_prefix}/{key}"
|
|
123
|
+
response = client.secrets.kv.v2.read_secret_version(path=path)
|
|
124
|
+
value = response["data"]["data"].get("value")
|
|
125
|
+
if value:
|
|
126
|
+
return SecretValue(key=key, value=value, source="vault")
|
|
127
|
+
except Exception:
|
|
128
|
+
pass
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def list_secrets(self) -> List[str]:
|
|
132
|
+
try:
|
|
133
|
+
client = self._get_client()
|
|
134
|
+
response = client.secrets.kv.v2.list_secrets(path=self.path_prefix)
|
|
135
|
+
return response["data"]["keys"]
|
|
136
|
+
except Exception:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
def is_available(self) -> bool:
|
|
140
|
+
try:
|
|
141
|
+
client = self._get_client()
|
|
142
|
+
return client.is_authenticated()
|
|
143
|
+
except Exception:
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class SOPSSecretsProvider(SecretsProvider):
|
|
148
|
+
"""Load secrets from SOPS-encrypted files."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, secrets_file: Optional[Path] = None):
|
|
151
|
+
self.secrets_file = secrets_file or Path(".clonebox.secrets.yaml")
|
|
152
|
+
self._cache: Optional[Dict[str, Any]] = None
|
|
153
|
+
|
|
154
|
+
def _decrypt(self) -> Dict[str, Any]:
|
|
155
|
+
if self._cache is not None:
|
|
156
|
+
return self._cache
|
|
157
|
+
|
|
158
|
+
if not self.secrets_file.exists():
|
|
159
|
+
self._cache = {}
|
|
160
|
+
return self._cache
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
result = subprocess.run(
|
|
164
|
+
["sops", "-d", str(self.secrets_file)],
|
|
165
|
+
capture_output=True,
|
|
166
|
+
text=True,
|
|
167
|
+
check=True,
|
|
168
|
+
)
|
|
169
|
+
import yaml
|
|
170
|
+
|
|
171
|
+
self._cache = yaml.safe_load(result.stdout) or {}
|
|
172
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
173
|
+
self._cache = {}
|
|
174
|
+
|
|
175
|
+
return self._cache
|
|
176
|
+
|
|
177
|
+
def get_secret(self, key: str) -> Optional[SecretValue]:
|
|
178
|
+
data = self._decrypt()
|
|
179
|
+
# Support nested keys: "vm.password" -> data['vm']['password']
|
|
180
|
+
parts = key.split(".")
|
|
181
|
+
value = data
|
|
182
|
+
for part in parts:
|
|
183
|
+
if isinstance(value, dict) and part in value:
|
|
184
|
+
value = value[part]
|
|
185
|
+
else:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
if isinstance(value, str):
|
|
189
|
+
return SecretValue(key=key, value=value, source="sops")
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
def list_secrets(self) -> List[str]:
|
|
193
|
+
data = self._decrypt()
|
|
194
|
+
return list(data.keys())
|
|
195
|
+
|
|
196
|
+
def is_available(self) -> bool:
|
|
197
|
+
try:
|
|
198
|
+
subprocess.run(["sops", "--version"], capture_output=True, check=True)
|
|
199
|
+
return self.secrets_file.exists()
|
|
200
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class SecretsManager:
|
|
205
|
+
"""
|
|
206
|
+
Unified secrets management with multiple provider fallback.
|
|
207
|
+
|
|
208
|
+
Usage:
|
|
209
|
+
secrets = SecretsManager()
|
|
210
|
+
password = secrets.get('VM_PASSWORD')
|
|
211
|
+
|
|
212
|
+
# Or with explicit provider
|
|
213
|
+
secrets = SecretsManager(provider='vault')
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
PROVIDERS = {
|
|
217
|
+
"env": EnvSecretsProvider,
|
|
218
|
+
"vault": VaultSecretsProvider,
|
|
219
|
+
"sops": SOPSSecretsProvider,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
def __init__(self, provider: Optional[str] = None, **kwargs):
|
|
223
|
+
self._providers: List[SecretsProvider] = []
|
|
224
|
+
|
|
225
|
+
if provider:
|
|
226
|
+
# Use specific provider
|
|
227
|
+
if provider not in self.PROVIDERS:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"Unknown provider: {provider}. Available: {list(self.PROVIDERS.keys())}"
|
|
230
|
+
)
|
|
231
|
+
self._providers = [self.PROVIDERS[provider](**kwargs)]
|
|
232
|
+
else:
|
|
233
|
+
# Auto-detect and use all available providers
|
|
234
|
+
for name, cls in self.PROVIDERS.items():
|
|
235
|
+
try:
|
|
236
|
+
instance = cls(**kwargs)
|
|
237
|
+
if instance.is_available():
|
|
238
|
+
self._providers.append(instance)
|
|
239
|
+
except Exception:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
# Always have env as fallback
|
|
243
|
+
if not any(isinstance(p, EnvSecretsProvider) for p in self._providers):
|
|
244
|
+
self._providers.append(EnvSecretsProvider())
|
|
245
|
+
|
|
246
|
+
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
|
247
|
+
"""Get a secret value, trying each provider in order."""
|
|
248
|
+
for provider in self._providers:
|
|
249
|
+
secret = provider.get_secret(key)
|
|
250
|
+
if secret:
|
|
251
|
+
return secret.value
|
|
252
|
+
return default
|
|
253
|
+
|
|
254
|
+
def get_secret(self, key: str) -> Optional[SecretValue]:
|
|
255
|
+
"""Get a SecretValue with metadata."""
|
|
256
|
+
for provider in self._providers:
|
|
257
|
+
secret = provider.get_secret(key)
|
|
258
|
+
if secret:
|
|
259
|
+
return secret
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
def require(self, key: str) -> str:
|
|
263
|
+
"""Get a secret or raise an error if not found."""
|
|
264
|
+
value = self.get(key)
|
|
265
|
+
if value is None:
|
|
266
|
+
raise ValueError(f"Required secret '{key}' not found in any provider")
|
|
267
|
+
return value
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def generate_password(length: int = 24) -> str:
|
|
271
|
+
"""Generate a secure random password."""
|
|
272
|
+
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
|
|
273
|
+
return "".join(secrets.choice(alphabet) for _ in range(length))
|
|
274
|
+
|
|
275
|
+
@staticmethod
|
|
276
|
+
def generate_one_time_password() -> Tuple[str, str]:
|
|
277
|
+
"""
|
|
278
|
+
Generate a one-time password that expires on first login.
|
|
279
|
+
Returns (password, cloud-init chpasswd config)
|
|
280
|
+
"""
|
|
281
|
+
password = SecretsManager.generate_password(16)
|
|
282
|
+
# Force password change on first login
|
|
283
|
+
chpasswd_config = f"""
|
|
284
|
+
chpasswd:
|
|
285
|
+
expire: true
|
|
286
|
+
users:
|
|
287
|
+
- name: ubuntu
|
|
288
|
+
password: {password}
|
|
289
|
+
type: text
|
|
290
|
+
"""
|
|
291
|
+
return password, chpasswd_config
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# SSH Key Management
|
|
295
|
+
@dataclass
|
|
296
|
+
class SSHKeyPair:
|
|
297
|
+
"""SSH key pair for VM authentication."""
|
|
298
|
+
|
|
299
|
+
private_key: str
|
|
300
|
+
public_key: str
|
|
301
|
+
key_type: str = "ed25519"
|
|
302
|
+
|
|
303
|
+
@classmethod
|
|
304
|
+
def generate(cls, key_type: str = "ed25519") -> "SSHKeyPair":
|
|
305
|
+
"""Generate a new SSH key pair."""
|
|
306
|
+
import tempfile
|
|
307
|
+
|
|
308
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
309
|
+
key_path = Path(tmpdir) / "key"
|
|
310
|
+
subprocess.run(
|
|
311
|
+
["ssh-keygen", "-t", key_type, "-N", "", "-f", str(key_path), "-q"],
|
|
312
|
+
check=True,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
private_key = key_path.read_text()
|
|
316
|
+
public_key = key_path.with_suffix(".pub").read_text()
|
|
317
|
+
|
|
318
|
+
return cls(private_key=private_key, public_key=public_key, key_type=key_type)
|
|
319
|
+
|
|
320
|
+
@classmethod
|
|
321
|
+
def from_file(cls, private_key_path: Path) -> "SSHKeyPair":
|
|
322
|
+
"""Load existing SSH key pair."""
|
|
323
|
+
private_key = private_key_path.read_text()
|
|
324
|
+
public_key = private_key_path.with_suffix(".pub").read_text()
|
|
325
|
+
return cls(private_key=private_key, public_key=public_key)
|
|
326
|
+
|
|
327
|
+
def save(self, private_key_path: Path) -> None:
|
|
328
|
+
"""Save key pair to files."""
|
|
329
|
+
private_key_path.write_text(self.private_key)
|
|
330
|
+
private_key_path.chmod(0o600)
|
|
331
|
+
private_key_path.with_suffix(".pub").write_text(self.public_key)
|
clonebox/snapshots/manager.py
CHANGED
|
@@ -86,9 +86,7 @@ class SnapshotManager:
|
|
|
86
86
|
auto_created=auto_policy is not None,
|
|
87
87
|
auto_policy=auto_policy,
|
|
88
88
|
expires_at=(
|
|
89
|
-
datetime.now() + timedelta(days=expires_in_days)
|
|
90
|
-
if expires_in_days
|
|
91
|
-
else None
|
|
89
|
+
datetime.now() + timedelta(days=expires_in_days) if expires_in_days else None
|
|
92
90
|
),
|
|
93
91
|
)
|
|
94
92
|
|
|
@@ -114,9 +112,7 @@ class SnapshotManager:
|
|
|
114
112
|
|
|
115
113
|
# Check if VM is running
|
|
116
114
|
if domain.isActive() and not force:
|
|
117
|
-
raise RuntimeError(
|
|
118
|
-
f"VM '{vm_name}' is running. Stop it first or use --force"
|
|
119
|
-
)
|
|
115
|
+
raise RuntimeError(f"VM '{vm_name}' is running. Stop it first or use --force")
|
|
120
116
|
|
|
121
117
|
try:
|
|
122
118
|
snap = domain.snapshotLookupByName(name)
|
|
@@ -331,9 +327,7 @@ class SnapshotManager:
|
|
|
331
327
|
meta_file = vm_dir / f"{snapshot.name}.json"
|
|
332
328
|
meta_file.write_text(json.dumps(snapshot.to_dict(), indent=2))
|
|
333
329
|
|
|
334
|
-
def _load_snapshot_metadata(
|
|
335
|
-
self, vm_name: str, name: str
|
|
336
|
-
) -> Optional[Dict[str, Any]]:
|
|
330
|
+
def _load_snapshot_metadata(self, vm_name: str, name: str) -> Optional[Dict[str, Any]]:
|
|
337
331
|
"""Load snapshot metadata from disk."""
|
|
338
332
|
meta_file = self._snapshots_dir / vm_name / f"{name}.json"
|
|
339
333
|
if meta_file.exists():
|
clonebox/snapshots/models.py
CHANGED
|
@@ -97,9 +97,7 @@ class Snapshot:
|
|
|
97
97
|
auto_created=data.get("auto_created", False),
|
|
98
98
|
auto_policy=data.get("auto_policy"),
|
|
99
99
|
expires_at=(
|
|
100
|
-
datetime.fromisoformat(data["expires_at"])
|
|
101
|
-
if data.get("expires_at")
|
|
102
|
-
else None
|
|
100
|
+
datetime.fromisoformat(data["expires_at"]) if data.get("expires_at") else None
|
|
103
101
|
),
|
|
104
102
|
)
|
|
105
103
|
|
|
@@ -167,9 +165,7 @@ class SnapshotPolicy:
|
|
|
167
165
|
max_snapshots=data.get("max_snapshots", 10),
|
|
168
166
|
max_age_days=data.get("max_age_days", 30),
|
|
169
167
|
min_snapshots=data.get("min_snapshots", 1),
|
|
170
|
-
before_operations=data.get(
|
|
171
|
-
"before_operations", ["upgrade", "resize", "config-change"]
|
|
172
|
-
),
|
|
168
|
+
before_operations=data.get("before_operations", ["upgrade", "resize", "config-change"]),
|
|
173
169
|
scheduled_interval_hours=data.get("scheduled_interval_hours"),
|
|
174
170
|
name_prefix=data.get("name_prefix", "auto-"),
|
|
175
171
|
include_timestamp=data.get("include_timestamp", True),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clonebox
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.5
|
|
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
|
|
@@ -32,6 +32,7 @@ Requires-Dist: psutil>=5.9.0
|
|
|
32
32
|
Requires-Dist: pyyaml>=6.0
|
|
33
33
|
Requires-Dist: pydantic>=2.0.0
|
|
34
34
|
Requires-Dist: python-dotenv>=1.0.0
|
|
35
|
+
Requires-Dist: cryptography>=42.0.0
|
|
35
36
|
Provides-Extra: dev
|
|
36
37
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
37
38
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -114,6 +115,8 @@ CloneBox excels in scenarios where developers need:
|
|
|
114
115
|
| 🎛️ Profiles System (`ml-dev`, `web-stack`) | ✅ Stable |
|
|
115
116
|
| 🔍 Auto-detection (services, apps, paths) | ✅ Stable |
|
|
116
117
|
| 🔒 P2P Secure Transfer (AES-256) | ✅ **NEW** |
|
|
118
|
+
| 📸 Snapshot Management | ✅ **NEW** |
|
|
119
|
+
| 🏥 Health Check System | ✅ **NEW** |
|
|
117
120
|
| 🧪 95%+ Test Coverage | ✅ |
|
|
118
121
|
|
|
119
122
|
### P2P Secure VM Sharing
|
|
@@ -141,9 +144,55 @@ clonebox sync-key user@hostB # Sync encryption key
|
|
|
141
144
|
clonebox list-remote user@hostB # List remote VMs
|
|
142
145
|
```
|
|
143
146
|
|
|
147
|
+
### Snapshot Management
|
|
148
|
+
|
|
149
|
+
Save and restore VM states:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Create snapshot before risky operation
|
|
153
|
+
clonebox snapshot create my-vm --name "before-upgrade" --user
|
|
154
|
+
|
|
155
|
+
# List all snapshots
|
|
156
|
+
clonebox snapshot list my-vm --user
|
|
157
|
+
|
|
158
|
+
# Restore to previous state
|
|
159
|
+
clonebox snapshot restore my-vm --name "before-upgrade" --user
|
|
160
|
+
|
|
161
|
+
# Delete old snapshot
|
|
162
|
+
clonebox snapshot delete my-vm --name "before-upgrade" --user
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Health Checks
|
|
166
|
+
|
|
167
|
+
Configure health probes in `.clonebox.yaml`:
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
health_checks:
|
|
171
|
+
- name: nginx
|
|
172
|
+
type: http
|
|
173
|
+
url: http://localhost:80/health
|
|
174
|
+
expected_status: 200
|
|
175
|
+
|
|
176
|
+
- name: postgres
|
|
177
|
+
type: tcp
|
|
178
|
+
host: localhost
|
|
179
|
+
port: 5432
|
|
180
|
+
|
|
181
|
+
- name: redis
|
|
182
|
+
type: command
|
|
183
|
+
exec: "redis-cli ping"
|
|
184
|
+
expected_output: "PONG"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Run health checks:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
clonebox health my-vm --user
|
|
191
|
+
```
|
|
192
|
+
|
|
144
193
|
### Roadmap
|
|
145
194
|
|
|
146
|
-
- **v1.2.0**:
|
|
195
|
+
- **v1.2.0**: Resource limits, progress bars, secrets isolation
|
|
147
196
|
- **v1.3.0**: Multi-VM orchestration, cluster mode
|
|
148
197
|
- **v2.0.0**: Cloud provider support (AWS, GCP, Azure), Windows WSL2 support
|
|
149
198
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
|
|
2
|
+
clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
|
|
3
|
+
clonebox/cli.py,sha256=m2WBz1SDTgpeDvX-dc93bDp4CKp7AcEI17vXWx3D4os,136109
|
|
4
|
+
clonebox/cloner.py,sha256=aH7BwmLgPITHzyHn_n_AKqtEADDm_aUXiRG-nB2rxMQ,90063
|
|
5
|
+
clonebox/container.py,sha256=tiYK1ZB-DhdD6A2FuMA0h_sRNkUI7KfYcJ0tFOcdyeM,6105
|
|
6
|
+
clonebox/dashboard.py,sha256=dMY6odvPq3j6FronhRRsX7aY3qdCwznB-aCWKEmHDNw,5768
|
|
7
|
+
clonebox/detector.py,sha256=vS65cvFNPmUBCX1Y_TMTnSRljw6r1Ae9dlVtACs5XFc,23075
|
|
8
|
+
clonebox/di.py,sha256=feFMXP5ff-7gwrIqgnoCpk1ivaiZA_lv2wcpkCSKiew,5648
|
|
9
|
+
clonebox/exporter.py,sha256=WIzVvmA0z_jjrpyXxvnXoLp9oaW6fKS7k0PGwzx_PIM,5629
|
|
10
|
+
clonebox/importer.py,sha256=Q9Uk1IOA41mgGhU4ynW2k-h9GEoGxRKI3c9wWE4uxcA,7097
|
|
11
|
+
clonebox/logging.py,sha256=jD_WgkHmt_h99EmX82QWK7D0OKWyxqcLwgT4UDQFp2g,3675
|
|
12
|
+
clonebox/models.py,sha256=zwejkNtEEO_aPy_Q5UzXG5tszU-c7lkqh9LQus9eWMo,8307
|
|
13
|
+
clonebox/monitor.py,sha256=zlIarNf8w_i34XI8hZGxxrg5PVZK_Yxm6FQnkhLavRI,9181
|
|
14
|
+
clonebox/p2p.py,sha256=6o0JnscKqF9-BftQhW5fF1W6YY1wXshY9LEklNcHGJc,5913
|
|
15
|
+
clonebox/profiles.py,sha256=UP37fX_rhrG_O9ehNFJBUcULPmUtN1A8KsJ6cM44oK0,1986
|
|
16
|
+
clonebox/resource_monitor.py,sha256=lDR9KyPbVtImeeOkOBPPVP-5yCgoL5hsVFPZ_UqsY0w,5286
|
|
17
|
+
clonebox/resources.py,sha256=IkuM4OdSDV4qhyc0eIynwbAHBTv0aVSxxW-gghsnCAs,6815
|
|
18
|
+
clonebox/rollback.py,sha256=hpwO-8Ehe1pW0wHuZvJkC_qxZ6yEo9otCJRhGIUArCo,5711
|
|
19
|
+
clonebox/secrets.py,sha256=QtGMIjhTKq14qQRGy-pX0TU3vKLiZt7CMloAlF2GmzQ,10515
|
|
20
|
+
clonebox/validator.py,sha256=CF4hMlY69-AGRH5HdG8HAA9_LNCwDKD4xPlYQPWJ9Rw,36647
|
|
21
|
+
clonebox/backends/libvirt_backend.py,sha256=sIHFIvFO1hIOXEFR_foSkOGBgIzaJVQs-njOU8GdafA,7170
|
|
22
|
+
clonebox/backends/qemu_disk.py,sha256=YsGjYX5sbEf35Y4yjTpNkZat73a4RGBxY-KTVzJhqIs,1687
|
|
23
|
+
clonebox/backends/subprocess_runner.py,sha256=c-IyaMxM1cmUu64h654oAvulm83K5Mu-VQxXJ_0BOds,1506
|
|
24
|
+
clonebox/health/__init__.py,sha256=aKJJPQwJLnoCY728QuKUxYx1TZyooGEnyUVOegZ58Ok,422
|
|
25
|
+
clonebox/health/manager.py,sha256=6nn0a8QtxeEuuafDbn5ZBqHQdaJ2qg7yTstyAGPJWP0,9987
|
|
26
|
+
clonebox/health/models.py,sha256=sPumwj8S-88KgzSGw1Kq9bBbPVRd2RR0R87Z8hKJ_28,6001
|
|
27
|
+
clonebox/health/probes.py,sha256=CkiGcayqRRysqaBJst-YpSrvUzMdwsqD4TiQSluLt3Y,11305
|
|
28
|
+
clonebox/interfaces/disk.py,sha256=F7Xzj2dq5UTZ2KGCuThDM8bwTps6chFbquOUmfLREjI,985
|
|
29
|
+
clonebox/interfaces/hypervisor.py,sha256=8ms4kZLA-5Ba1e_n68mCucwP_K9mufbmTBlo7XzURn4,1991
|
|
30
|
+
clonebox/interfaces/network.py,sha256=YPIquxEB7sZHczbpuopcZpffTjWYI6cKmAu3wAEFllk,853
|
|
31
|
+
clonebox/interfaces/process.py,sha256=njvAIZw_TCjw01KpyVQKIDoRvhTwl0FfVGbQ6mxTROk,1024
|
|
32
|
+
clonebox/snapshots/__init__.py,sha256=ndlrIavPAiA8z4Ep3-D_EPhOcjNKYFnP3rIpEKaGdb8,273
|
|
33
|
+
clonebox/snapshots/manager.py,sha256=hGzM8V6ZJPXjTqj47c4Kr8idlE-c1Q3gPUvuw1HvS1A,11393
|
|
34
|
+
clonebox/snapshots/models.py,sha256=sRnn3OZE8JG9FZJlRuA3ihO-JXoPCQ3nD3SQytflAao,6206
|
|
35
|
+
clonebox/templates/profiles/ml-dev.yaml,sha256=w07MToGh31xtxpjbeXTBk9BkpAN8A3gv8HeA3ESKG9M,461
|
|
36
|
+
clonebox/templates/profiles/web-stack.yaml,sha256=EBnnGMzML5vAjXmIUbCpbTCwmRaNJiuWd3EcL43DOK8,485
|
|
37
|
+
clonebox-1.1.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
38
|
+
clonebox-1.1.5.dist-info/METADATA,sha256=0sp9wPpm969pIM4iwq3m22B3uxh0vC694mJSIRjrUoc,48882
|
|
39
|
+
clonebox-1.1.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
40
|
+
clonebox-1.1.5.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
|
|
41
|
+
clonebox-1.1.5.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
|
|
42
|
+
clonebox-1.1.5.dist-info/RECORD,,
|
clonebox-1.1.4.dist-info/RECORD
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
|
|
2
|
-
clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
|
|
3
|
-
clonebox/cli.py,sha256=MeW_Jnbmrp9QrfA72a2pydeeINCY_LNBqJEGaD2rxtE,128687
|
|
4
|
-
clonebox/cloner.py,sha256=2YQO4SHCv0xOsU1hL9IqdgmxxJN-2j75X9pe-LpTpJE,82696
|
|
5
|
-
clonebox/container.py,sha256=tiYK1ZB-DhdD6A2FuMA0h_sRNkUI7KfYcJ0tFOcdyeM,6105
|
|
6
|
-
clonebox/dashboard.py,sha256=dMY6odvPq3j6FronhRRsX7aY3qdCwznB-aCWKEmHDNw,5768
|
|
7
|
-
clonebox/detector.py,sha256=vS65cvFNPmUBCX1Y_TMTnSRljw6r1Ae9dlVtACs5XFc,23075
|
|
8
|
-
clonebox/exporter.py,sha256=WIzVvmA0z_jjrpyXxvnXoLp9oaW6fKS7k0PGwzx_PIM,5629
|
|
9
|
-
clonebox/importer.py,sha256=Q9Uk1IOA41mgGhU4ynW2k-h9GEoGxRKI3c9wWE4uxcA,7097
|
|
10
|
-
clonebox/models.py,sha256=zwejkNtEEO_aPy_Q5UzXG5tszU-c7lkqh9LQus9eWMo,8307
|
|
11
|
-
clonebox/monitor.py,sha256=KQKi63mcz6KULi2SpD5oi1g05CKaFTC2dAyyRJtJX-E,9211
|
|
12
|
-
clonebox/p2p.py,sha256=LPQQ7wNO84yDnpVrGkaRU-FDUzqmC4URdZXVeHsNOew,5889
|
|
13
|
-
clonebox/profiles.py,sha256=UP37fX_rhrG_O9ehNFJBUcULPmUtN1A8KsJ6cM44oK0,1986
|
|
14
|
-
clonebox/validator.py,sha256=CF4hMlY69-AGRH5HdG8HAA9_LNCwDKD4xPlYQPWJ9Rw,36647
|
|
15
|
-
clonebox/health/__init__.py,sha256=hW6MB8qc3pE-Jub1Djnz2G1AGs4Tn4Y2FbuYur6m8aE,394
|
|
16
|
-
clonebox/health/models.py,sha256=sPumwj8S-88KgzSGw1Kq9bBbPVRd2RR0R87Z8hKJ_28,6001
|
|
17
|
-
clonebox/snapshots/__init__.py,sha256=ndlrIavPAiA8z4Ep3-D_EPhOcjNKYFnP3rIpEKaGdb8,273
|
|
18
|
-
clonebox/snapshots/manager.py,sha256=FuJB_q9fUs7GScVdX5vePezBDI9m8zwIrG1BDFvjeNM,11469
|
|
19
|
-
clonebox/snapshots/models.py,sha256=upJhlHLYFWBrMzCMI8Zzd1z66JRV69R2qLDCTrDtJUY,6268
|
|
20
|
-
clonebox/templates/profiles/ml-dev.yaml,sha256=w07MToGh31xtxpjbeXTBk9BkpAN8A3gv8HeA3ESKG9M,461
|
|
21
|
-
clonebox/templates/profiles/web-stack.yaml,sha256=EBnnGMzML5vAjXmIUbCpbTCwmRaNJiuWd3EcL43DOK8,485
|
|
22
|
-
clonebox-1.1.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
23
|
-
clonebox-1.1.4.dist-info/METADATA,sha256=TfNWk7EAyhCtPtEoyERrJz2f2Xhn6Lg08DjfC6zuj0E,47947
|
|
24
|
-
clonebox-1.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
25
|
-
clonebox-1.1.4.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
|
|
26
|
-
clonebox-1.1.4.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
|
|
27
|
-
clonebox-1.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|