Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

drakrun: VM unit test and refactor #496

Merged
merged 28 commits into from
May 21, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f4f1b58
added vm_unit_tests
manorit2001 Mar 13, 2021
75a474f
Merge branch 'master' into vm-unit-test
manorit2001 Mar 24, 2021
3565a22
fixed network setup in tests and added suggestions
manorit2001 Mar 24, 2021
4f38d3c
Merge branch 'master' into vm-unit-test
manorit2001 Apr 14, 2021
2951642
fixup! Merge branch 'master' into vm-unit-test
manorit2001 Apr 14, 2021
c5cb8dd
implemented new functions in vm.py
manorit2001 Apr 15, 2021
5834161
fixup! implemented new functions in vm.py
manorit2001 Apr 16, 2021
83b349a
test VM WIP
manorit2001 Apr 17, 2021
034176b
fix-lint
manorit2001 May 3, 2021
e0cb088
Merge branch 'master' into vm-unit-test
manorit2001 May 11, 2021
a14a072
removed raw xl commands from xtf tests
manorit2001 May 11, 2021
183ff9d
fix?
manorit2001 May 11, 2021
54a8b15
fix
manorit2001 May 12, 2021
2a9c48c
remove exists_vm function as it won't be required
manorit2001 May 12, 2021
f3d8703
fix typo
manorit2001 May 12, 2021
9e53cf2
added logging in vm.py
manorit2001 May 12, 2021
aae2de6
configured live logging in pytests and wrote base test codes
manorit2001 May 12, 2021
df2b4ed
tests completed
manorit2001 May 13, 2021
f1c4462
minor improvements in logging
manorit2001 May 13, 2021
f378610
lint fix
manorit2001 May 13, 2021
2f26c6d
suggested changes
manorit2001 May 14, 2021
7427bab
removing a stale comment
manorit2001 May 14, 2021
9001777
move cfg_path to constructor of class
manorit2001 May 14, 2021
d0a0c4e
removed stdout=subprocess.STDOUT condition
manorit2001 May 17, 2021
8a06602
Update drakrun/drakrun/vm.py
manorit2001 May 18, 2021
a7f02b0
removed comments and suggested changes
manorit2001 May 18, 2021
d2e44bb
log kwargs also in try_run
manorit2001 May 18, 2021
0e0bfe4
Merge branch 'master' into vm-unit-test
manorit2001 May 21, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions drakrun/drakrun/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ def delete_vm_volume(self, vm_id: int):
""" Delete vm_id disk volume """
raise NotImplementedError

def exists_vm(self, vm_id: int):
""" Checks if vm_id disk volume exists """
raise NotImplementedError


class ZfsStorageBackend(StorageBackendBase):
"""Implements storage backend based on ZFS zvols"""
Expand Down Expand Up @@ -193,6 +197,10 @@ def delete_zfs_tank(self):
logging.error(exc.stdout)
raise Exception(f"Couldn't delete {self.zfs_tank_name}")

def exists_vm(self, vm_id: int) -> bool:
""" Checks if vm_id disk volume exists """
return os.path.exists(f'/dev/zvol/{self.zfs_tank_name}/vm-{vm_id}')


class Qcow2StorageBackend(StorageBackendBase):
""" Implements storage backend based on QEMU QCOW2 image format """
Expand Down Expand Up @@ -274,6 +282,11 @@ def delete_vm_volume(self, vm_id: str):
if not safe_delete(disk_path):
raise Exception(f"Couldn't delete vm-{vm_id}.img")

def exists_vm(self, vm_id: int) -> bool:
""" Checks if vm_id disk volume exists """
disk_path = os.path.join(VOLUME_DIR, f"vm-{vm_id}.img")
return os.path.exists(disk_path)


class LvmStorageBackend(StorageBackendBase):
"""Implements storage backend based on lvm storage"""
Expand Down Expand Up @@ -431,6 +444,10 @@ def import_vm0(self, path: str):
check=True
)

def exists_vm(self, vm_id: int):
""" Checks if vm_id disk volume exists """
return os.path.exists(f'/dev/{self.lvm_volume_group}/vm-{vm_id}')


