cua-computer 0.1.29__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.
@@ -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
@@ -0,0 +1,8 @@
1
+ """Lumier VM provider implementation."""
2
+
3
+ try:
4
+ # Use the same import approach as in the Lume provider
5
+ from .provider import LumierProvider
6
+ HAS_LUMIER = True
7
+ except ImportError:
8
+ HAS_LUMIER = False