golem-vm-provider 0.1.26__py3-none-any.whl → 0.1.27__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.
- {golem_vm_provider-0.1.26.dist-info → golem_vm_provider-0.1.27.dist-info}/METADATA +7 -2
- golem_vm_provider-0.1.27.dist-info/RECORD +38 -0
- {golem_vm_provider-0.1.26.dist-info → golem_vm_provider-0.1.27.dist-info}/entry_points.txt +1 -0
- provider/api/models.py +7 -7
- provider/api/routes.py +89 -95
- provider/config.py +50 -28
- provider/container.py +84 -0
- provider/discovery/__init__.py +8 -2
- provider/discovery/advertiser.py +41 -63
- provider/discovery/golem_base_advertiser.py +12 -6
- provider/discovery/resource_monitor.py +34 -0
- provider/discovery/resource_tracker.py +1 -1
- provider/discovery/service.py +24 -0
- provider/main.py +56 -155
- provider/service.py +67 -0
- provider/utils/__init__.py +0 -0
- provider/utils/logging.py +11 -27
- provider/utils/port_display.py +6 -8
- provider/utils/retry.py +39 -0
- provider/vm/__init__.py +1 -1
- provider/vm/models.py +8 -7
- provider/vm/multipass.py +2 -420
- provider/vm/multipass_adapter.py +221 -0
- provider/vm/name_mapper.py +5 -5
- provider/vm/port_manager.py +24 -6
- provider/vm/provider.py +48 -0
- provider/vm/proxy_manager.py +1 -1
- provider/vm/service.py +91 -0
- golem_vm_provider-0.1.26.dist-info/RECORD +0 -30
- {golem_vm_provider-0.1.26.dist-info → golem_vm_provider-0.1.27.dist-info}/WHEEL +0 -0
provider/utils/port_display.py
CHANGED
@@ -100,10 +100,8 @@ class PortVerificationDisplay:
|
|
100
100
|
print("-------------------------")
|
101
101
|
|
102
102
|
if self.skip_verification:
|
103
|
-
print("
|
104
|
-
print(f"└─ Port Range: {self.port_range_start}-{self.port_range_end}")
|
105
|
-
print("└─ Status: Port verification skipped")
|
106
|
-
print("└─ Note: Configure ports before deploying to production")
|
103
|
+
print("✅ Development Mode: Port verification skipped")
|
104
|
+
print(f"└─ Port Range: {self.port_range_start}-{self.port_range_end} assumed available")
|
107
105
|
return
|
108
106
|
|
109
107
|
await self.animate_verification("Scanning VM access ports...")
|
@@ -209,10 +207,10 @@ class PortVerificationDisplay:
|
|
209
207
|
print("\n🎯 Current Status:", end=" ")
|
210
208
|
|
211
209
|
if self.skip_verification:
|
212
|
-
print("Development Mode")
|
213
|
-
print("└─ Status:
|
214
|
-
print(f"└─ Available: All ports in range {self.port_range_start}-{self.port_range_end}")
|
215
|
-
print("└─ Note:
|
210
|
+
print("✅ Development Mode")
|
211
|
+
print("└─ Status: Local port verification complete")
|
212
|
+
print(f"└─ Available: All ports in range {self.port_range_start}-{self.port_range_end} are assumed to be available locally")
|
213
|
+
print("└─ Note: External accessibility is not checked in dev mode")
|
216
214
|
return
|
217
215
|
|
218
216
|
if discovery_result is None:
|
provider/utils/retry.py
CHANGED
@@ -46,3 +46,42 @@ def async_retry(
|
|
46
46
|
raise last_exception
|
47
47
|
return wrapper
|
48
48
|
return decorator
|
49
|
+
|
50
|
+
def async_retry_unless_not_found(
|
51
|
+
retries: int = 3,
|
52
|
+
delay: float = 1.0,
|
53
|
+
backoff: float = 2.0,
|
54
|
+
exceptions: tuple = (Exception,)
|
55
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]:
|
56
|
+
"""
|
57
|
+
Retry decorator for async functions with exponential backoff, but skips retries for VMNotFoundError.
|
58
|
+
"""
|
59
|
+
from ..vm.models import VMNotFoundError
|
60
|
+
|
61
|
+
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
62
|
+
@wraps(func)
|
63
|
+
async def wrapper(*args: Any, **kwargs: Any) -> T:
|
64
|
+
current_delay = delay
|
65
|
+
last_exception = None
|
66
|
+
|
67
|
+
for attempt in range(retries + 1):
|
68
|
+
try:
|
69
|
+
return await func(*args, **kwargs)
|
70
|
+
except exceptions as e:
|
71
|
+
if isinstance(e, VMNotFoundError):
|
72
|
+
raise e
|
73
|
+
|
74
|
+
last_exception = e
|
75
|
+
if attempt == retries:
|
76
|
+
break
|
77
|
+
|
78
|
+
logger.warning(
|
79
|
+
f"Attempt {attempt + 1}/{retries} failed for {func.__name__}: {e}. "
|
80
|
+
f"Retrying in {current_delay:.1f}s..."
|
81
|
+
)
|
82
|
+
await asyncio.sleep(current_delay)
|
83
|
+
current_delay *= backoff
|
84
|
+
|
85
|
+
raise last_exception
|
86
|
+
return wrapper
|
87
|
+
return decorator
|
provider/vm/__init__.py
CHANGED
provider/vm/models.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from enum import Enum
|
2
|
-
from pydantic import BaseModel, Field,
|
2
|
+
from pydantic import BaseModel, Field, field_validator
|
3
3
|
from typing import Dict, Optional
|
4
4
|
from datetime import datetime
|
5
5
|
|
@@ -28,14 +28,14 @@ class VMResources(BaseModel):
|
|
28
28
|
memory: int = Field(..., ge=1, description="Memory in GB")
|
29
29
|
storage: int = Field(..., ge=10, description="Storage in GB")
|
30
30
|
|
31
|
-
@
|
31
|
+
@field_validator("cpu")
|
32
32
|
def validate_cpu(cls, v: int) -> int:
|
33
33
|
"""Validate CPU cores."""
|
34
34
|
if v not in [1, 2, 4, 8, 16]:
|
35
35
|
raise ValueError("CPU cores must be 1, 2, 4, 8, or 16")
|
36
36
|
return v
|
37
37
|
|
38
|
-
@
|
38
|
+
@field_validator("memory")
|
39
39
|
def validate_memory(cls, v: int) -> int:
|
40
40
|
"""Validate memory."""
|
41
41
|
if v not in [1, 2, 4, 8, 16, 32, 64]:
|
@@ -66,21 +66,21 @@ class VMCreateRequest(BaseModel):
|
|
66
66
|
ssh_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ",
|
67
67
|
description="SSH public key for VM access")
|
68
68
|
|
69
|
-
@
|
69
|
+
@field_validator("name")
|
70
70
|
def validate_name(cls, v: str) -> str:
|
71
71
|
"""Validate VM name."""
|
72
72
|
if "--" in v:
|
73
73
|
raise ValueError("VM name cannot contain consecutive hyphens")
|
74
74
|
return v
|
75
75
|
|
76
|
-
@
|
76
|
+
@field_validator("cpu_cores")
|
77
77
|
def validate_cpu(cls, v: Optional[int]) -> Optional[int]:
|
78
78
|
"""Validate CPU cores."""
|
79
79
|
if v is not None and v not in [1, 2, 4, 8, 16]:
|
80
80
|
raise ValueError("CPU cores must be 1, 2, 4, 8, or 16")
|
81
81
|
return v
|
82
82
|
|
83
|
-
@
|
83
|
+
@field_validator("memory_gb")
|
84
84
|
def validate_memory(cls, v: Optional[int]) -> Optional[int]:
|
85
85
|
"""Validate memory."""
|
86
86
|
if v is not None and v not in [1, 2, 4, 8, 16, 32, 64]:
|
@@ -97,8 +97,9 @@ class VMConfig(BaseModel):
|
|
97
97
|
size: Optional[VMSize] = None
|
98
98
|
ssh_key: str = Field(..., pattern="^(ssh-rsa|ssh-ed25519) ",
|
99
99
|
description="SSH public key for VM access")
|
100
|
+
cloud_init_path: Optional[str] = None
|
100
101
|
|
101
|
-
@
|
102
|
+
@field_validator("name")
|
102
103
|
def validate_name(cls, v: str) -> str:
|
103
104
|
"""Validate VM name."""
|
104
105
|
if "--" in v:
|
provider/vm/multipass.py
CHANGED
@@ -15,423 +15,5 @@ from .name_mapper import VMNameMapper
|
|
15
15
|
logger = setup_logger(__name__)
|
16
16
|
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
pass
|
21
|
-
|
22
|
-
|
23
|
-
class MultipassProvider(VMProvider):
|
24
|
-
"""Manages VMs using Multipass."""
|
25
|
-
|
26
|
-
def __init__(self, resource_tracker: "ResourceTracker", port_manager: "PortManager"):
|
27
|
-
"""Initialize the multipass provider.
|
28
|
-
|
29
|
-
Args:
|
30
|
-
resource_tracker: Resource tracker instance
|
31
|
-
port_manager: Port manager instance for SSH port allocation
|
32
|
-
"""
|
33
|
-
self.resource_tracker = resource_tracker
|
34
|
-
self.port_manager = port_manager
|
35
|
-
self.multipass_path = settings.MULTIPASS_BINARY_PATH
|
36
|
-
self.vm_data_dir = Path(settings.VM_DATA_DIR)
|
37
|
-
self.vm_data_dir.mkdir(parents=True, exist_ok=True)
|
38
|
-
|
39
|
-
# Initialize managers
|
40
|
-
self.name_mapper = VMNameMapper(self.vm_data_dir / "vm_names.json")
|
41
|
-
self.proxy_manager = PythonProxyManager(
|
42
|
-
port_manager=port_manager,
|
43
|
-
name_mapper=self.name_mapper
|
44
|
-
)
|
45
|
-
|
46
|
-
def _verify_installation(self) -> None:
|
47
|
-
"""Verify multipass is installed and get version."""
|
48
|
-
try:
|
49
|
-
result = subprocess.run(
|
50
|
-
[self.multipass_path, "version"],
|
51
|
-
capture_output=True,
|
52
|
-
text=True,
|
53
|
-
check=True
|
54
|
-
)
|
55
|
-
logger.info(f"🔧 Using Multipass version: {result.stdout.strip()}")
|
56
|
-
except subprocess.CalledProcessError as e:
|
57
|
-
raise MultipassError(
|
58
|
-
f"Failed to verify multipass installation: {e.stderr}")
|
59
|
-
except FileNotFoundError:
|
60
|
-
raise MultipassError(
|
61
|
-
f"Multipass not found at {self.multipass_path}")
|
62
|
-
|
63
|
-
def _get_all_vms_resources(self) -> Dict[str, VMResources]:
|
64
|
-
"""Get resources for all running VMs from multipass.
|
65
|
-
|
66
|
-
Returns:
|
67
|
-
Dictionary mapping VM names to their resources
|
68
|
-
"""
|
69
|
-
result = self._run_multipass(["list", "--format", "json"])
|
70
|
-
data = json.loads(result.stdout)
|
71
|
-
vm_resources = {}
|
72
|
-
|
73
|
-
for vm in data.get("list", []):
|
74
|
-
if vm.get("name", "").startswith("golem-"):
|
75
|
-
try:
|
76
|
-
info = self._get_vm_info(vm["name"])
|
77
|
-
vm_resources[vm["name"]] = VMResources(
|
78
|
-
cpu=int(info.get("cpu_count", 1)),
|
79
|
-
memory=int(info.get("memory_total", 1024) / 1024),
|
80
|
-
storage=int(info.get("disk_total", 10 * 1024) / 1024)
|
81
|
-
)
|
82
|
-
except Exception as e:
|
83
|
-
logger.error(f"Failed to get info for VM {vm['name']}: {e}")
|
84
|
-
continue
|
85
|
-
|
86
|
-
return vm_resources
|
87
|
-
|
88
|
-
async def initialize(self) -> None:
|
89
|
-
"""Initialize the provider."""
|
90
|
-
self._verify_installation()
|
91
|
-
|
92
|
-
# Create SSH key directory
|
93
|
-
ssh_key_dir = Path(settings.SSH_KEY_DIR)
|
94
|
-
ssh_key_dir.mkdir(parents=True, exist_ok=True)
|
95
|
-
|
96
|
-
# Sync resource tracker with actual multipass state
|
97
|
-
logger.info("🔄 Syncing resource tracker with multipass state...")
|
98
|
-
vm_resources = self._get_all_vms_resources()
|
99
|
-
await self.resource_tracker.sync_with_multipass(vm_resources)
|
100
|
-
logger.info("✨ Resource tracker synced with multipass state")
|
101
|
-
|
102
|
-
def _run_multipass(self, args: List[str], check: bool = True) -> subprocess.CompletedProcess:
|
103
|
-
"""Run a multipass command.
|
104
|
-
|
105
|
-
Args:
|
106
|
-
args: Command arguments
|
107
|
-
check: Whether to check return code
|
108
|
-
|
109
|
-
Returns:
|
110
|
-
CompletedProcess instance
|
111
|
-
"""
|
112
|
-
try:
|
113
|
-
return subprocess.run(
|
114
|
-
[self.multipass_path, *args],
|
115
|
-
capture_output=True,
|
116
|
-
text=True,
|
117
|
-
check=check
|
118
|
-
)
|
119
|
-
except subprocess.CalledProcessError as e:
|
120
|
-
raise MultipassError(f"Multipass command failed: {e.stderr}")
|
121
|
-
|
122
|
-
def _get_vm_info(self, vm_id: str) -> Dict:
|
123
|
-
"""Get detailed information about a VM.
|
124
|
-
|
125
|
-
Args:
|
126
|
-
vm_id: VM identifier
|
127
|
-
|
128
|
-
Returns:
|
129
|
-
Dictionary with VM information
|
130
|
-
"""
|
131
|
-
result = self._run_multipass(["info", vm_id, "--format", "json"])
|
132
|
-
try:
|
133
|
-
info = json.loads(result.stdout)
|
134
|
-
return info["info"][vm_id]
|
135
|
-
except (json.JSONDecodeError, KeyError) as e:
|
136
|
-
raise MultipassError(f"Failed to parse VM info: {e}")
|
137
|
-
|
138
|
-
def _get_vm_ip(self, vm_id: str) -> Optional[str]:
|
139
|
-
"""Get IP address of a VM.
|
140
|
-
|
141
|
-
Args:
|
142
|
-
vm_id: VM identifier
|
143
|
-
|
144
|
-
Returns:
|
145
|
-
IP address or None if not found
|
146
|
-
"""
|
147
|
-
try:
|
148
|
-
info = self._get_vm_info(vm_id)
|
149
|
-
return info.get("ipv4", [None])[0]
|
150
|
-
except Exception:
|
151
|
-
return None
|
152
|
-
|
153
|
-
async def create_vm(self, config: VMConfig) -> VMInfo:
|
154
|
-
"""Create a new VM.
|
155
|
-
|
156
|
-
Args:
|
157
|
-
config: VM configuration
|
158
|
-
|
159
|
-
Returns:
|
160
|
-
Information about the created VM
|
161
|
-
"""
|
162
|
-
multipass_name = f"golem-{config.name}-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
163
|
-
await self.name_mapper.add_mapping(config.name, multipass_name)
|
164
|
-
cloud_init_path = None
|
165
|
-
config_id = None
|
166
|
-
|
167
|
-
# Verify resources are properly allocated
|
168
|
-
if not self.resource_tracker.can_accept_resources(config.resources):
|
169
|
-
raise VMCreateError("Resources not properly allocated or insufficient")
|
170
|
-
|
171
|
-
try:
|
172
|
-
# Generate cloud-init config with requestor's public key
|
173
|
-
cloud_init_path, config_id = generate_cloud_init(
|
174
|
-
hostname=config.name,
|
175
|
-
ssh_key=config.ssh_key
|
176
|
-
)
|
177
|
-
|
178
|
-
# Launch VM
|
179
|
-
logger.process(f"🚀 Launching VM {multipass_name} with config {config_id}")
|
180
|
-
launch_cmd = [
|
181
|
-
"launch",
|
182
|
-
config.image,
|
183
|
-
"--name", multipass_name,
|
184
|
-
"--cloud-init", cloud_init_path,
|
185
|
-
"--cpus", str(config.resources.cpu),
|
186
|
-
"--memory", f"{config.resources.memory}G",
|
187
|
-
"--disk", f"{config.resources.storage}G"
|
188
|
-
]
|
189
|
-
self._run_multipass(launch_cmd)
|
190
|
-
|
191
|
-
# Get VM IP
|
192
|
-
ip_address = self._get_vm_ip(multipass_name)
|
193
|
-
if not ip_address:
|
194
|
-
raise MultipassError("Failed to get VM IP address")
|
195
|
-
|
196
|
-
# Allocate port and configure proxy
|
197
|
-
try:
|
198
|
-
# First allocate a verified port
|
199
|
-
ssh_port = self.port_manager.allocate_port(multipass_name)
|
200
|
-
if not ssh_port:
|
201
|
-
if settings.DEV_MODE:
|
202
|
-
logger.warning("Failed to allocate verified SSH port in dev mode, falling back to random port")
|
203
|
-
ssh_port = 0 # Let the proxy manager pick a random port
|
204
|
-
else:
|
205
|
-
raise MultipassError("Failed to allocate verified SSH port")
|
206
|
-
|
207
|
-
# Then configure proxy with allocated port
|
208
|
-
success = await self.proxy_manager.add_vm(multipass_name, ip_address, port=ssh_port)
|
209
|
-
if not success:
|
210
|
-
# Clean up allocated port if proxy fails
|
211
|
-
self.port_manager.deallocate_port(multipass_name)
|
212
|
-
raise MultipassError("Failed to configure proxy")
|
213
|
-
|
214
|
-
# Create VM info and register with resource tracker
|
215
|
-
vm_info = VMInfo(
|
216
|
-
id=config.name, # Use requestor name as VM ID
|
217
|
-
name=config.name,
|
218
|
-
status=VMStatus.RUNNING,
|
219
|
-
resources=config.resources,
|
220
|
-
ip_address=ip_address,
|
221
|
-
ssh_port=ssh_port
|
222
|
-
)
|
223
|
-
|
224
|
-
# Update resource tracker with VM ID
|
225
|
-
await self.resource_tracker.allocate(config.resources, config.name)
|
226
|
-
|
227
|
-
return vm_info
|
228
|
-
|
229
|
-
except Exception as e:
|
230
|
-
# If proxy configuration fails, ensure we cleanup the VM and resources
|
231
|
-
self._run_multipass(["delete", multipass_name, "--purge"], check=False)
|
232
|
-
await self.resource_tracker.deallocate(config.resources, config.name)
|
233
|
-
await self.name_mapper.remove_mapping(config.name)
|
234
|
-
raise VMCreateError(
|
235
|
-
f"Failed to configure VM networking: {str(e)}", vm_id=config.name)
|
236
|
-
|
237
|
-
except Exception as e:
|
238
|
-
# Cleanup on failure (this catches VM creation errors)
|
239
|
-
try:
|
240
|
-
await self.delete_vm(config.name)
|
241
|
-
except Exception as cleanup_error:
|
242
|
-
logger.error(f"Error during VM cleanup: {cleanup_error}")
|
243
|
-
# Ensure resources are deallocated even if delete_vm fails
|
244
|
-
await self.resource_tracker.deallocate(config.resources, config.name)
|
245
|
-
raise VMCreateError(f"Failed to create VM: {str(e)}", vm_id=config.name)
|
246
|
-
|
247
|
-
finally:
|
248
|
-
# Cleanup cloud-init file
|
249
|
-
if cloud_init_path and config_id:
|
250
|
-
cleanup_cloud_init(cloud_init_path, config_id)
|
251
|
-
|
252
|
-
def _verify_vm_exists(self, vm_id: str) -> bool:
|
253
|
-
"""Check if VM exists in multipass.
|
254
|
-
|
255
|
-
Args:
|
256
|
-
vm_id: VM identifier
|
257
|
-
|
258
|
-
Returns:
|
259
|
-
True if VM exists, False otherwise
|
260
|
-
"""
|
261
|
-
try:
|
262
|
-
result = self._run_multipass(["list", "--format", "json"])
|
263
|
-
data = json.loads(result.stdout)
|
264
|
-
vms = data.get("list", [])
|
265
|
-
return any(vm.get("name") == vm_id for vm in vms)
|
266
|
-
except Exception:
|
267
|
-
return False
|
268
|
-
|
269
|
-
async def delete_vm(self, requestor_name: str) -> None:
|
270
|
-
"""Delete a VM.
|
271
|
-
|
272
|
-
Args:
|
273
|
-
requestor_name: Requestor's VM name
|
274
|
-
"""
|
275
|
-
# Get multipass name from mapper
|
276
|
-
multipass_name = await self.name_mapper.get_multipass_name(requestor_name)
|
277
|
-
if not multipass_name:
|
278
|
-
logger.warning(f"No multipass name found for VM {requestor_name}")
|
279
|
-
return
|
280
|
-
|
281
|
-
logger.process(f"🗑️ Initiating deletion of VM {multipass_name}")
|
282
|
-
|
283
|
-
# Get VM info for resource deallocation
|
284
|
-
try:
|
285
|
-
vm_info = await self.get_vm_status(requestor_name)
|
286
|
-
except Exception as e:
|
287
|
-
logger.error(f"Failed to get VM info for cleanup: {e}")
|
288
|
-
vm_info = None
|
289
|
-
|
290
|
-
# Check if VM exists
|
291
|
-
if not self._verify_vm_exists(multipass_name):
|
292
|
-
logger.warning(f"VM {multipass_name} not found in multipass")
|
293
|
-
else:
|
294
|
-
try:
|
295
|
-
# First mark for deletion
|
296
|
-
logger.info("🔄 Marking VM for deletion...")
|
297
|
-
self._run_multipass(["delete", multipass_name], check=False)
|
298
|
-
|
299
|
-
# Then purge
|
300
|
-
logger.info("🔄 Purging deleted VM...")
|
301
|
-
self._run_multipass(["purge"], check=False)
|
302
|
-
|
303
|
-
# Verify deletion
|
304
|
-
if self._verify_vm_exists(multipass_name):
|
305
|
-
logger.error(f"VM {multipass_name} still exists after deletion attempt")
|
306
|
-
# Try one more time with force
|
307
|
-
logger.info("🔄 Attempting forced deletion...")
|
308
|
-
self._run_multipass(["stop", "--all", multipass_name], check=False)
|
309
|
-
self._run_multipass(["delete", "--purge", multipass_name], check=False)
|
310
|
-
if self._verify_vm_exists(multipass_name):
|
311
|
-
raise MultipassError(f"Failed to delete VM {multipass_name}")
|
312
|
-
|
313
|
-
logger.success("✨ VM instance removed")
|
314
|
-
except Exception as e:
|
315
|
-
logger.error(f"Error deleting VM {multipass_name} from multipass: {e}")
|
316
|
-
raise
|
317
|
-
|
318
|
-
# Clean up proxy config and port allocation
|
319
|
-
try:
|
320
|
-
logger.info("🔄 Cleaning up network configuration...")
|
321
|
-
await self.proxy_manager.remove_vm(multipass_name)
|
322
|
-
self.port_manager.deallocate_port(multipass_name)
|
323
|
-
logger.success("✨ Network configuration cleaned up")
|
324
|
-
except Exception as e:
|
325
|
-
logger.error(f"Error cleaning up network configuration for VM {multipass_name}: {e}")
|
326
|
-
|
327
|
-
# Deallocate resources
|
328
|
-
if vm_info and vm_info.resources:
|
329
|
-
try:
|
330
|
-
logger.info("🔄 Deallocating resources...")
|
331
|
-
await self.resource_tracker.deallocate(vm_info.resources, requestor_name)
|
332
|
-
logger.success("✨ Resources deallocated")
|
333
|
-
except Exception as e:
|
334
|
-
logger.error(f"Error deallocating resources: {e}")
|
335
|
-
|
336
|
-
# Remove name mapping
|
337
|
-
try:
|
338
|
-
await self.name_mapper.remove_mapping(requestor_name)
|
339
|
-
logger.success("✨ Name mapping removed")
|
340
|
-
except Exception as e:
|
341
|
-
logger.error(f"Error removing name mapping: {e}")
|
342
|
-
|
343
|
-
# Sync resource tracker with actual state
|
344
|
-
logger.info("🔄 Syncing resource tracker with multipass state...")
|
345
|
-
vm_resources = self._get_all_vms_resources()
|
346
|
-
await self.resource_tracker.sync_with_multipass(vm_resources)
|
347
|
-
logger.info("✨ Resource tracker synced with multipass state")
|
348
|
-
|
349
|
-
async def start_vm(self, requestor_name: str) -> VMInfo:
|
350
|
-
"""Start a VM.
|
351
|
-
|
352
|
-
Args:
|
353
|
-
requestor_name: Requestor's VM name
|
354
|
-
|
355
|
-
Returns:
|
356
|
-
Updated VM information
|
357
|
-
"""
|
358
|
-
# Get multipass name from mapper
|
359
|
-
multipass_name = await self.name_mapper.get_multipass_name(requestor_name)
|
360
|
-
if not multipass_name:
|
361
|
-
raise VMNotFoundError(f"VM {requestor_name} not found")
|
362
|
-
|
363
|
-
logger.process(f"🔄 Starting VM '{requestor_name}'")
|
364
|
-
self._run_multipass(["start", multipass_name])
|
365
|
-
status = await self.get_vm_status(requestor_name)
|
366
|
-
logger.success(f"✨ VM '{requestor_name}' started successfully")
|
367
|
-
return status
|
368
|
-
|
369
|
-
async def stop_vm(self, requestor_name: str) -> VMInfo:
|
370
|
-
"""Stop a VM.
|
371
|
-
|
372
|
-
Args:
|
373
|
-
requestor_name: Requestor's VM name
|
374
|
-
|
375
|
-
Returns:
|
376
|
-
Updated VM information
|
377
|
-
"""
|
378
|
-
# Get multipass name from mapper
|
379
|
-
multipass_name = await self.name_mapper.get_multipass_name(requestor_name)
|
380
|
-
if not multipass_name:
|
381
|
-
raise VMNotFoundError(f"VM {requestor_name} not found")
|
382
|
-
|
383
|
-
logger.process(f"🔄 Stopping VM '{requestor_name}'")
|
384
|
-
self._run_multipass(["stop", multipass_name])
|
385
|
-
status = await self.get_vm_status(requestor_name)
|
386
|
-
logger.success(f"✨ VM '{requestor_name}' stopped successfully")
|
387
|
-
return status
|
388
|
-
|
389
|
-
async def get_vm_status(self, requestor_name: str) -> VMInfo:
|
390
|
-
"""Get current status of a VM.
|
391
|
-
|
392
|
-
Args:
|
393
|
-
requestor_name: Requestor's VM name
|
394
|
-
|
395
|
-
Returns:
|
396
|
-
VM status information
|
397
|
-
"""
|
398
|
-
try:
|
399
|
-
# Get multipass name from mapper
|
400
|
-
multipass_name = await self.name_mapper.get_multipass_name(requestor_name)
|
401
|
-
if not multipass_name:
|
402
|
-
raise VMNotFoundError(f"VM {requestor_name} not found")
|
403
|
-
|
404
|
-
# Get VM info from multipass
|
405
|
-
info = self._get_vm_info(multipass_name)
|
406
|
-
|
407
|
-
return VMInfo(
|
408
|
-
id=requestor_name, # Use requestor name as ID
|
409
|
-
name=requestor_name,
|
410
|
-
status=VMStatus(info.get("state", "unknown").lower()),
|
411
|
-
resources=VMResources(
|
412
|
-
cpu=int(info.get("cpu_count", 1)),
|
413
|
-
memory=int(info.get("memory_total", 1024) / 1024),
|
414
|
-
storage=int(info.get("disk_total", 10 * 1024) / 1024)
|
415
|
-
),
|
416
|
-
ip_address=info.get("ipv4", [None])[0],
|
417
|
-
ssh_port=self.proxy_manager.get_port(multipass_name)
|
418
|
-
)
|
419
|
-
except Exception as e:
|
420
|
-
logger.error(f"Error getting VM status: {e}")
|
421
|
-
return VMInfo(
|
422
|
-
id=requestor_name,
|
423
|
-
name=requestor_name,
|
424
|
-
status=VMStatus.ERROR,
|
425
|
-
resources=VMResources(cpu=1, memory=1, storage=10),
|
426
|
-
error_message=str(e)
|
427
|
-
)
|
428
|
-
|
429
|
-
async def add_ssh_key(self, vm_id: str, key: str) -> None:
|
430
|
-
"""Add SSH key to VM.
|
431
|
-
|
432
|
-
Args:
|
433
|
-
vm_id: VM identifier
|
434
|
-
key: SSH key to add
|
435
|
-
"""
|
436
|
-
# Not implemented - we use cloud-init for SSH key setup
|
437
|
-
pass
|
18
|
+
from .service import VMService
|
19
|
+
from .multipass_adapter import MultipassAdapter
|