REGISTERED_BACKENDS = {
"qcow2": Qcow2StorageBackend,
Expand Down
23 changes: 23 additions & 0 deletions drakrun/drakrun/test/common_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import shutil
import os
import contextlib


@contextlib.contextmanager
def remove_files(paths):

# change names
for i in paths:
try:
shutil.move(i, f"{i}.bak")
except FileNotFoundError:
pass

yield

# restore the names
for i in paths:
try:
shutil.move(f"{i}.bak", i)
except FileNotFoundError:
pass
45 changes: 45 additions & 0 deletions drakrun/drakrun/test/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import Dict, Tuple
import pytest

# store history of failures per test class name and per index in parametrize (if parametrize used)
_test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {}


def pytest_runtest_makereport(item, call):
if "incremental" in item.keywords:
# incremental marker is used
if call.excinfo is not None:
# the test has failed
# retrieve the class name of the test
cls_name = str(item.cls)
# retrieve the index of the test (if parametrize is used in combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the test function
test_name = item.originalname or item.name
# store in _test_failed_incremental the original name of the failed test
_test_failed_incremental.setdefault(cls_name, {}).setdefault(
parametrize_index, test_name
)


def pytest_runtest_setup(item):
if "incremental" in item.keywords:
# retrieve the class name of the test
cls_name = str(item.cls)
# check if a previous test has failed for this class
if cls_name in _test_failed_incremental:
# retrieve the index of the test (if parametrize is used in combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the first test function to fail for this class name and index
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
# if name found, test has failed for the combination of class name & test name
if test_name is not None:
pytest.xfail("previous test failed ({})".format(test_name))
3 changes: 3 additions & 0 deletions drakrun/drakrun/test/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
markers =
incremental: incremental tests
107 changes: 107 additions & 0 deletions drakrun/drakrun/test/test_vm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import pytest

from drakrun.config import InstallInfo, VM_CONFIG_DIR, VOLUME_DIR
from drakrun.vm import (
VirtualMachine,
generate_vm_conf,
get_all_vm_conf,
delete_vm_conf
)
from drakrun.storage import get_storage_backend
from drakrun.draksetup import find_default_interface
from drakrun.networking import setup_vm_network, delete_vm_network
from common_utils import remove_files
import drakrun
import subprocess
import os
import shutil
import logging


def backup(install_info):
pass


def restore(install_info):
pass


@pytest.fixture(autouse=True)
def enable_logging():
logging.basicConfig(
level=logging.DEBUG,
format='[%(asctime)s][%(levelname)s] %(message)s',
handlers=[logging.StreamHandler()]
)


@pytest.fixture(scope="module")
def backend():
install_info = InstallInfo.try_load()

if install_info is None:
pytest.skip("no install info")
backend = get_storage_backend(install_info)

backup(install_info)

yield backend

restore(install_info)


@pytest.mark.incremental
class TestVM:
def test_vm_name(self, backend):
self.vm = VirtualMachine(backend, 0)
assert self.vm.vm_name == 'vm-0'

def test_backend(self, backend):
# else restore tests will fail
assert backend.exists_vm(0) is True

def test_vm_restore(self, backend):
self.vm = VirtualMachine(backend, 0)

# I think this part should be abstracted and automatically handled when creating or destroying VMs
setup_vm_network(0, True, find_default_interface(), '8.8.8.8')

# if snapshot doesn't exist
with remove_files([os.path.join(VOLUME_DIR, 'snapshot.sav')]):
with pytest.raises(Exception):
self.vm.restore()
assert self.vm.is_running is False

# if configuration file doesn't exist
with remove_files([os.path.join(VM_CONFIG_DIR, 'vm-0.cfg')]):
with pytest.raises(Exception):
self.vm.restore()
assert self.vm.is_running is False

# monkeypatch will be required to hide the storage backend
if backend.exists_vm(0) is False:
with pytest.raises(Exception):
self.vm.restore()
assert self.vm.is_running is False

# should not raise any exceptions if everything is fine
self.vm.restore()
assert self.vm.is_running is True

# restoring a restored VM
# what should be the expected behavior?
# self.vm.restore()

delete_vm_network(0, True, find_default_interface(), '8.8.8.8')

def test_vm_destroy(self):
self.vm = VirtualMachine(backend, 0)

# VM should be running from the previous test

self.vm.destroy()
assert self.vm.is_running is False

# should 2nd time destory raise/log exceptions and then handle it?
# vm.destroy()
# assert vm.is_running is False
21 changes: 21 additions & 0 deletions drakrun/drakrun/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,31 @@ def is_running(self) -> bool:
)
return res.returncode == 0

"""
I have a few suggestions that can help remove the raw xl commands from draksetup
Here is the abstract, should i pursue with the idea?
"""
# def uptime(self):
# xl uptime command

# def create(self):
# xl create command

# def pause(self):
# xl pause command

# def unpause(self):
# xl unpause command

# def save(self, pause=False):
# xl save command
manorit2001 marked this conversation as resolved.
Show resolved Hide resolved

def restore(self) -> None:
""" Restore virtual machine from snapshot.
:raises: subprocess.CalledProcessError
"""
# if the vm is running
# shouldn't we raise exceptions? and then handle it?
manorit2001 marked this conversation as resolved.
Show resolved Hide resolved
if self.is_running:
self.destroy()
cfg_path = Path(VM_CONFIG_DIR) / f"{self.vm_name}.cfg"
Expand Down