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 24 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
37 changes: 15 additions & 22 deletions drakrun/drakrun/draksetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,12 @@ def perform_xtf():
tmpf.write(test_cfg)
tmpf.flush()

test_hvm64 = VirtualMachine(None, None, "test-hvm64-example", tmpf.name)
logging.info('Checking if the test domain already exists...')
subprocess.run('xl destroy test-hvm64-example', shell=True)
test_hvm64.destroy()

logging.info('Creating new test domain...')
subprocess.run(f'xl create -p {tmpf.name}', shell=True, stderr=subprocess.STDOUT, timeout=30, check=True)
test_hvm64.create(pause=True, timeout=30)

module_dir = os.path.dirname(os.path.realpath(__file__))
test_altp2m_tool = os.path.join(module_dir, "tools", "test-altp2m")
Expand All @@ -217,12 +218,12 @@ def perform_xtf():
except subprocess.CalledProcessError as e:
output = e.output.decode('utf-8', 'replace')
logging.error(f'Failed to enable altp2m on domain. Your hardware might not support Extended Page Tables. Logs:\n{output}')
subprocess.run('xl destroy test-hvm64-example', shell=True)
test_hvm64.destroy()
return False

logging.info('Performing simple XTF test...')
p = subprocess.Popen(['xl', 'console', 'test-hvm64-example'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run('xl unpause test-hvm64-example', shell=True, stderr=subprocess.STDOUT, timeout=30, check=True)
test_hvm64.unpause(timeout=30)
stdout_b, _ = p.communicate(timeout=10)

stdout_text = stdout_b.decode('utf-8')
Expand Down Expand Up @@ -343,17 +344,13 @@ def install(vcpus, memory, storage_backend, disk_size, iso_path, zfs_tank_name,
)
install_info.save()

try:
subprocess.check_output('xl uptime vm-0', shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError:
pass
else:
logging.info('Detected that vm-0 is already running, stopping it.')
subprocess.run('xl destroy vm-0', shell=True, check=True)
backend = get_storage_backend(install_info)

vm0 = VirtualMachine(backend, 0)
vm0.destroy()

generate_vm_conf(install_info, 0)

backend = get_storage_backend(install_info)
backend.initialize_vm0_volume(disk_size)

try:
Expand All @@ -373,11 +370,7 @@ def install(vcpus, memory, storage_backend, disk_size, iso_path, zfs_tank_name,

cfg_path = os.path.join(VM_CONFIG_DIR, "vm-0.cfg")

try:
subprocess.run('xl create {}'.format(shlex.quote(cfg_path)), shell=True, check=True)
except subprocess.CalledProcessError:
logging.exception("Failed to launch VM vm-0")
return
vm0.create()

logging.info("-" * 80)
logging.info("Initial VM setup is complete and the vm-0 was launched.")
Expand Down Expand Up @@ -544,9 +537,9 @@ def postinstall(report, generate_usermode):
install_info = InstallInfo.load()
storage_backend = get_storage_backend(install_info)

vm = VirtualMachine(storage_backend, 0)
vm0 = VirtualMachine(storage_backend, 0)

if vm.is_running is False:
if vm0.is_running is False:
logging.exception("vm-0 is not running")
return

Expand Down Expand Up @@ -596,14 +589,14 @@ def postinstall(report, generate_usermode):
logging.info("Saving VM snapshot...")

# snapshot domain but don't destroy it, leave it in paused state
subprocess.check_output('xl save -p vm-0 ' + os.path.join(VOLUME_DIR, "snapshot.sav"), shell=True)
vm0.save(os.path.join(VOLUME_DIR, "snapshot.sav"), pause=True)
logging.info("Snapshot was saved succesfully.")

logging.info("Snapshotting persistent memory...")
storage_backend.snapshot_vm0_volume()

logging.info("Unpausing VM")
subprocess.check_output('xl unpause vm-0', shell=True)
vm0.unpause()

injector = Injector('vm-0', runtime_info, kernel_profile)
if generate_usermode:
Expand All @@ -614,7 +607,7 @@ def postinstall(report, generate_usermode):
logging.warning("Generating usermode profiles failed")
logging.exception(e)

subprocess.check_output('xl destroy vm-0', shell=True)
vm0.destroy()

if report:
send_usage_report({
Expand Down
4 changes: 4 additions & 0 deletions drakrun/drakrun/test/pytest.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
[pytest]
markers =
incremental: incremental tests
log_cli = 1
log_cli_level = DEBUG
log_cli_format = %(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)
log_cli_date_format=%Y-%m-%d %H:%M:%S
269 changes: 269 additions & 0 deletions drakrun/drakrun/test/test_vm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import pytest

from drakrun.config import InstallInfo
from drakrun.vm import VirtualMachine
from drakrun.util import safe_delete
from _pytest.monkeypatch import MonkeyPatch
from common_utils import remove_files, tool_exists
import tempfile
import subprocess
import os
import re
import logging


@pytest.fixture(scope="session")
def monkeysession(request):
mp = MonkeyPatch()
yield mp
mp.undo()


@pytest.fixture(scope="module")
def patch(monkeysession):
if not tool_exists('xl'):
pytest.skip("xen is not found")

def install_patch():
return InstallInfo(
vcpus=1,
memory=512,
storage_backend='qcow2',
disk_size='200M',
iso_path=None, # not being required
zfs_tank_name=None,
lvm_volume_group=None,
enable_unattended=None,
iso_sha256=None
)
monkeysession.setattr(InstallInfo, "load", install_patch)
monkeysession.setattr(InstallInfo, "try_load", install_patch)

# being yielded so the the monkeypatch doesn't start cleanup if returned
yield monkeysession


@pytest.fixture(scope="module")
def test_vm(patch, config):
monkeysession = patch

@property
def vm_name(self):
return "test-hvm64-example"
monkeysession.setattr(VirtualMachine, "vm_name", vm_name)
manorit2001 marked this conversation as resolved.
Show resolved Hide resolved
test_vm = VirtualMachine(None, 0, "test-hvm64-example", config)

yield test_vm


@pytest.fixture(scope="module")
def config():
tmpf = tempfile.NamedTemporaryFile(delete=False).name
module_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')
cfg_path = os.path.join(module_dir, "tools", "test-hvm64-example.cfg")
firmware_path = os.path.join(module_dir, "tools", "test-hvm64-example")

with open(cfg_path, 'r') as f:
test_cfg = f.read().replace('{{ FIRMWARE_PATH }}', firmware_path).encode('utf-8')

with open(tmpf, 'wb') as f:
f.write(test_cfg)

yield tmpf
safe_delete(tmpf)
chivay marked this conversation as resolved.
Show resolved Hide resolved


@pytest.fixture(scope="module")
def snapshot_file():
tmpf = tempfile.NamedTemporaryFile(delete=False).name
yield tmpf
safe_delete(tmpf)
chivay marked this conversation as resolved.
Show resolved Hide resolved


def get_vm_state(vm_name: str) -> str:
out_lines = subprocess.check_output("xl list", shell=True).decode().split('\n')
# get the line with vm_name
out = next((line for line in out_lines if vm_name in line), None)
if out is None:
raise Exception(f"{vm_name} not found in xl list")
else:
state = re.sub(r' +', ' ', out).split(' ')[4].strip().strip('-')
return state


def destroy_vm(vm_name: str) -> str:
if subprocess.run(f"xl list {vm_name}", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0:
subprocess.run(f"xl destroy {vm_name}", shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
logging.info(f"Destroying {vm_name}")


@pytest.mark.incremental
class TestVM:
def test_vm_name(self, patch):
logging.info("testing VM names")
vm = VirtualMachine(None, 0)
assert vm.vm_name == 'vm-0'
logging.info("testing VM names with new fmt")
vm = VirtualMachine(None, 0, "test-vm-{}")
assert vm.vm_name == 'test-vm-0'

def test_vm_create_and_is_running(self, config, test_vm):

# initial cleanup
destroy_vm(test_vm.vm_name)
assert test_vm.is_running is False

logging.info("testing vm create with pause=False")
test_vm.create(pause=False)
assert get_vm_state(test_vm.vm_name) != 'p'
assert test_vm.is_running is True

logging.info("testing vm create for a created VM")
with pytest.raises(Exception):
test_vm.create(pause=True)

# second run
destroy_vm(test_vm.vm_name)

logging.info("testing vm create with pause=True")
test_vm.create(pause=True)
assert get_vm_state(test_vm.vm_name) == 'p'

# destroy the vm
destroy_vm(test_vm.vm_name)

logging.info("testing vm create with non-existant file")
with pytest.raises(Exception):
new_vm = VirtualMachine(None, 0, "test-hvm64-example", '/tmp/unexitant-file')
new_vm.create()

logging.info("testing vm create with empty file")
with tempfile.NamedTemporaryFile() as tempf:
with pytest.raises(Exception):
new_vm = VirtualMachine(None, 0, "test-hvm64-example", tempf.name)
new_vm.create()

# check if vm is shutdown
with pytest.raises(Exception):
get_vm_state(test_vm.name)

def test_vm_unpause(self, test_vm):
test_vm.create(pause=True)
assert get_vm_state(test_vm.vm_name) == 'p'

logging.info("testing vm unpause")
test_vm.unpause()
assert get_vm_state(test_vm.vm_name) != 'p'

# it shows stderr but rc is 0

# logging.info("testing vm unpause on an unpaused VM")
# with pytest.raises(Exception):
# test_vm.unpause()

# it is a short lived VM so we will create a new one whenever we unpause
destroy_vm(test_vm.vm_name)

def test_vm_save(self, test_vm, snapshot_file):
# test-hvm64-example VM can't be snapshotted in unpaused state
"""
root@debian:/home/user/drakvuf-sandbox/drakrun/drakrun/test# xl create /tmp/tmpjyoganif && xl save -c test-hvm64-example /tmp/test.sav
Parsing config from /tmp/tmpjyoganif
libxl: error: libxl_qmp.c:1334:qmp_ev_lock_aquired: Domain 122:Failed to connect to QMP socket /var/run/xen/qmp-libxl-122: No such file or directory
unable to retrieve domain configuration
"""

# test_vm.create(pause=True)
# test_vm.unpause()
# test_vm.save(snapshot_file, cont=True)
# assert get_vm_state(test_vm.vm_name) != 'p'

# reset

# destroy_vm(test_vm.vm_name)
test_vm.create(pause=True)
assert get_vm_state(test_vm.vm_name) == 'p'

logging.info("test vm save with pause=True")
test_vm.save(snapshot_file, pause=True)
assert get_vm_state(test_vm.vm_name) == 'p'

# should destroy the vm
logging.info("test vm save with with no pause/cont args")
test_vm.save(snapshot_file)
with pytest.raises(Exception):
get_vm_state(test_vm.name)

def test_vm_pause(self, test_vm):
# initialize the VM after previous destruction
test_vm.create()

assert get_vm_state(test_vm.vm_name) != 'p'

# test-hvm64-example goes to shutdown immediately, we get `--ps--` state during assertion

# logging.info("testing pause on VM")
# test_vm.pause()
# assert get_vm_state(test_vm.vm_name) == 'p'

# manual test shows, xl pause on a paused VM doesn't give any errors but pauses the VM again
# requiring the VM be unpaused twice for reaching running state

# logging.info("testing pause on a paused vm VM")
# with pytest.raises(Exception):
# test_vm.pause()

destroy_vm(test_vm.vm_name)

def test_vm_restore(self, config, snapshot_file, test_vm):
# if snapshot doesn't exist
logging.info("test vm restore without snapshot file")
with remove_files([snapshot_file]):
with pytest.raises(Exception):
test_vm.restore(snapshot_path=snapshot_file)
assert test_vm.is_running is False

# if configuration file doesn't exist
logging.info("test vm restore without config")
with remove_files([config]):
with pytest.raises(Exception):
test_vm.restore(snapshot_path=snapshot_file)
assert test_vm.is_running is False

# although test-hvm64-example doesn't depend on storage backend
# some test like this would have been good where storage backend doesn't exist
# and it is trying to restore from vm-1 or vm-0
# vm-0 should fail but vm-1 should succeed
# if backend.exists_vm(0) is False:
# with pytest.raises(Exception):
# test_vm.restore()
# assert test_vm.is_running is False

# should not raise any exceptions if everything is fine
logging.info("test vm with proper args")
test_vm.restore(snapshot_path=snapshot_file)
assert get_vm_state(test_vm.vm_name) != 'p'

destroy_vm(test_vm.vm_name)

logging.info("test vm with proper args and pause=True")
test_vm.restore(snapshot_path=snapshot_file, pause=True)
assert get_vm_state(test_vm.vm_name) == 'p'

# restoring a restored VM
# what should be the expected behavior?
# test_vm.restore(cfg_path= config, snapshot_path = snapshot_file)
manorit2001 marked this conversation as resolved.
Show resolved Hide resolved

destroy_vm(test_vm.vm_name)

def test_vm_destroy(self, test_vm):
test_vm.create(pause=True)

logging.info("test vm destroy")
test_vm.destroy()
with pytest.raises(Exception):
get_vm_state(test_vm.name)

# should 2nd time destory raise/log exceptions and then handle it?
# test_vm.destroy()
manorit2001 marked this conversation as resolved.
Show resolved Hide resolved
# assert test_vm.is_running is False
Loading