55from typing import Dict , List , Any , Optional
66import re
77from async_timeout import timeout
8+ from enum import Enum
9+ import json
810
911_LOGGER = logging .getLogger (__name__ )
1012
13+ class VMState (Enum ):
14+ """VM states matching Unraid/libvirt states."""
15+ RUNNING = 'running'
16+ STOPPED = 'shut off'
17+ PAUSED = 'paused'
18+ IDLE = 'idle'
19+ IN_SHUTDOWN = 'in shutdown'
20+ CRASHED = 'crashed'
21+ SUSPENDED = 'pmsuspended'
22+
23+ @classmethod
24+ def is_running (cls , state : str ) -> bool :
25+ """Check if the state represents a running VM."""
26+ return state .lower () == cls .RUNNING .value
27+
28+ @classmethod
29+ def parse (cls , state : str ) -> str :
30+ """Parse the VM state string."""
31+ state = state .lower ().strip ()
32+ try :
33+ return next (s .value for s in cls if s .value == state )
34+ except StopIteration :
35+ return state
36+
37+ class ContainerStates (Enum ):
38+ """Docker container states."""
39+ RUNNING = 'running'
40+ EXITED = 'exited'
41+ PAUSED = 'paused'
42+ RESTARTING = 'restarting'
43+ DEAD = 'dead'
44+ CREATED = 'created'
45+
46+ @classmethod
47+ def parse (cls , state : str ) -> str :
48+ """Parse the container state string."""
49+ state = state .lower ().strip ()
50+ try :
51+ return next (s .value for s in cls if s .value == state )
52+ except StopIteration :
53+ return state
54+
1155class UnraidAPI :
1256 """API client for interacting with Unraid servers."""
1357
@@ -397,18 +441,35 @@ async def get_docker_containers(self) -> List[Dict[str, Any]]:
397441 """Fetch information about Docker containers."""
398442 try :
399443 _LOGGER .debug ("Fetching Docker container information" )
400- result = await self .execute_command ("docker ps -a --format '{{.Names}}|{{.State}}'" )
444+ # Get basic container info with proven format
445+ result = await self .execute_command ("docker ps -a --format '{{.Names}}|{{.State}}|{{.ID}}|{{.Image}}'" )
401446 if result .exit_status != 0 :
402447 _LOGGER .error ("Docker container list command failed with exit status %d" , result .exit_status )
403448 return []
404-
449+
405450 containers = []
406451 for line in result .stdout .splitlines ():
407452 parts = line .split ('|' )
408- if len (parts ) == 2 :
409- containers .append ({"name" : parts [0 ], "status" : parts [1 ]})
453+ if len (parts ) == 4 : # Now expecting 4 parts
454+ container_name = parts [0 ].strip ()
455+ # Get container icon if available
456+ icon_path = f"/var/lib/docker/unraid/images/{ container_name } -icon.png"
457+ icon_result = await self .execute_command (
458+ f"[ -f { icon_path } ] && (base64 { icon_path } ) || echo ''"
459+ )
460+ icon_data = icon_result .stdout [0 ] if icon_result .exit_status == 0 else ""
461+
462+ containers .append ({
463+ "name" : container_name ,
464+ "state" : ContainerStates .parse (parts [1 ].strip ()),
465+ "status" : parts [1 ].strip (),
466+ "id" : parts [2 ].strip (),
467+ "image" : parts [3 ].strip (),
468+ "icon" : icon_data
469+ })
410470 else :
411471 _LOGGER .warning ("Unexpected format in docker container output: %s" , line )
472+
412473 return containers
413474 except Exception as e :
414475 _LOGGER .error ("Error getting docker containers: %s" , str (e ))
@@ -417,8 +478,8 @@ async def get_docker_containers(self) -> List[Dict[str, Any]]:
417478 async def start_container (self , container_name : str ) -> bool :
418479 """Start a Docker container."""
419480 try :
420- _LOGGER .debug ("Starting Docker container: %s" , container_name )
421- result = await self .execute_command (f" docker start { container_name } " )
481+ _LOGGER .debug ("Starting container: %s" , container_name )
482+ result = await self .execute_command (f' docker start " { container_name } "' )
422483 if result .exit_status != 0 :
423484 _LOGGER .error ("Failed to start container %s: %s" , container_name , result .stderr )
424485 return False
@@ -427,12 +488,12 @@ async def start_container(self, container_name: str) -> bool:
427488 except Exception as e :
428489 _LOGGER .error ("Error starting container %s: %s" , container_name , str (e ))
429490 return False
430-
491+
431492 async def stop_container (self , container_name : str ) -> bool :
432493 """Stop a Docker container."""
433494 try :
434- _LOGGER .debug ("Stopping Docker container: %s" , container_name )
435- result = await self .execute_command (f" docker stop { container_name } " )
495+ _LOGGER .debug ("Stopping container: %s" , container_name )
496+ result = await self .execute_command (f' docker stop " { container_name } "' )
436497 if result .exit_status != 0 :
437498 _LOGGER .error ("Failed to stop container %s: %s" , container_name , result .stderr )
438499 return False
@@ -455,43 +516,100 @@ async def get_vms(self) -> List[Dict[str, Any]]:
455516 for line in result .stdout .splitlines ():
456517 if line .strip ():
457518 name = line .strip ()
458- status = await self ._get_vm_status (name )
459- vms .append ({"name" : name , "status" : status })
519+ status = await self .get_vm_status (name )
520+ os_type = await self .get_vm_os_info (name )
521+ vms .append ({
522+ "name" : name ,
523+ "status" : status ,
524+ "os_type" : os_type
525+ })
460526 return vms
461527 except Exception as e :
462528 _LOGGER .error ("Error getting VMs: %s" , str (e ))
463529 return []
530+
531+ async def get_vm_os_info (self , vm_name : str ) -> str :
532+ """Get the OS type of a VM."""
533+ try :
534+ # First try to get OS info from VM XML
535+ result = await self .execute_command (f'virsh dumpxml "{ vm_name } " | grep "<os>"' )
536+ xml_output = result .stdout
537+
538+ # Check for Windows-specific indicators
539+ if any (indicator in '\n ' .join (xml_output ).lower () for indicator in ['windows' , 'win' , 'microsoft' ]):
540+ return 'windows'
541+
542+ # Try to get detailed OS info if available
543+ result = await self .execute_command (f'virsh domosinfo "{ vm_name } " 2>/dev/null' )
544+ if result .exit_status == 0 :
545+ os_info = '\n ' .join (result .stdout ).lower ()
546+ if any (indicator in os_info for indicator in ['windows' , 'win' , 'microsoft' ]):
547+ return 'windows'
548+ elif any (indicator in os_info for indicator in ['linux' , 'unix' , 'ubuntu' , 'debian' , 'centos' , 'fedora' , 'rhel' ]):
549+ return 'linux'
550+
551+ # Default to checking common paths in VM name
552+ vm_name_lower = vm_name .lower ()
553+ if any (win_term in vm_name_lower for win_term in ['windows' , 'win' ]):
554+ return 'windows'
555+ elif any (linux_term in vm_name_lower for linux_term in ['linux' , 'ubuntu' , 'debian' , 'centos' , 'fedora' , 'rhel' ]):
556+ return 'linux'
557+
558+ return 'unknown'
559+ except Exception as e :
560+ _LOGGER .debug ("Error getting OS info for VM %s: %s" , vm_name , str (e ))
561+ return 'unknown'
464562
465- async def _get_vm_status (self , vm_name : str ) -> str :
466- """Get the status of a specific virtual machine."""
563+ async def get_vm_status (self , vm_name : str ) -> str :
564+ """Get detailed status of a specific virtual machine."""
467565 try :
468566 result = await self .execute_command (f"virsh domstate { vm_name } " )
469567 if result .exit_status != 0 :
470- _LOGGER .error ("VM status command for %s failed with exit status %d " , vm_name , result .exit_status )
471- return "unknown"
472- return result .stdout .strip ()
568+ _LOGGER .error ("Failed to get VM status for %s: %s " , vm_name , result .stderr )
569+ return VMState . CRASHED . value
570+ return VMState . parse ( result .stdout .strip () )
473571 except Exception as e :
474572 _LOGGER .error ("Error getting VM status for %s: %s" , vm_name , str (e ))
475- return "unknown"
573+ return VMState . CRASHED . value
476574
477- async def start_vm (self , vm_name : str ) -> bool :
478- """Start a virtual machine."""
575+ async def stop_vm (self , vm_name : str ) -> bool :
576+ """Stop a virtual machine using ACPI shutdown ."""
479577 try :
480- _LOGGER .debug ("Starting VM: %s" , vm_name )
481- result = await self .execute_command (f"virsh start { vm_name } " )
482- return result .exit_status == 0 and "started" in result .stdout .lower ()
578+ _LOGGER .debug ("Stopping VM: %s" , vm_name )
579+ result = await self .execute_command (f'virsh shutdown "{ vm_name } " --mode acpi' )
580+ success = result .exit_status == 0
581+
582+ if success :
583+ # Wait for the VM to actually shut down
584+ for _ in range (30 ): # Wait up to 60 seconds
585+ await asyncio .sleep (2 )
586+ status = await self .get_vm_status (vm_name )
587+ if status == VMState .STOPPED .value :
588+ return True
589+ return False
590+ return False
483591 except Exception as e :
484- _LOGGER .error ("Error starting VM %s: %s" , vm_name , str (e ))
592+ _LOGGER .error ("Error stopping VM %s: %s" , vm_name , str (e ))
485593 return False
486594
487- async def stop_vm (self , vm_name : str ) -> bool :
488- """Stop a virtual machine."""
595+ async def start_vm (self , vm_name : str ) -> bool :
596+ """Start a virtual machine and wait for it to be running ."""
489597 try :
490- _LOGGER .debug ("Stopping VM: %s" , vm_name )
491- result = await self .execute_command (f"virsh shutdown { vm_name } " )
492- return result .exit_status == 0 and "shutting down" in result .stdout .lower ()
598+ _LOGGER .debug ("Starting VM: %s" , vm_name )
599+ result = await self .execute_command (f'virsh start "{ vm_name } "' )
600+ success = result .exit_status == 0
601+
602+ if success :
603+ # Wait for the VM to actually start
604+ for _ in range (15 ): # Wait up to 30 seconds
605+ await asyncio .sleep (2 )
606+ status = await self .get_vm_status (vm_name )
607+ if status == VMState .RUNNING .value :
608+ return True
609+ return False
610+ return False
493611 except Exception as e :
494- _LOGGER .error ("Error stopping VM %s: %s" , vm_name , str (e ))
612+ _LOGGER .error ("Error starting VM %s: %s" , vm_name , str (e ))
495613 return False
496614
497615 async def get_user_scripts (self ) -> List [Dict [str , Any ]]:
0 commit comments