cua-computer 0.1.28__py3-none-any.whl → 0.2.0__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.
- computer/__init__.py +5 -1
- computer/computer.py +268 -140
- computer/interface/factory.py +4 -1
- computer/interface/linux.py +595 -23
- computer/interface/macos.py +9 -1
- computer/models.py +17 -5
- computer/providers/__init__.py +4 -0
- computer/providers/base.py +105 -0
- computer/providers/cloud/__init__.py +5 -0
- computer/providers/cloud/provider.py +100 -0
- computer/providers/factory.py +118 -0
- computer/providers/lume/__init__.py +9 -0
- computer/providers/lume/provider.py +541 -0
- computer/providers/lume_api.py +559 -0
- computer/providers/lumier/__init__.py +8 -0
- computer/providers/lumier/provider.py +943 -0
- {cua_computer-0.1.28.dist-info → cua_computer-0.2.0.dist-info}/METADATA +7 -2
- cua_computer-0.2.0.dist-info/RECORD +29 -0
- cua_computer-0.1.28.dist-info/RECORD +0 -19
- {cua_computer-0.1.28.dist-info → cua_computer-0.2.0.dist-info}/WHEEL +0 -0
- {cua_computer-0.1.28.dist-info → cua_computer-0.2.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,541 @@
|
|
1
|
+
"""Lume VM provider implementation using curl commands.
|
2
|
+
|
3
|
+
This provider uses direct curl commands to interact with the Lume API,
|
4
|
+
removing the dependency on the pylume Python package.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import re
|
9
|
+
import asyncio
|
10
|
+
import json
|
11
|
+
import logging
|
12
|
+
import subprocess
|
13
|
+
import urllib.parse
|
14
|
+
from typing import Dict, Any, Optional, List, Tuple
|
15
|
+
|
16
|
+
from ..base import BaseVMProvider, VMProviderType
|
17
|
+
from ...logger import Logger, LogLevel
|
18
|
+
from ..lume_api import (
|
19
|
+
lume_api_get,
|
20
|
+
lume_api_run,
|
21
|
+
lume_api_stop,
|
22
|
+
lume_api_update,
|
23
|
+
lume_api_pull,
|
24
|
+
HAS_CURL,
|
25
|
+
parse_memory
|
26
|
+
)
|
27
|
+
|
28
|
+
# Setup logging
|
29
|
+
logger = logging.getLogger(__name__)
|
30
|
+
|
31
|
+
class LumeProvider(BaseVMProvider):
|
32
|
+
"""Lume VM provider implementation using direct curl commands.
|
33
|
+
|
34
|
+
This provider uses curl to interact with the Lume API server,
|
35
|
+
removing the dependency on the pylume Python package.
|
36
|
+
"""
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
port: int = 7777,
|
41
|
+
host: str = "localhost",
|
42
|
+
storage: Optional[str] = None,
|
43
|
+
verbose: bool = False,
|
44
|
+
ephemeral: bool = False,
|
45
|
+
):
|
46
|
+
"""Initialize the Lume provider.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
port: Port for the Lume API server (default: 7777)
|
50
|
+
host: Host to use for API connections (default: localhost)
|
51
|
+
storage: Path to store VM data
|
52
|
+
verbose: Enable verbose logging
|
53
|
+
"""
|
54
|
+
if not HAS_CURL:
|
55
|
+
raise ImportError(
|
56
|
+
"curl is required for LumeProvider. "
|
57
|
+
"Please ensure it is installed and in your PATH."
|
58
|
+
)
|
59
|
+
|
60
|
+
self.host = host
|
61
|
+
self.port = port # Default port for Lume API
|
62
|
+
self.storage = storage
|
63
|
+
self.verbose = verbose
|
64
|
+
self.ephemeral = ephemeral # If True, VMs will be deleted after stopping
|
65
|
+
|
66
|
+
# Base API URL for Lume API calls
|
67
|
+
self.api_base_url = f"http://{self.host}:{self.port}"
|
68
|
+
|
69
|
+
self.logger = logging.getLogger(__name__)
|
70
|
+
|
71
|
+
@property
|
72
|
+
def provider_type(self) -> VMProviderType:
|
73
|
+
"""Get the provider type."""
|
74
|
+
return VMProviderType.LUME
|
75
|
+
|
76
|
+
async def __aenter__(self):
|
77
|
+
"""Enter async context manager."""
|
78
|
+
# No initialization needed, just return self
|
79
|
+
return self
|
80
|
+
|
81
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
82
|
+
"""Exit async context manager."""
|
83
|
+
# No cleanup needed
|
84
|
+
pass
|
85
|
+
|
86
|
+
def _lume_api_get(self, vm_name: str = "", storage: Optional[str] = None, debug: bool = False) -> Dict[str, Any]:
|
87
|
+
"""Get VM information using shared lume_api function.
|
88
|
+
|
89
|
+
Args:
|
90
|
+
vm_name: Optional name of the VM to get info for.
|
91
|
+
If empty, lists all VMs.
|
92
|
+
storage: Optional storage path override. If provided, this will be used instead of self.storage
|
93
|
+
debug: Whether to show debug output
|
94
|
+
|
95
|
+
Returns:
|
96
|
+
Dictionary with VM status information parsed from JSON response
|
97
|
+
"""
|
98
|
+
# Use the shared implementation from lume_api module
|
99
|
+
return lume_api_get(
|
100
|
+
vm_name=vm_name,
|
101
|
+
host=self.host,
|
102
|
+
port=self.port,
|
103
|
+
storage=storage if storage is not None else self.storage,
|
104
|
+
debug=debug,
|
105
|
+
verbose=self.verbose
|
106
|
+
)
|
107
|
+
|
108
|
+
def _lume_api_run(self, vm_name: str, run_opts: Dict[str, Any], debug: bool = False) -> Dict[str, Any]:
|
109
|
+
"""Run a VM using shared lume_api function.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
vm_name: Name of the VM to run
|
113
|
+
run_opts: Dictionary of run options
|
114
|
+
debug: Whether to show debug output
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
Dictionary with API response or error information
|
118
|
+
"""
|
119
|
+
# Use the shared implementation from lume_api module
|
120
|
+
return lume_api_run(
|
121
|
+
vm_name=vm_name,
|
122
|
+
host=self.host,
|
123
|
+
port=self.port,
|
124
|
+
run_opts=run_opts,
|
125
|
+
storage=self.storage,
|
126
|
+
debug=debug,
|
127
|
+
verbose=self.verbose
|
128
|
+
)
|
129
|
+
|
130
|
+
def _lume_api_stop(self, vm_name: str, debug: bool = False) -> Dict[str, Any]:
|
131
|
+
"""Stop a VM using shared lume_api function.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
vm_name: Name of the VM to stop
|
135
|
+
debug: Whether to show debug output
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
Dictionary with API response or error information
|
139
|
+
"""
|
140
|
+
# Use the shared implementation from lume_api module
|
141
|
+
return lume_api_stop(
|
142
|
+
vm_name=vm_name,
|
143
|
+
host=self.host,
|
144
|
+
port=self.port,
|
145
|
+
storage=self.storage,
|
146
|
+
debug=debug,
|
147
|
+
verbose=self.verbose
|
148
|
+
)
|
149
|
+
|
150
|
+
def _lume_api_update(self, vm_name: str, update_opts: Dict[str, Any], debug: bool = False) -> Dict[str, Any]:
|
151
|
+
"""Update VM configuration using shared lume_api function.
|
152
|
+
|
153
|
+
Args:
|
154
|
+
vm_name: Name of the VM to update
|
155
|
+
update_opts: Dictionary of update options
|
156
|
+
debug: Whether to show debug output
|
157
|
+
|
158
|
+
Returns:
|
159
|
+
Dictionary with API response or error information
|
160
|
+
"""
|
161
|
+
# Use the shared implementation from lume_api module
|
162
|
+
return lume_api_update(
|
163
|
+
vm_name=vm_name,
|
164
|
+
host=self.host,
|
165
|
+
port=self.port,
|
166
|
+
update_opts=update_opts,
|
167
|
+
storage=self.storage,
|
168
|
+
debug=debug,
|
169
|
+
verbose=self.verbose
|
170
|
+
)
|
171
|
+
|
172
|
+
async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
173
|
+
"""Get VM information by name.
|
174
|
+
|
175
|
+
Args:
|
176
|
+
name: Name of the VM to get information for
|
177
|
+
storage: Optional storage path override. If provided, this will be used
|
178
|
+
instead of the provider's default storage path.
|
179
|
+
|
180
|
+
Returns:
|
181
|
+
Dictionary with VM information including status, IP address, etc.
|
182
|
+
|
183
|
+
Note:
|
184
|
+
If storage is not provided, the provider's default storage path will be used.
|
185
|
+
The storage parameter allows overriding the storage location for this specific call.
|
186
|
+
"""
|
187
|
+
if not HAS_CURL:
|
188
|
+
logger.error("curl is not available. Cannot get VM status.")
|
189
|
+
return {
|
190
|
+
"name": name,
|
191
|
+
"status": "unavailable",
|
192
|
+
"error": "curl is not available"
|
193
|
+
}
|
194
|
+
|
195
|
+
# First try to get detailed VM info from the API
|
196
|
+
try:
|
197
|
+
# Query the Lume API for VM status using the provider's storage_path
|
198
|
+
vm_info = self._lume_api_get(
|
199
|
+
vm_name=name,
|
200
|
+
storage=storage if storage is not None else self.storage,
|
201
|
+
debug=self.verbose
|
202
|
+
)
|
203
|
+
|
204
|
+
# Check for API errors
|
205
|
+
if "error" in vm_info:
|
206
|
+
logger.debug(f"API request error: {vm_info['error']}")
|
207
|
+
# If we got an error from the API, report the VM as not ready yet
|
208
|
+
return {
|
209
|
+
"name": name,
|
210
|
+
"status": "starting", # VM is still starting - do not attempt to connect yet
|
211
|
+
"api_status": "error",
|
212
|
+
"error": vm_info["error"]
|
213
|
+
}
|
214
|
+
|
215
|
+
# Process the VM status information
|
216
|
+
vm_status = vm_info.get("status", "unknown")
|
217
|
+
|
218
|
+
# Check if VM is stopped or not running - don't wait for IP in this case
|
219
|
+
if vm_status == "stopped":
|
220
|
+
logger.info(f"VM {name} is in '{vm_status}' state - not waiting for IP address")
|
221
|
+
# Return the status as-is without waiting for an IP
|
222
|
+
result = {
|
223
|
+
"name": name,
|
224
|
+
"status": vm_status,
|
225
|
+
**vm_info # Include all original fields from the API response
|
226
|
+
}
|
227
|
+
return result
|
228
|
+
|
229
|
+
# Handle field name differences between APIs
|
230
|
+
# Some APIs use camelCase, others use snake_case
|
231
|
+
if "vncUrl" in vm_info:
|
232
|
+
vnc_url = vm_info["vncUrl"]
|
233
|
+
elif "vnc_url" in vm_info:
|
234
|
+
vnc_url = vm_info["vnc_url"]
|
235
|
+
else:
|
236
|
+
vnc_url = ""
|
237
|
+
|
238
|
+
if "ipAddress" in vm_info:
|
239
|
+
ip_address = vm_info["ipAddress"]
|
240
|
+
elif "ip_address" in vm_info:
|
241
|
+
ip_address = vm_info["ip_address"]
|
242
|
+
else:
|
243
|
+
# If no IP address is provided and VM is supposed to be running,
|
244
|
+
# report it as still starting
|
245
|
+
ip_address = None
|
246
|
+
logger.info(f"VM {name} is in '{vm_status}' state but no IP address found - reporting as still starting")
|
247
|
+
|
248
|
+
logger.info(f"VM {name} status: {vm_status}")
|
249
|
+
|
250
|
+
# Return the complete status information
|
251
|
+
result = {
|
252
|
+
"name": name,
|
253
|
+
"status": vm_status if vm_status else "running",
|
254
|
+
"ip_address": ip_address,
|
255
|
+
"vnc_url": vnc_url,
|
256
|
+
"api_status": "ok"
|
257
|
+
}
|
258
|
+
|
259
|
+
# Include all original fields from the API response
|
260
|
+
if isinstance(vm_info, dict):
|
261
|
+
for key, value in vm_info.items():
|
262
|
+
if key not in result: # Don't override our carefully processed fields
|
263
|
+
result[key] = value
|
264
|
+
|
265
|
+
return result
|
266
|
+
|
267
|
+
except Exception as e:
|
268
|
+
logger.error(f"Failed to get VM status: {e}")
|
269
|
+
# Return a fallback status that indicates the VM is not ready yet
|
270
|
+
return {
|
271
|
+
"name": name,
|
272
|
+
"status": "initializing", # VM is still initializing
|
273
|
+
"error": f"Failed to get VM status: {str(e)}"
|
274
|
+
}
|
275
|
+
|
276
|
+
async def list_vms(self) -> List[Dict[str, Any]]:
|
277
|
+
"""List all available VMs."""
|
278
|
+
result = self._lume_api_get(debug=self.verbose)
|
279
|
+
|
280
|
+
# Extract the VMs list from the response
|
281
|
+
if "vms" in result and isinstance(result["vms"], list):
|
282
|
+
return result["vms"]
|
283
|
+
elif "error" in result:
|
284
|
+
logger.error(f"Error listing VMs: {result['error']}")
|
285
|
+
return []
|
286
|
+
else:
|
287
|
+
return []
|
288
|
+
|
289
|
+
async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
290
|
+
"""Run a VM with the given options.
|
291
|
+
|
292
|
+
If the VM does not exist in the storage location, this will attempt to pull it
|
293
|
+
from the Lume registry first.
|
294
|
+
|
295
|
+
Args:
|
296
|
+
image: Image name to use when pulling the VM if it doesn't exist
|
297
|
+
name: Name of the VM to run
|
298
|
+
run_opts: Dictionary of run options (memory, cpu, etc.)
|
299
|
+
storage: Optional storage path override. If provided, this will be used
|
300
|
+
instead of the provider's default storage path.
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
Dictionary with VM run status and information
|
304
|
+
"""
|
305
|
+
# First check if VM exists by trying to get its info
|
306
|
+
vm_info = await self.get_vm(name, storage=storage)
|
307
|
+
|
308
|
+
if "error" in vm_info:
|
309
|
+
# VM doesn't exist, try to pull it
|
310
|
+
self.logger.info(f"VM {name} not found, attempting to pull image {image} from registry...")
|
311
|
+
|
312
|
+
# Call pull_vm with the image parameter
|
313
|
+
pull_result = await self.pull_vm(
|
314
|
+
name=name,
|
315
|
+
image=image,
|
316
|
+
storage=storage
|
317
|
+
)
|
318
|
+
|
319
|
+
# Check if pull was successful
|
320
|
+
if "error" in pull_result:
|
321
|
+
self.logger.error(f"Failed to pull VM image: {pull_result['error']}")
|
322
|
+
return pull_result # Return the error from pull
|
323
|
+
|
324
|
+
self.logger.info(f"Successfully pulled VM image {image} as {name}")
|
325
|
+
|
326
|
+
# Now run the VM with the given options
|
327
|
+
self.logger.info(f"Running VM {name} with options: {run_opts}")
|
328
|
+
|
329
|
+
from ..lume_api import lume_api_run
|
330
|
+
return lume_api_run(
|
331
|
+
vm_name=name,
|
332
|
+
host=self.host,
|
333
|
+
port=self.port,
|
334
|
+
run_opts=run_opts,
|
335
|
+
storage=storage if storage is not None else self.storage,
|
336
|
+
debug=self.verbose,
|
337
|
+
verbose=self.verbose
|
338
|
+
)
|
339
|
+
|
340
|
+
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
341
|
+
"""Stop a running VM.
|
342
|
+
|
343
|
+
If this provider was initialized with ephemeral=True, the VM will also
|
344
|
+
be deleted after it is stopped.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
name: Name of the VM to stop
|
348
|
+
storage: Optional storage path override
|
349
|
+
|
350
|
+
Returns:
|
351
|
+
Dictionary with stop status and information
|
352
|
+
"""
|
353
|
+
# Stop the VM first
|
354
|
+
stop_result = self._lume_api_stop(name, debug=self.verbose)
|
355
|
+
|
356
|
+
# Log ephemeral status for debugging
|
357
|
+
self.logger.info(f"Ephemeral mode status: {self.ephemeral}")
|
358
|
+
|
359
|
+
# If ephemeral mode is enabled, delete the VM after stopping
|
360
|
+
if self.ephemeral and (stop_result.get("success", False) or "error" not in stop_result):
|
361
|
+
self.logger.info(f"Ephemeral mode enabled - deleting VM {name} after stopping")
|
362
|
+
try:
|
363
|
+
delete_result = await self.delete_vm(name, storage=storage)
|
364
|
+
|
365
|
+
# Return combined result
|
366
|
+
return {
|
367
|
+
**stop_result, # Include all stop result info
|
368
|
+
"deleted": True,
|
369
|
+
"delete_result": delete_result
|
370
|
+
}
|
371
|
+
except Exception as e:
|
372
|
+
self.logger.error(f"Failed to delete ephemeral VM {name}: {e}")
|
373
|
+
# Include the error but still return stop result
|
374
|
+
return {
|
375
|
+
**stop_result,
|
376
|
+
"deleted": False,
|
377
|
+
"delete_error": str(e)
|
378
|
+
}
|
379
|
+
|
380
|
+
# Just return the stop result if not ephemeral
|
381
|
+
return stop_result
|
382
|
+
|
383
|
+
async def pull_vm(
|
384
|
+
self,
|
385
|
+
name: str,
|
386
|
+
image: str,
|
387
|
+
storage: Optional[str] = None,
|
388
|
+
registry: str = "ghcr.io",
|
389
|
+
organization: str = "trycua",
|
390
|
+
pull_opts: Optional[Dict[str, Any]] = None,
|
391
|
+
) -> Dict[str, Any]:
|
392
|
+
"""Pull a VM image from the registry.
|
393
|
+
|
394
|
+
Args:
|
395
|
+
name: Name for the VM after pulling
|
396
|
+
image: The image name to pull (e.g. 'macos-sequoia-cua:latest')
|
397
|
+
storage: Optional storage path to use
|
398
|
+
registry: Registry to pull from (default: ghcr.io)
|
399
|
+
organization: Organization in registry (default: trycua)
|
400
|
+
pull_opts: Additional options for pulling the VM (optional)
|
401
|
+
|
402
|
+
Returns:
|
403
|
+
Dictionary with information about the pulled VM
|
404
|
+
|
405
|
+
Raises:
|
406
|
+
RuntimeError: If pull operation fails or image is not provided
|
407
|
+
"""
|
408
|
+
# Validate image parameter
|
409
|
+
if not image:
|
410
|
+
raise ValueError("Image parameter is required for pull_vm")
|
411
|
+
|
412
|
+
self.logger.info(f"Pulling VM image '{image}' as '{name}'")
|
413
|
+
self.logger.info("You can check the pull progress using: lume logs -f")
|
414
|
+
|
415
|
+
# Set default pull_opts if not provided
|
416
|
+
if pull_opts is None:
|
417
|
+
pull_opts = {}
|
418
|
+
|
419
|
+
# Log information about the operation
|
420
|
+
self.logger.debug(f"Pull storage location: {storage or 'default'}")
|
421
|
+
|
422
|
+
try:
|
423
|
+
# Call the lume_api_pull function from lume_api.py
|
424
|
+
from ..lume_api import lume_api_pull
|
425
|
+
|
426
|
+
result = lume_api_pull(
|
427
|
+
image=image,
|
428
|
+
name=name,
|
429
|
+
host=self.host,
|
430
|
+
port=self.port,
|
431
|
+
storage=storage if storage is not None else self.storage,
|
432
|
+
registry=registry,
|
433
|
+
organization=organization,
|
434
|
+
debug=self.verbose,
|
435
|
+
verbose=self.verbose
|
436
|
+
)
|
437
|
+
|
438
|
+
# Check for errors in the result
|
439
|
+
if "error" in result:
|
440
|
+
self.logger.error(f"Failed to pull VM image: {result['error']}")
|
441
|
+
return result
|
442
|
+
|
443
|
+
self.logger.info(f"Successfully pulled VM image '{image}' as '{name}'")
|
444
|
+
return result
|
445
|
+
except Exception as e:
|
446
|
+
self.logger.error(f"Failed to pull VM image '{image}': {e}")
|
447
|
+
return {"error": f"Failed to pull VM: {str(e)}"}
|
448
|
+
|
449
|
+
async def delete_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
450
|
+
"""Delete a VM permanently.
|
451
|
+
|
452
|
+
Args:
|
453
|
+
name: Name of the VM to delete
|
454
|
+
storage: Optional storage path override
|
455
|
+
|
456
|
+
Returns:
|
457
|
+
Dictionary with delete status and information
|
458
|
+
"""
|
459
|
+
self.logger.info(f"Deleting VM {name}...")
|
460
|
+
|
461
|
+
try:
|
462
|
+
# Call the lume_api_delete function we created
|
463
|
+
from ..lume_api import lume_api_delete
|
464
|
+
|
465
|
+
result = lume_api_delete(
|
466
|
+
vm_name=name,
|
467
|
+
host=self.host,
|
468
|
+
port=self.port,
|
469
|
+
storage=storage if storage is not None else self.storage,
|
470
|
+
debug=self.verbose,
|
471
|
+
verbose=self.verbose
|
472
|
+
)
|
473
|
+
|
474
|
+
# Check for errors in the result
|
475
|
+
if "error" in result:
|
476
|
+
self.logger.error(f"Failed to delete VM: {result['error']}")
|
477
|
+
return result
|
478
|
+
|
479
|
+
self.logger.info(f"Successfully deleted VM '{name}'")
|
480
|
+
return result
|
481
|
+
except Exception as e:
|
482
|
+
self.logger.error(f"Failed to delete VM '{name}': {e}")
|
483
|
+
return {"error": f"Failed to delete VM: {str(e)}"}
|
484
|
+
|
485
|
+
async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
486
|
+
"""Update VM configuration."""
|
487
|
+
return self._lume_api_update(name, update_opts, debug=self.verbose)
|
488
|
+
|
489
|
+
async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str:
|
490
|
+
"""Get the IP address of a VM, waiting indefinitely until it's available.
|
491
|
+
|
492
|
+
Args:
|
493
|
+
name: Name of the VM to get the IP for
|
494
|
+
storage: Optional storage path override
|
495
|
+
retry_delay: Delay between retries in seconds (default: 2)
|
496
|
+
|
497
|
+
Returns:
|
498
|
+
IP address of the VM when it becomes available
|
499
|
+
"""
|
500
|
+
# Track total attempts for logging purposes
|
501
|
+
total_attempts = 0
|
502
|
+
|
503
|
+
# Loop indefinitely until we get a valid IP
|
504
|
+
while True:
|
505
|
+
total_attempts += 1
|
506
|
+
|
507
|
+
# Log retry message but not on first attempt
|
508
|
+
if total_attempts > 1:
|
509
|
+
self.logger.info(f"Waiting for VM {name} IP address (attempt {total_attempts})...")
|
510
|
+
|
511
|
+
try:
|
512
|
+
# Get VM information
|
513
|
+
vm_info = await self.get_vm(name, storage=storage)
|
514
|
+
|
515
|
+
# Check if we got a valid IP
|
516
|
+
ip = vm_info.get("ip_address", None)
|
517
|
+
if ip and ip != "unknown" and not ip.startswith("0.0.0.0"):
|
518
|
+
self.logger.info(f"Got valid VM IP address: {ip}")
|
519
|
+
return ip
|
520
|
+
|
521
|
+
# Check the VM status
|
522
|
+
status = vm_info.get("status", "unknown")
|
523
|
+
|
524
|
+
# If VM is not running yet, log and wait
|
525
|
+
if status != "running":
|
526
|
+
self.logger.info(f"VM is not running yet (status: {status}). Waiting...")
|
527
|
+
# If VM is running but no IP yet, wait and retry
|
528
|
+
else:
|
529
|
+
self.logger.info("VM is running but no valid IP address yet. Waiting...")
|
530
|
+
|
531
|
+
except Exception as e:
|
532
|
+
self.logger.warning(f"Error getting VM {name} IP: {e}, continuing to wait...")
|
533
|
+
|
534
|
+
# Wait before next retry
|
535
|
+
await asyncio.sleep(retry_delay)
|
536
|
+
|
537
|
+
# Add progress log every 10 attempts
|
538
|
+
if total_attempts % 10 == 0:
|
539
|
+
self.logger.info(f"Still waiting for VM {name} IP after {total_attempts} attempts...")
|
540
|
+
|
541
|
+
|