clonebox 1.1.2__py3-none-any.whl → 1.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/cli.py +359 -0
- clonebox/exporter.py +189 -0
- clonebox/health/__init__.py +16 -0
- clonebox/health/models.py +194 -0
- clonebox/importer.py +220 -0
- clonebox/monitor.py +269 -0
- clonebox/p2p.py +184 -0
- clonebox/snapshots/__init__.py +12 -0
- clonebox/snapshots/manager.py +355 -0
- clonebox/snapshots/models.py +187 -0
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/METADATA +28 -2
- clonebox-1.1.4.dist-info/RECORD +27 -0
- clonebox-1.1.2.dist-info/RECORD +0 -18
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/WHEEL +0 -0
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/entry_points.txt +0 -0
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/licenses/LICENSE +0 -0
- {clonebox-1.1.2.dist-info → clonebox-1.1.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Data models for snapshot management."""
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SnapshotType(Enum):
|
|
12
|
+
"""Type of snapshot."""
|
|
13
|
+
|
|
14
|
+
DISK_ONLY = "disk" # Only disk state (offline)
|
|
15
|
+
FULL = "full" # Disk + memory + device state (online)
|
|
16
|
+
EXTERNAL = "external" # External snapshot file
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SnapshotState(Enum):
|
|
20
|
+
"""State of snapshot operation."""
|
|
21
|
+
|
|
22
|
+
CREATING = "creating"
|
|
23
|
+
READY = "ready"
|
|
24
|
+
REVERTING = "reverting"
|
|
25
|
+
DELETING = "deleting"
|
|
26
|
+
FAILED = "failed"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class Snapshot:
|
|
31
|
+
"""Represents a VM snapshot."""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
vm_name: str
|
|
35
|
+
snapshot_type: SnapshotType
|
|
36
|
+
state: SnapshotState
|
|
37
|
+
created_at: datetime
|
|
38
|
+
|
|
39
|
+
description: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
# Snapshot hierarchy
|
|
42
|
+
parent_name: Optional[str] = None
|
|
43
|
+
children: List[str] = field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
# Storage info
|
|
46
|
+
disk_path: Optional[Path] = None
|
|
47
|
+
memory_path: Optional[Path] = None
|
|
48
|
+
size_bytes: int = 0
|
|
49
|
+
|
|
50
|
+
# Metadata
|
|
51
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
tags: List[str] = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
# Auto-snapshot info
|
|
55
|
+
auto_created: bool = False
|
|
56
|
+
auto_policy: Optional[str] = None
|
|
57
|
+
expires_at: Optional[datetime] = None
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
60
|
+
"""Convert to dictionary for serialization."""
|
|
61
|
+
return {
|
|
62
|
+
"name": self.name,
|
|
63
|
+
"vm_name": self.vm_name,
|
|
64
|
+
"type": self.snapshot_type.value,
|
|
65
|
+
"state": self.state.value,
|
|
66
|
+
"created_at": self.created_at.isoformat(),
|
|
67
|
+
"description": self.description,
|
|
68
|
+
"parent_name": self.parent_name,
|
|
69
|
+
"children": self.children,
|
|
70
|
+
"disk_path": str(self.disk_path) if self.disk_path else None,
|
|
71
|
+
"memory_path": str(self.memory_path) if self.memory_path else None,
|
|
72
|
+
"size_bytes": self.size_bytes,
|
|
73
|
+
"metadata": self.metadata,
|
|
74
|
+
"tags": self.tags,
|
|
75
|
+
"auto_created": self.auto_created,
|
|
76
|
+
"auto_policy": self.auto_policy,
|
|
77
|
+
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Snapshot":
|
|
82
|
+
"""Create from dictionary."""
|
|
83
|
+
return cls(
|
|
84
|
+
name=data["name"],
|
|
85
|
+
vm_name=data["vm_name"],
|
|
86
|
+
snapshot_type=SnapshotType(data["type"]),
|
|
87
|
+
state=SnapshotState(data["state"]),
|
|
88
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
89
|
+
description=data.get("description"),
|
|
90
|
+
parent_name=data.get("parent_name"),
|
|
91
|
+
children=data.get("children", []),
|
|
92
|
+
disk_path=Path(data["disk_path"]) if data.get("disk_path") else None,
|
|
93
|
+
memory_path=Path(data["memory_path"]) if data.get("memory_path") else None,
|
|
94
|
+
size_bytes=data.get("size_bytes", 0),
|
|
95
|
+
metadata=data.get("metadata", {}),
|
|
96
|
+
tags=data.get("tags", []),
|
|
97
|
+
auto_created=data.get("auto_created", False),
|
|
98
|
+
auto_policy=data.get("auto_policy"),
|
|
99
|
+
expires_at=(
|
|
100
|
+
datetime.fromisoformat(data["expires_at"])
|
|
101
|
+
if data.get("expires_at")
|
|
102
|
+
else None
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def is_expired(self) -> bool:
|
|
108
|
+
"""Check if snapshot has expired."""
|
|
109
|
+
if self.expires_at is None:
|
|
110
|
+
return False
|
|
111
|
+
return datetime.now() > self.expires_at
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def age(self) -> timedelta:
|
|
115
|
+
"""Get snapshot age."""
|
|
116
|
+
return datetime.now() - self.created_at
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class SnapshotPolicy:
|
|
121
|
+
"""Policy for automatic snapshots."""
|
|
122
|
+
|
|
123
|
+
name: str
|
|
124
|
+
enabled: bool = True
|
|
125
|
+
|
|
126
|
+
# Retention settings
|
|
127
|
+
max_snapshots: int = 10
|
|
128
|
+
max_age_days: int = 30
|
|
129
|
+
min_snapshots: int = 1 # Keep at least N snapshots
|
|
130
|
+
|
|
131
|
+
# Auto-snapshot triggers
|
|
132
|
+
before_operations: List[str] = field(
|
|
133
|
+
default_factory=lambda: ["upgrade", "resize", "config-change"]
|
|
134
|
+
)
|
|
135
|
+
scheduled_interval_hours: Optional[int] = None # e.g., 24 for daily
|
|
136
|
+
|
|
137
|
+
# Naming
|
|
138
|
+
name_prefix: str = "auto-"
|
|
139
|
+
include_timestamp: bool = True
|
|
140
|
+
|
|
141
|
+
# Cleanup
|
|
142
|
+
auto_cleanup: bool = True
|
|
143
|
+
cleanup_on_success: bool = False # Remove pre-operation snapshot if op succeeds
|
|
144
|
+
|
|
145
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
146
|
+
"""Convert to dictionary."""
|
|
147
|
+
return {
|
|
148
|
+
"name": self.name,
|
|
149
|
+
"enabled": self.enabled,
|
|
150
|
+
"max_snapshots": self.max_snapshots,
|
|
151
|
+
"max_age_days": self.max_age_days,
|
|
152
|
+
"min_snapshots": self.min_snapshots,
|
|
153
|
+
"before_operations": self.before_operations,
|
|
154
|
+
"scheduled_interval_hours": self.scheduled_interval_hours,
|
|
155
|
+
"name_prefix": self.name_prefix,
|
|
156
|
+
"include_timestamp": self.include_timestamp,
|
|
157
|
+
"auto_cleanup": self.auto_cleanup,
|
|
158
|
+
"cleanup_on_success": self.cleanup_on_success,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def from_dict(cls, data: Dict[str, Any]) -> "SnapshotPolicy":
|
|
163
|
+
"""Create from dictionary."""
|
|
164
|
+
return cls(
|
|
165
|
+
name=data["name"],
|
|
166
|
+
enabled=data.get("enabled", True),
|
|
167
|
+
max_snapshots=data.get("max_snapshots", 10),
|
|
168
|
+
max_age_days=data.get("max_age_days", 30),
|
|
169
|
+
min_snapshots=data.get("min_snapshots", 1),
|
|
170
|
+
before_operations=data.get(
|
|
171
|
+
"before_operations", ["upgrade", "resize", "config-change"]
|
|
172
|
+
),
|
|
173
|
+
scheduled_interval_hours=data.get("scheduled_interval_hours"),
|
|
174
|
+
name_prefix=data.get("name_prefix", "auto-"),
|
|
175
|
+
include_timestamp=data.get("include_timestamp", True),
|
|
176
|
+
auto_cleanup=data.get("auto_cleanup", True),
|
|
177
|
+
cleanup_on_success=data.get("cleanup_on_success", False),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def generate_snapshot_name(self, operation: Optional[str] = None) -> str:
|
|
181
|
+
"""Generate snapshot name based on policy."""
|
|
182
|
+
parts = [self.name_prefix]
|
|
183
|
+
if operation:
|
|
184
|
+
parts.append(operation)
|
|
185
|
+
if self.include_timestamp:
|
|
186
|
+
parts.append(datetime.now().strftime("%Y%m%d-%H%M%S"))
|
|
187
|
+
return "-".join(parts)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clonebox
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.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
|
|
@@ -104,7 +104,7 @@ CloneBox excels in scenarios where developers need:
|
|
|
104
104
|
|
|
105
105
|
## What's New in v1.1
|
|
106
106
|
|
|
107
|
-
**v1.1.
|
|
107
|
+
**v1.1.2** is production-ready with two full runtimes and P2P secure sharing:
|
|
108
108
|
|
|
109
109
|
| Feature | Status |
|
|
110
110
|
|---------|--------|
|
|
@@ -113,8 +113,34 @@ CloneBox excels in scenarios where developers need:
|
|
|
113
113
|
| 📊 Web Dashboard (FastAPI + HTMX + Tailwind) | ✅ Stable |
|
|
114
114
|
| 🎛️ Profiles System (`ml-dev`, `web-stack`) | ✅ Stable |
|
|
115
115
|
| 🔍 Auto-detection (services, apps, paths) | ✅ Stable |
|
|
116
|
+
| 🔒 P2P Secure Transfer (AES-256) | ✅ **NEW** |
|
|
116
117
|
| 🧪 95%+ Test Coverage | ✅ |
|
|
117
118
|
|
|
119
|
+
### P2P Secure VM Sharing
|
|
120
|
+
|
|
121
|
+
Share VMs between workstations with AES-256 encryption:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Generate team encryption key (once per team)
|
|
125
|
+
clonebox keygen
|
|
126
|
+
# 🔑 Key saved: ~/.clonebox.key
|
|
127
|
+
|
|
128
|
+
# Export encrypted VM
|
|
129
|
+
clonebox export-encrypted my-dev-vm -o team-env.enc --user-data
|
|
130
|
+
|
|
131
|
+
# Transfer via SCP/SMB/USB
|
|
132
|
+
scp team-env.enc user@workstationB:~/
|
|
133
|
+
|
|
134
|
+
# Import on another machine (needs same key)
|
|
135
|
+
clonebox import-encrypted team-env.enc --name my-dev-copy
|
|
136
|
+
|
|
137
|
+
# Or use P2P commands directly
|
|
138
|
+
clonebox export-remote user@hostA my-vm -o local.enc --encrypted
|
|
139
|
+
clonebox import-remote local.enc user@hostB --encrypted
|
|
140
|
+
clonebox sync-key user@hostB # Sync encryption key
|
|
141
|
+
clonebox list-remote user@hostB # List remote VMs
|
|
142
|
+
```
|
|
143
|
+
|
|
118
144
|
### Roadmap
|
|
119
145
|
|
|
120
146
|
- **v1.2.0**: `clonebox exec` command, VM snapshots, snapshot restore
|
|
@@ -0,0 +1,27 @@
|
|
|
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,,
|
clonebox-1.1.2.dist-info/RECORD
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
clonebox/__init__.py,sha256=CyfHVVq6KqBr4CNERBpXk_O6Q5B35q03YpdQbokVvvI,408
|
|
2
|
-
clonebox/__main__.py,sha256=Fcoyzwwyz5-eC_sBlQk5a5RbKx8uodQz5sKJ190U0NU,135
|
|
3
|
-
clonebox/cli.py,sha256=0ZOE66WVbIDdmIBf0_VyWw4U-jzPo_T4pCTXkWect5o,114804
|
|
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/models.py,sha256=zwejkNtEEO_aPy_Q5UzXG5tszU-c7lkqh9LQus9eWMo,8307
|
|
9
|
-
clonebox/profiles.py,sha256=UP37fX_rhrG_O9ehNFJBUcULPmUtN1A8KsJ6cM44oK0,1986
|
|
10
|
-
clonebox/validator.py,sha256=CF4hMlY69-AGRH5HdG8HAA9_LNCwDKD4xPlYQPWJ9Rw,36647
|
|
11
|
-
clonebox/templates/profiles/ml-dev.yaml,sha256=w07MToGh31xtxpjbeXTBk9BkpAN8A3gv8HeA3ESKG9M,461
|
|
12
|
-
clonebox/templates/profiles/web-stack.yaml,sha256=EBnnGMzML5vAjXmIUbCpbTCwmRaNJiuWd3EcL43DOK8,485
|
|
13
|
-
clonebox-1.1.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
14
|
-
clonebox-1.1.2.dist-info/METADATA,sha256=rcpTr77zlQIQ2VwauZBL5QEIGvmxuD0-4HQYIYj_jbE,47164
|
|
15
|
-
clonebox-1.1.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
16
|
-
clonebox-1.1.2.dist-info/entry_points.txt,sha256=FES95Vi3btfViLEEoHdb8nikNxTqzaooi9ehZw9ZfWI,47
|
|
17
|
-
clonebox-1.1.2.dist-info/top_level.txt,sha256=LdMo2cvCrEcRGH2M8JgQNVsCoszLV0xug6kx1JnaRjo,9
|
|
18
|
-
clonebox-1.1.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|