clonebox 1.1.4__py3-none-any.whl → 1.1.6__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/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)
@@ -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():
@@ -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),
clonebox/validator.py CHANGED
@@ -826,6 +826,26 @@ class VMValidator:
826
826
  self.console.print(table)
827
827
  return self.results["smoke"]
828
828
 
829
+ def _check_qga_ready(self) -> bool:
830
+ """Check if QEMU guest agent is responding."""
831
+ try:
832
+ result = subprocess.run(
833
+ [
834
+ "virsh",
835
+ "--connect",
836
+ self.conn_uri,
837
+ "qemu-agent-command",
838
+ self.vm_name,
839
+ '{"execute":"guest-ping"}',
840
+ ],
841
+ capture_output=True,
842
+ text=True,
843
+ timeout=5,
844
+ )
845
+ return result.returncode == 0
846
+ except Exception:
847
+ return False
848
+
829
849
  def validate_all(self) -> Dict:
830
850
  """Run all validations and return comprehensive results."""
831
851
  self.console.print("[bold cyan]🔍 Running Full Validation...[/]")
@@ -850,6 +870,20 @@ class VMValidator:
850
870
  self.results["overall"] = "error"
851
871
  return self.results
852
872
 
873
+ # Check QEMU Guest Agent
874
+ if not self._check_qga_ready():
875
+ self.console.print("[red]❌ QEMU Guest Agent not responding[/]")
876
+ self.console.print("\n[bold]🔧 Troubleshooting QGA:[/]")
877
+ self.console.print(" 1. The VM might still be booting. Wait 30-60 seconds.")
878
+ self.console.print(" 2. Ensure the agent is installed and running inside the VM:")
879
+ self.console.print(" [dim]virsh console " + self.vm_name + "[/]")
880
+ self.console.print(" [dim]sudo systemctl status qemu-guest-agent[/]")
881
+ self.console.print(" 3. If newly created, cloud-init might still be running.")
882
+ self.console.print(" 4. Check VM logs: [dim]clonebox logs " + self.vm_name + "[/]")
883
+ self.console.print(f"\n[yellow]⚠️ Skipping deep validation as it requires a working Guest Agent.[/]")
884
+ self.results["overall"] = "qga_not_ready"
885
+ return self.results
886
+
853
887
  # Run all validations
854
888
  self.validate_mounts()
855
889
  self.validate_packages()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clonebox
3
- Version: 1.1.4
3
+ Version: 1.1.6
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,8 @@ 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
36
+ Requires-Dist: structlog>=24.0.0
35
37
  Provides-Extra: dev
36
38
  Requires-Dist: pytest>=7.0.0; extra == "dev"
37
39
  Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
@@ -114,6 +116,8 @@ CloneBox excels in scenarios where developers need:
114
116
  | 🎛️ Profiles System (`ml-dev`, `web-stack`) | ✅ Stable |
115
117
  | 🔍 Auto-detection (services, apps, paths) | ✅ Stable |
116
118
  | 🔒 P2P Secure Transfer (AES-256) | ✅ **NEW** |
119
+ | 📸 Snapshot Management | ✅ **NEW** |
120
+ | 🏥 Health Check System | ✅ **NEW** |
117
121
  | 🧪 95%+ Test Coverage | ✅ |
118
122
 
119
123
  ### P2P Secure VM Sharing
@@ -141,9 +145,55 @@ clonebox sync-key user@hostB # Sync encryption key
141
145
  clonebox list-remote user@hostB # List remote VMs
142
146
  ```
143
147
 
148
+ ### Snapshot Management
149
+
150
+ Save and restore VM states:
151
+
152
+ ```bash
153
+ # Create snapshot before risky operation
154
+ clonebox snapshot create my-vm --name "before-upgrade" --user
155
+
156
+ # List all snapshots
157
+ clonebox snapshot list my-vm --user
158
+
159
+ # Restore to previous state
160
+ clonebox snapshot restore my-vm --name "before-upgrade" --user
161
+
162
+ # Delete old snapshot
163
+ clonebox snapshot delete my-vm --name "before-upgrade" --user
164
+ ```
165
+
166
+ ### Health Checks
167
+
168
+ Configure health probes in `.clonebox.yaml`:
169
+
170
+ ```yaml
171
+ health_checks:
172
+ - name: nginx
173
+ type: http
174
+ url: http://localhost:80/health
175
+ expected_status: 200
176
+
177
+ - name: postgres
178
+ type: tcp
179
+ host: localhost
180
+ port: 5432
181
+
182
+ - name: redis
183
+ type: command
184
+ exec: "redis-cli ping"
185
+ expected_output: "PONG"
186
+ ```
187
+
188
+ Run health checks:
189
+
190
+ ```bash
191
+ clonebox health my-vm --user
192
+ ```
193
+
144
194
  ### Roadmap
145
195
 
146
- - **v1.2.0**: `clonebox exec` command, VM snapshots, snapshot restore
196
+ - **v1.2.0**: Resource limits, progress bars, secrets isolation
147
197
  - **v1.3.0**: Multi-VM orchestration, cluster mode
148
198
  - **v2.0.0**: Cloud provider support (AWS, GCP, Azure), Windows WSL2 support
149
199
 
@@ -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=uVACDtbGh0bNcZY3p2s5W90aU6NQHzuTnTLelWCOlss,136330
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=l9e5U4ZEEcgCTt2c47GpqFwppwVeifoCoNFi57lFGFo,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=2yQXDaL5bzHmxKkEP0fGJ85HV7Ax3iQl7KzSlBVLZkc,38220
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.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
38
+ clonebox-1.1.6.dist-info/METADATA,sha256=36_PJreaPh-6Ot5Irm6NSdFjzwGkAGZGHBs4TYcB9lQ,48915
39
+ clonebox-1.1.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
40
+ clonebox-1.1.6.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
41
+ clonebox-1.1.6.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
42
+ clonebox-1.1.6.dist-info/RECORD,,
@@ -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,,