commit 44cd7625ec1e483c3a0296f370a5e21551310aac
parent 4b9c40e8aaef97db3bbbfab0fb43d03f7d63c0f8
Author: Stefan Koch <programming@stefan-koch.name>
Date: Sat, 30 Jan 2021 13:05:01 +0100
list all VMs, not only running VMs
Diffstat:
4 files changed, 64 insertions(+), 4 deletions(-)
diff --git a/aetherscale/computing.py b/aetherscale/computing.py
@@ -5,6 +5,7 @@ from pathlib import Path
import pika
import psutil
import random
+import re
import shlex
import string
import subprocess
@@ -101,12 +102,33 @@ class ComputingHandler:
self.available_vpn_ports = config.VPN_PORTS
def list_vms(self, _: Dict[str, Any]) -> Iterator[List[Dict[str, Any]]]:
- vms = []
-
+ all_vms = []
+ for service in self.service_manager.list_services():
+ try:
+ all_vms.append(vm_id_from_systemd_unit(service))
+ except ValueError:
+ # Not a VM systemd unit
+ pass
+
+ running_vms = []
for proc in psutil.process_iter(['pid', 'name']):
if proc.name().startswith('vm-'):
vm_id = proc.name()[3:]
+ running_vms.append(vm_id)
+
+ orphaned_vms = set(running_vms).difference(all_vms)
+ for orphaned_vm in orphaned_vms:
+ logging.warning(f'VM "{orphaned_vm} is orphaned')
+ vms = []
+ for vm_id in all_vms:
+ if vm_id not in running_vms:
+ vms.append({
+ 'vm-id': vm_id,
+ })
+ else:
+ # Fetch IP info for running VMs
+ # TODO: IP info should be moved to a details request
socket_file = qemu_socket_guest_agent(vm_id)
hint = None
ip_addresses = []
@@ -423,6 +445,15 @@ def systemd_unit_name_for_vm(vm_id: str) -> str:
return f'aetherscale-vm-{vm_id}.service'
+def vm_id_from_systemd_unit(systemd_unit: str) -> str:
+ m = re.match(r'aetherscale-vm-([a-z0-9]+)(?:\.service)?', systemd_unit)
+ if m:
+ return m.group(1)
+ else:
+ raise ValueError(
+ f'{systemd_unit} is not a valid systemd unit file for a VM')
+
+
def noop_responder(_: Dict[str, Any]):
pass
diff --git a/aetherscale/services.py b/aetherscale/services.py
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
from pathlib import Path
import shutil
import subprocess
-from typing import Optional
+from typing import Optional, List
from aetherscale.execution import run_command_chain
@@ -52,6 +52,10 @@ class ServiceManager(ABC):
def service_exists(self, service_name: str) -> bool:
"""Check whether a service is currently installed"""
+ @abstractmethod
+ def list_services(self) -> List[str]:
+ """List all available services"""
+
class SystemdServiceManager(ServiceManager):
def __init__(self, unit_folder: Path):
@@ -142,5 +146,14 @@ class SystemdServiceManager(ServiceManager):
def service_exists(self, service_name: str) -> bool:
return self._systemd_unit_path(service_name).is_file()
+ def list_services(self) -> List[str]:
+ services = []
+
+ for filepath in self.unit_folder.iterdir():
+ if filepath.suffix == '.service':
+ services.append(filepath.name)
+
+ return services
+
def _systemd_unit_path(self, service_name: str) -> Path:
return self.unit_folder / service_name
diff --git a/tests/conftest.py b/tests/conftest.py
@@ -1,6 +1,6 @@
from pathlib import Path
import pytest
-from typing import Optional
+from typing import Optional, List
from aetherscale.services import ServiceManager
import aetherscale.timing
@@ -78,4 +78,7 @@ def mock_service_manager():
def service_exists(self, service_name: str) -> bool:
return service_name in self.services
+ def list_services(self) -> List[str]:
+ return self.services
+
return MockServiceManager()
diff --git a/tests/test_computing.py b/tests/test_computing.py
@@ -41,27 +41,35 @@ def test_vm_lifecycle(tmppath, mock_service_manager: ServiceManager):
with base_image(tmppath) as img:
results = list(handler.create_vm({'image': img.stem}))
+ list_results = list(handler.list_vms({}))
vm_id = results[0]['vm-id']
service_name = computing.systemd_unit_name_for_vm(vm_id)
assert results[0]['status'] == 'allocating'
assert results[1]['status'] == 'starting'
assert mock_service_manager.service_is_running(service_name)
+ assert list_results[0][0]['vm-id'] == vm_id
# TODO: Test graceful stop, needs mock of QemuMonitor
results = list(handler.stop_vm({'vm-id': vm_id, 'kill': True}))
+ list_results = list(handler.list_vms({}))
assert results[0]['status'] == 'killed'
assert mock_service_manager.service_exists(service_name)
assert not mock_service_manager.service_is_running(service_name)
+ assert list_results[0][0]['vm-id'] == vm_id
results = list(handler.start_vm({'vm-id': vm_id}))
+ list_results = list(handler.list_vms({}))
assert results[0]['status'] == 'starting'
assert mock_service_manager.service_exists(service_name)
assert mock_service_manager.service_is_running(service_name)
+ assert list_results[0][0]['vm-id'] == vm_id
results = list(handler.delete_vm({'vm-id': vm_id}))
+ list_results = list(handler.list_vms({}))
assert results[0]['status'] == 'deleted'
assert not mock_service_manager.service_exists(service_name)
assert not mock_service_manager.service_is_running(service_name)
+ assert len(list_results[0]) == 0
def test_run_missing_base_image(tmppath, mock_service_manager: ServiceManager):
@@ -80,3 +88,8 @@ def test_run_missing_base_image(tmppath, mock_service_manager: ServiceManager):
with pytest.raises(ValueError):
# make sure to exhaust the iterator
list(handler.create_vm({}))
+
+
+def test_vm_id_systemd_unit():
+ assert 'myvmid' == computing.vm_id_from_systemd_unit(
+ computing.systemd_unit_name_for_vm('myvmid'))