cua-computer 0.1.29__py3-none-any.whl → 0.2.1__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.29.dist-info → cua_computer-0.2.1.dist-info}/METADATA +7 -2
- cua_computer-0.2.1.dist-info/RECORD +29 -0
- cua_computer-0.1.29.dist-info/RECORD +0 -19
- {cua_computer-0.1.29.dist-info → cua_computer-0.2.1.dist-info}/WHEEL +0 -0
- {cua_computer-0.1.29.dist-info → cua_computer-0.2.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,559 @@
|
|
1
|
+
"""Shared API utilities for Lume and Lumier providers.
|
2
|
+
|
3
|
+
This module contains shared functions for interacting with the Lume API,
|
4
|
+
used by both the LumeProvider and LumierProvider classes.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import json
|
9
|
+
import subprocess
|
10
|
+
import urllib.parse
|
11
|
+
from typing import Dict, List, Optional, Any
|
12
|
+
|
13
|
+
# Setup logging
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
# Check if curl is available
|
17
|
+
try:
|
18
|
+
subprocess.run(["curl", "--version"], capture_output=True, check=True)
|
19
|
+
HAS_CURL = True
|
20
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
21
|
+
HAS_CURL = False
|
22
|
+
|
23
|
+
|
24
|
+
def lume_api_get(
|
25
|
+
vm_name: str,
|
26
|
+
host: str,
|
27
|
+
port: int,
|
28
|
+
storage: Optional[str] = None,
|
29
|
+
debug: bool = False,
|
30
|
+
verbose: bool = False
|
31
|
+
) -> Dict[str, Any]:
|
32
|
+
"""Use curl to get VM information from Lume API.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
vm_name: Name of the VM to get info for
|
36
|
+
host: API host
|
37
|
+
port: API port
|
38
|
+
storage: Storage path for the VM
|
39
|
+
debug: Whether to show debug output
|
40
|
+
verbose: Enable verbose logging
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Dictionary with VM status information parsed from JSON response
|
44
|
+
"""
|
45
|
+
# URL encode the storage parameter for the query
|
46
|
+
encoded_storage = ""
|
47
|
+
storage_param = ""
|
48
|
+
|
49
|
+
if storage:
|
50
|
+
# First encode the storage path properly
|
51
|
+
encoded_storage = urllib.parse.quote(storage, safe='')
|
52
|
+
storage_param = f"?storage={encoded_storage}"
|
53
|
+
|
54
|
+
# Construct API URL with encoded storage parameter if needed
|
55
|
+
api_url = f"http://{host}:{port}/lume/vms/{vm_name}{storage_param}"
|
56
|
+
|
57
|
+
# Construct the curl command with increased timeouts for more reliability
|
58
|
+
# --connect-timeout: Time to establish connection (15 seconds)
|
59
|
+
# --max-time: Maximum time for the whole operation (20 seconds)
|
60
|
+
# -f: Fail silently (no output at all) on server errors
|
61
|
+
# Add single quotes around URL to ensure special characters are handled correctly
|
62
|
+
cmd = ["curl", "--connect-timeout", "15", "--max-time", "20", "-s", "-f", f"'{api_url}'"]
|
63
|
+
|
64
|
+
# For logging and display, show the properly escaped URL
|
65
|
+
display_cmd = ["curl", "--connect-timeout", "15", "--max-time", "20", "-s", "-f", api_url]
|
66
|
+
|
67
|
+
# Only print the curl command when debug is enabled
|
68
|
+
display_curl_string = ' '.join(display_cmd)
|
69
|
+
if debug or verbose:
|
70
|
+
print(f"DEBUG: Executing curl API call: {display_curl_string}")
|
71
|
+
logger.debug(f"Executing API request: {display_curl_string}")
|
72
|
+
|
73
|
+
# Execute the command - for execution we need to use shell=True to handle URLs with special characters
|
74
|
+
try:
|
75
|
+
# Use a single string with shell=True for proper URL handling
|
76
|
+
shell_cmd = ' '.join(cmd)
|
77
|
+
result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True)
|
78
|
+
|
79
|
+
# Handle curl exit codes
|
80
|
+
if result.returncode != 0:
|
81
|
+
curl_error = "Unknown error"
|
82
|
+
|
83
|
+
# Map common curl error codes to helpful messages
|
84
|
+
if result.returncode == 7:
|
85
|
+
curl_error = "Failed to connect to the API server - it might still be starting up"
|
86
|
+
elif result.returncode == 22:
|
87
|
+
curl_error = "HTTP error returned from API server"
|
88
|
+
elif result.returncode == 28:
|
89
|
+
curl_error = "Operation timeout - the API server is taking too long to respond"
|
90
|
+
elif result.returncode == 52:
|
91
|
+
curl_error = "Empty reply from server - the API server is starting but not fully ready yet"
|
92
|
+
elif result.returncode == 56:
|
93
|
+
curl_error = "Network problem during data transfer - check container networking"
|
94
|
+
|
95
|
+
# Only log at debug level to reduce noise during retries
|
96
|
+
logger.debug(f"API request failed with code {result.returncode}: {curl_error}")
|
97
|
+
|
98
|
+
# Return a more useful error message
|
99
|
+
return {
|
100
|
+
"error": f"API request failed: {curl_error}",
|
101
|
+
"curl_code": result.returncode,
|
102
|
+
"vm_name": vm_name,
|
103
|
+
"status": "unknown" # We don't know the actual status due to API error
|
104
|
+
}
|
105
|
+
|
106
|
+
# Try to parse the response as JSON
|
107
|
+
if result.stdout and result.stdout.strip():
|
108
|
+
try:
|
109
|
+
vm_status = json.loads(result.stdout)
|
110
|
+
if debug or verbose:
|
111
|
+
logger.info(f"Successfully parsed VM status: {vm_status.get('status', 'unknown')}")
|
112
|
+
return vm_status
|
113
|
+
except json.JSONDecodeError as e:
|
114
|
+
# Return the raw response if it's not valid JSON
|
115
|
+
logger.warning(f"Invalid JSON response: {e}")
|
116
|
+
if "Virtual machine not found" in result.stdout:
|
117
|
+
return {"status": "not_found", "message": "VM not found in Lume API"}
|
118
|
+
|
119
|
+
return {"error": f"Invalid JSON response: {result.stdout[:100]}...", "status": "unknown"}
|
120
|
+
else:
|
121
|
+
return {"error": "Empty response from API", "status": "unknown"}
|
122
|
+
except subprocess.SubprocessError as e:
|
123
|
+
logger.error(f"Failed to execute API request: {e}")
|
124
|
+
return {"error": f"Failed to execute API request: {str(e)}", "status": "unknown"}
|
125
|
+
|
126
|
+
|
127
|
+
def lume_api_run(
|
128
|
+
vm_name: str,
|
129
|
+
host: str,
|
130
|
+
port: int,
|
131
|
+
run_opts: Dict[str, Any],
|
132
|
+
storage: Optional[str] = None,
|
133
|
+
debug: bool = False,
|
134
|
+
verbose: bool = False
|
135
|
+
) -> Dict[str, Any]:
|
136
|
+
"""Run a VM using curl.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
vm_name: Name of the VM to run
|
140
|
+
host: API host
|
141
|
+
port: API port
|
142
|
+
run_opts: Dictionary of run options
|
143
|
+
storage: Storage path for the VM
|
144
|
+
debug: Whether to show debug output
|
145
|
+
verbose: Enable verbose logging
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
Dictionary with API response or error information
|
149
|
+
"""
|
150
|
+
# Construct API URL
|
151
|
+
api_url = f"http://{host}:{port}/lume/vms/{vm_name}/run"
|
152
|
+
|
153
|
+
# Prepare JSON payload with required parameters
|
154
|
+
payload = {}
|
155
|
+
|
156
|
+
# Add CPU cores if specified
|
157
|
+
if "cpu" in run_opts:
|
158
|
+
payload["cpu"] = run_opts["cpu"]
|
159
|
+
|
160
|
+
# Add memory if specified
|
161
|
+
if "memory" in run_opts:
|
162
|
+
payload["memory"] = run_opts["memory"]
|
163
|
+
|
164
|
+
# Add storage parameter if specified
|
165
|
+
if storage:
|
166
|
+
payload["storage"] = storage
|
167
|
+
elif "storage" in run_opts:
|
168
|
+
payload["storage"] = run_opts["storage"]
|
169
|
+
|
170
|
+
# Add shared directories if specified
|
171
|
+
if "shared_directories" in run_opts and run_opts["shared_directories"]:
|
172
|
+
payload["sharedDirectories"] = run_opts["shared_directories"]
|
173
|
+
|
174
|
+
# Log the payload for debugging
|
175
|
+
if debug or verbose:
|
176
|
+
print(f"DEBUG: Payload for {vm_name} run request: {json.dumps(payload, indent=2)}")
|
177
|
+
logger.debug(f"API payload: {json.dumps(payload, indent=2)}")
|
178
|
+
|
179
|
+
# Construct the curl command
|
180
|
+
cmd = [
|
181
|
+
"curl", "--connect-timeout", "30", "--max-time", "30",
|
182
|
+
"-s", "-X", "POST", "-H", "Content-Type: application/json",
|
183
|
+
"-d", json.dumps(payload),
|
184
|
+
api_url
|
185
|
+
]
|
186
|
+
|
187
|
+
# Always print the command for debugging
|
188
|
+
if debug or verbose:
|
189
|
+
print(f"DEBUG: Executing curl run API call: {' '.join(cmd)}")
|
190
|
+
print(f"Run payload: {json.dumps(payload, indent=2)}")
|
191
|
+
|
192
|
+
# Execute the command
|
193
|
+
try:
|
194
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
195
|
+
|
196
|
+
if result.returncode != 0:
|
197
|
+
logger.warning(f"API request failed with code {result.returncode}: {result.stderr}")
|
198
|
+
return {"error": f"API request failed: {result.stderr}"}
|
199
|
+
|
200
|
+
# Try to parse the response as JSON
|
201
|
+
if result.stdout and result.stdout.strip():
|
202
|
+
try:
|
203
|
+
response = json.loads(result.stdout)
|
204
|
+
return response
|
205
|
+
except json.JSONDecodeError:
|
206
|
+
# Return the raw response if it's not valid JSON
|
207
|
+
return {"success": True, "message": "VM started successfully", "raw_response": result.stdout}
|
208
|
+
else:
|
209
|
+
return {"success": True, "message": "VM started successfully"}
|
210
|
+
except subprocess.SubprocessError as e:
|
211
|
+
logger.error(f"Failed to execute run request: {e}")
|
212
|
+
return {"error": f"Failed to execute run request: {str(e)}"}
|
213
|
+
|
214
|
+
|
215
|
+
def lume_api_stop(
|
216
|
+
vm_name: str,
|
217
|
+
host: str,
|
218
|
+
port: int,
|
219
|
+
storage: Optional[str] = None,
|
220
|
+
debug: bool = False,
|
221
|
+
verbose: bool = False
|
222
|
+
) -> Dict[str, Any]:
|
223
|
+
"""Stop a VM using curl.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
vm_name: Name of the VM to stop
|
227
|
+
host: API host
|
228
|
+
port: API port
|
229
|
+
storage: Storage path for the VM
|
230
|
+
debug: Whether to show debug output
|
231
|
+
verbose: Enable verbose logging
|
232
|
+
|
233
|
+
Returns:
|
234
|
+
Dictionary with API response or error information
|
235
|
+
"""
|
236
|
+
# Construct API URL
|
237
|
+
api_url = f"http://{host}:{port}/lume/vms/{vm_name}/stop"
|
238
|
+
|
239
|
+
# Prepare JSON payload with required parameters
|
240
|
+
payload = {}
|
241
|
+
|
242
|
+
# Add storage path if specified
|
243
|
+
if storage:
|
244
|
+
payload["storage"] = storage
|
245
|
+
|
246
|
+
# Construct the curl command
|
247
|
+
cmd = [
|
248
|
+
"curl", "--connect-timeout", "15", "--max-time", "20",
|
249
|
+
"-s", "-X", "POST", "-H", "Content-Type: application/json",
|
250
|
+
"-d", json.dumps(payload),
|
251
|
+
api_url
|
252
|
+
]
|
253
|
+
|
254
|
+
# Execute the command
|
255
|
+
try:
|
256
|
+
if debug or verbose:
|
257
|
+
logger.info(f"Executing: {' '.join(cmd)}")
|
258
|
+
|
259
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
260
|
+
|
261
|
+
if result.returncode != 0:
|
262
|
+
logger.warning(f"API request failed with code {result.returncode}: {result.stderr}")
|
263
|
+
return {"error": f"API request failed: {result.stderr}"}
|
264
|
+
|
265
|
+
# Try to parse the response as JSON
|
266
|
+
if result.stdout and result.stdout.strip():
|
267
|
+
try:
|
268
|
+
response = json.loads(result.stdout)
|
269
|
+
return response
|
270
|
+
except json.JSONDecodeError:
|
271
|
+
# Return the raw response if it's not valid JSON
|
272
|
+
return {"success": True, "message": "VM stopped successfully", "raw_response": result.stdout}
|
273
|
+
else:
|
274
|
+
return {"success": True, "message": "VM stopped successfully"}
|
275
|
+
except subprocess.SubprocessError as e:
|
276
|
+
logger.error(f"Failed to execute stop request: {e}")
|
277
|
+
return {"error": f"Failed to execute stop request: {str(e)}"}
|
278
|
+
|
279
|
+
|
280
|
+
def lume_api_update(
|
281
|
+
vm_name: str,
|
282
|
+
host: str,
|
283
|
+
port: int,
|
284
|
+
update_opts: Dict[str, Any],
|
285
|
+
storage: Optional[str] = None,
|
286
|
+
debug: bool = False,
|
287
|
+
verbose: bool = False
|
288
|
+
) -> Dict[str, Any]:
|
289
|
+
"""Update VM settings using curl.
|
290
|
+
|
291
|
+
Args:
|
292
|
+
vm_name: Name of the VM to update
|
293
|
+
host: API host
|
294
|
+
port: API port
|
295
|
+
update_opts: Dictionary of update options
|
296
|
+
storage: Storage path for the VM
|
297
|
+
debug: Whether to show debug output
|
298
|
+
verbose: Enable verbose logging
|
299
|
+
|
300
|
+
Returns:
|
301
|
+
Dictionary with API response or error information
|
302
|
+
"""
|
303
|
+
# Construct API URL
|
304
|
+
api_url = f"http://{host}:{port}/lume/vms/{vm_name}/update"
|
305
|
+
|
306
|
+
# Prepare JSON payload with required parameters
|
307
|
+
payload = {}
|
308
|
+
|
309
|
+
# Add CPU cores if specified
|
310
|
+
if "cpu" in update_opts:
|
311
|
+
payload["cpu"] = update_opts["cpu"]
|
312
|
+
|
313
|
+
# Add memory if specified
|
314
|
+
if "memory" in update_opts:
|
315
|
+
payload["memory"] = update_opts["memory"]
|
316
|
+
|
317
|
+
# Add storage path if specified
|
318
|
+
if storage:
|
319
|
+
payload["storage"] = storage
|
320
|
+
|
321
|
+
# Construct the curl command
|
322
|
+
cmd = [
|
323
|
+
"curl", "--connect-timeout", "15", "--max-time", "20",
|
324
|
+
"-s", "-X", "POST", "-H", "Content-Type: application/json",
|
325
|
+
"-d", json.dumps(payload),
|
326
|
+
api_url
|
327
|
+
]
|
328
|
+
|
329
|
+
# Execute the command
|
330
|
+
try:
|
331
|
+
if debug:
|
332
|
+
logger.info(f"Executing: {' '.join(cmd)}")
|
333
|
+
|
334
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
335
|
+
|
336
|
+
if result.returncode != 0:
|
337
|
+
logger.warning(f"API request failed with code {result.returncode}: {result.stderr}")
|
338
|
+
return {"error": f"API request failed: {result.stderr}"}
|
339
|
+
|
340
|
+
# Try to parse the response as JSON
|
341
|
+
if result.stdout and result.stdout.strip():
|
342
|
+
try:
|
343
|
+
response = json.loads(result.stdout)
|
344
|
+
return response
|
345
|
+
except json.JSONDecodeError:
|
346
|
+
# Return the raw response if it's not valid JSON
|
347
|
+
return {"success": True, "message": "VM updated successfully", "raw_response": result.stdout}
|
348
|
+
else:
|
349
|
+
return {"success": True, "message": "VM updated successfully"}
|
350
|
+
except subprocess.SubprocessError as e:
|
351
|
+
logger.error(f"Failed to execute update request: {e}")
|
352
|
+
return {"error": f"Failed to execute update request: {str(e)}"}
|
353
|
+
|
354
|
+
|
355
|
+
def lume_api_pull(
|
356
|
+
image: str,
|
357
|
+
name: str,
|
358
|
+
host: str,
|
359
|
+
port: int,
|
360
|
+
storage: Optional[str] = None,
|
361
|
+
registry: str = "ghcr.io",
|
362
|
+
organization: str = "trycua",
|
363
|
+
debug: bool = False,
|
364
|
+
verbose: bool = False
|
365
|
+
) -> Dict[str, Any]:
|
366
|
+
"""Pull a VM image from a registry using curl.
|
367
|
+
|
368
|
+
Args:
|
369
|
+
image: Name/tag of the image to pull
|
370
|
+
name: Name to give the VM after pulling
|
371
|
+
host: API host
|
372
|
+
port: API port
|
373
|
+
storage: Storage path for the VM
|
374
|
+
registry: Registry to pull from (default: ghcr.io)
|
375
|
+
organization: Organization in registry (default: trycua)
|
376
|
+
debug: Whether to show debug output
|
377
|
+
verbose: Enable verbose logging
|
378
|
+
|
379
|
+
Returns:
|
380
|
+
Dictionary with pull status and information
|
381
|
+
"""
|
382
|
+
# Prepare pull request payload
|
383
|
+
pull_payload = {
|
384
|
+
"image": image, # Use provided image name
|
385
|
+
"name": name, # Always use name as the target VM name
|
386
|
+
"registry": registry,
|
387
|
+
"organization": organization
|
388
|
+
}
|
389
|
+
|
390
|
+
if storage:
|
391
|
+
pull_payload["storage"] = storage
|
392
|
+
|
393
|
+
# Construct pull command with proper JSON payload
|
394
|
+
pull_cmd = [
|
395
|
+
"curl"
|
396
|
+
]
|
397
|
+
|
398
|
+
if not verbose:
|
399
|
+
pull_cmd.append("-s")
|
400
|
+
|
401
|
+
pull_cmd.extend([
|
402
|
+
"-X", "POST",
|
403
|
+
"-H", "Content-Type: application/json",
|
404
|
+
"-d", json.dumps(pull_payload),
|
405
|
+
f"http://{host}:{port}/lume/pull"
|
406
|
+
])
|
407
|
+
|
408
|
+
if debug or verbose:
|
409
|
+
print(f"DEBUG: Executing curl API call: {' '.join(pull_cmd)}")
|
410
|
+
logger.debug(f"Executing API request: {' '.join(pull_cmd)}")
|
411
|
+
|
412
|
+
try:
|
413
|
+
# Execute pull command
|
414
|
+
result = subprocess.run(pull_cmd, capture_output=True, text=True)
|
415
|
+
|
416
|
+
if result.returncode != 0:
|
417
|
+
error_msg = f"Failed to pull VM {name}: {result.stderr}"
|
418
|
+
logger.error(error_msg)
|
419
|
+
return {"error": error_msg}
|
420
|
+
|
421
|
+
try:
|
422
|
+
response = json.loads(result.stdout)
|
423
|
+
logger.info(f"Successfully initiated pull for VM {name}")
|
424
|
+
return response
|
425
|
+
except json.JSONDecodeError:
|
426
|
+
if result.stdout:
|
427
|
+
logger.info(f"Pull response: {result.stdout}")
|
428
|
+
return {"success": True, "message": f"Successfully initiated pull for VM {name}"}
|
429
|
+
|
430
|
+
except subprocess.SubprocessError as e:
|
431
|
+
error_msg = f"Failed to execute pull command: {str(e)}"
|
432
|
+
logger.error(error_msg)
|
433
|
+
return {"error": error_msg}
|
434
|
+
|
435
|
+
|
436
|
+
def lume_api_delete(
|
437
|
+
vm_name: str,
|
438
|
+
host: str,
|
439
|
+
port: int,
|
440
|
+
storage: Optional[str] = None,
|
441
|
+
debug: bool = False,
|
442
|
+
verbose: bool = False
|
443
|
+
) -> Dict[str, Any]:
|
444
|
+
"""Delete a VM using curl.
|
445
|
+
|
446
|
+
Args:
|
447
|
+
vm_name: Name of the VM to delete
|
448
|
+
host: API host
|
449
|
+
port: API port
|
450
|
+
storage: Storage path for the VM
|
451
|
+
debug: Whether to show debug output
|
452
|
+
verbose: Enable verbose logging
|
453
|
+
|
454
|
+
Returns:
|
455
|
+
Dictionary with API response or error information
|
456
|
+
"""
|
457
|
+
# URL encode the storage parameter for the query
|
458
|
+
encoded_storage = ""
|
459
|
+
storage_param = ""
|
460
|
+
|
461
|
+
if storage:
|
462
|
+
# First encode the storage path properly
|
463
|
+
encoded_storage = urllib.parse.quote(storage, safe='')
|
464
|
+
storage_param = f"?storage={encoded_storage}"
|
465
|
+
|
466
|
+
# Construct API URL with encoded storage parameter if needed
|
467
|
+
api_url = f"http://{host}:{port}/lume/vms/{vm_name}{storage_param}"
|
468
|
+
|
469
|
+
# Construct the curl command for DELETE operation - using much longer timeouts matching shell implementation
|
470
|
+
cmd = ["curl", "--connect-timeout", "6000", "--max-time", "5000", "-s", "-X", "DELETE", f"'{api_url}'"]
|
471
|
+
|
472
|
+
# For logging and display, show the properly escaped URL
|
473
|
+
display_cmd = ["curl", "--connect-timeout", "6000", "--max-time", "5000", "-s", "-X", "DELETE", api_url]
|
474
|
+
|
475
|
+
# Only print the curl command when debug is enabled
|
476
|
+
display_curl_string = ' '.join(display_cmd)
|
477
|
+
if debug or verbose:
|
478
|
+
print(f"DEBUG: Executing curl API call: {display_curl_string}")
|
479
|
+
logger.debug(f"Executing API request: {display_curl_string}")
|
480
|
+
|
481
|
+
# Execute the command - for execution we need to use shell=True to handle URLs with special characters
|
482
|
+
try:
|
483
|
+
# Use a single string with shell=True for proper URL handling
|
484
|
+
shell_cmd = ' '.join(cmd)
|
485
|
+
result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True)
|
486
|
+
|
487
|
+
# Handle curl exit codes
|
488
|
+
if result.returncode != 0:
|
489
|
+
curl_error = "Unknown error"
|
490
|
+
|
491
|
+
# Map common curl error codes to helpful messages
|
492
|
+
if result.returncode == 7:
|
493
|
+
curl_error = "Failed to connect to the API server - it might still be starting up"
|
494
|
+
elif result.returncode == 22:
|
495
|
+
curl_error = "HTTP error returned from API server"
|
496
|
+
elif result.returncode == 28:
|
497
|
+
curl_error = "Operation timeout - the API server is taking too long to respond"
|
498
|
+
elif result.returncode == 52:
|
499
|
+
curl_error = "Empty reply from server - the API server is starting but not fully ready yet"
|
500
|
+
elif result.returncode == 56:
|
501
|
+
curl_error = "Network problem during data transfer - check container networking"
|
502
|
+
|
503
|
+
# Only log at debug level to reduce noise during retries
|
504
|
+
logger.debug(f"API request failed with code {result.returncode}: {curl_error}")
|
505
|
+
|
506
|
+
# Return a more useful error message
|
507
|
+
return {
|
508
|
+
"error": f"API request failed: {curl_error}",
|
509
|
+
"curl_code": result.returncode,
|
510
|
+
"vm_name": vm_name,
|
511
|
+
"storage": storage
|
512
|
+
}
|
513
|
+
|
514
|
+
# Try to parse the response as JSON
|
515
|
+
if result.stdout and result.stdout.strip():
|
516
|
+
try:
|
517
|
+
response = json.loads(result.stdout)
|
518
|
+
return response
|
519
|
+
except json.JSONDecodeError:
|
520
|
+
# Return the raw response if it's not valid JSON
|
521
|
+
return {"success": True, "message": "VM deleted successfully", "raw_response": result.stdout}
|
522
|
+
else:
|
523
|
+
return {"success": True, "message": "VM deleted successfully"}
|
524
|
+
except subprocess.SubprocessError as e:
|
525
|
+
logger.error(f"Failed to execute delete request: {e}")
|
526
|
+
return {"error": f"Failed to execute delete request: {str(e)}"}
|
527
|
+
|
528
|
+
|
529
|
+
def parse_memory(memory_str: str) -> int:
|
530
|
+
"""Parse memory string to MB integer.
|
531
|
+
|
532
|
+
Examples:
|
533
|
+
"8GB" -> 8192
|
534
|
+
"1024MB" -> 1024
|
535
|
+
"512" -> 512
|
536
|
+
|
537
|
+
Returns:
|
538
|
+
Memory value in MB
|
539
|
+
"""
|
540
|
+
if isinstance(memory_str, int):
|
541
|
+
return memory_str
|
542
|
+
|
543
|
+
if isinstance(memory_str, str):
|
544
|
+
# Extract number and unit
|
545
|
+
import re
|
546
|
+
match = re.match(r"(\d+)([A-Za-z]*)", memory_str)
|
547
|
+
if match:
|
548
|
+
value, unit = match.groups()
|
549
|
+
value = int(value)
|
550
|
+
unit = unit.upper()
|
551
|
+
|
552
|
+
if unit == "GB" or unit == "G":
|
553
|
+
return value * 1024
|
554
|
+
elif unit == "MB" or unit == "M" or unit == "":
|
555
|
+
return value
|
556
|
+
|
557
|
+
# Default fallback
|
558
|
+
logger.warning(f"Could not parse memory string '{memory_str}', using 8GB default")
|
559
|
+
return 8192 # Default to 8GB
|