aetherscale

[unmaintained] code for a cloud provider tutorial
Log | Files | Refs | README | LICENSE

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:
Maetherscale/computing.py | 35+++++++++++++++++++++++++++++++++--
Maetherscale/services.py | 15++++++++++++++-
Mtests/conftest.py | 5++++-
Mtests/test_computing.py | 13+++++++++++++
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'))