Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 23 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ def pytest_addoption(parser):
"4KiB blocksize to be formatted and used in storage tests. "
"Set it to 'auto' to let the fixtures auto-detect available disks."
)
parser.addoption(
"--image-format",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you work in the context of storage everyday, maybe it's a clear name, but otherwise it might be too generic a name. Both image and format can be many things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's why I added vdi-image-format as suggestion.
But I wonder if we should not use the naming volume-image-format instead.

No preference.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And vdi-type looked consistent with sm's terminology 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# xe vdi-list params=type | sort | uniq
type ( RO)    : CBT metadata
type ( RO)    : HA statefile
type ( RO)    : Redo log
type ( RO)    : User

Not really consistent. In this example here, type is completly different.

  • On the XAPI side, we have a vdi_type stored in the sm-config attribute. Here you can have the VHD/AIO value. However there are weird situations like: sm-config (MRO) : type: raw; vdi_type: aio.
  • Regarding the new QCOW2 format we discussed internally to use a new attribute image-format. type is completely ambiguous depending on what you are talking about and the context in which it is used.
  • In the SMAPIv3, vdi type is completly removed (same for VDI, we use volume instead). And image-format is used.

action="append",
default=[],
help="Format of VDI to execute tests on."
"Example: vhd,qcow2"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this a CLI parameter means you're deporting the test job definition outside pytest. When rishi wanted to do the same for thin vs thick in the context of XOSTOR tests, we asked him to rework the tests so they actually test both formats.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to allow to test only one type by hand, the objectives for tests is to keep the default which at the moment is vhd but will be ["vhd", "qcow2"] eventually.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pytest already has test selection mechanisms:

  • path to test files
  • -k
  • -m

Copy link
Member

@stormi stormi Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as the parameter is not necessary in jobs.py, fine by me. We won't have SR types that only support VHD or only support QCOW2 at some point?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter default to VHD when not given meaning it's not needed to change jobs.py. But I will need to add the new coalesce tests in jobs.py either way and it's the only test at the moment that will use vdi-type
I'm not sure to see how the other selection mechanisms from pytest could work in this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point to enable "qcow2", this will be required to make modifications in jobs.py

@Nambrok while doing similar for thin vs thick, we did

@pytest.fixture(params=["thin"], scope="session")
def provisioning_type(request):
    return request.param

So here in this case, if we do -

@pytest.fixture(params=["vhd"], scope="session")
def vdi_image_format(request):
    return request.param

and

@pytest.fixture(scope='package')
def ext_sr(host, sr_disk, vdi_image_format):
   """ An EXT SR on first host. """
   sr = host.sr_create('ext', "EXT-local-SR-test", {
       'device': '/dev/' + sr_disk,
       'preferred-image-formats': vdi_image_format
   })
   yield sr
   # teardown
   sr.destroy()

the tests can be upgraded for new types in future for supported SR.

As the whole SR tests needs to be tested for both vhd & qcow2, it makes it suitable per SR tests package.

I'm fine with both approach, to get this added.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just change the default parameter value in my case, but it also allows to run only one type of image_format when started manually.
Your method always run both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/xcp-ng/xcp-ng-tests/pull/290/files#diff-a31c7ed5d35f5ed8233994868c54d625b18e6bacb6794344c4531e62bd9dde59R111
We can just add qcow2 on this line to add qcow2 to also run by default without modifying jobs.py.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works, I think we can move ahead with this merge.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your method always run both.

Well, there is still -k and -m for filtering as Sam mentioned. We may also want to avoid doing similar things different ways.

)

def pytest_configure(config):
global_config.ignore_ssh_banner = config.getoption('--ignore-ssh-banner')
Expand All @@ -106,6 +113,12 @@ def pytest_generate_tests(metafunc):
vms = [None] # no --vm parameter does not mean skip the test, for us, it means use the default
metafunc.parametrize("vm_ref", vms, indirect=True, scope="module")

if "image_format" in metafunc.fixturenames:
image_format = metafunc.config.getoption("image_format")
if len(image_format) == 0:
image_format = ["vhd"] # Not giving image-format will default to doing tests on vhd
Comment on lines +118 to +119
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"vhd,qcow2" are considered as one single option by pytest when passing like --image-format=vhd,qcow2 thus need to convert it into list

image_format = image_format[0].split(",") if image_format else ["vhd"]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but it's because different input are made into a list so --image-format=vhd --image-format=qcow2 => ["vhd", "qcow2"].
Some fixture are doing both but I didn't explicitely, we could do both.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's cleaner to just use --image-format=vhd --image-format=qcow2 like this, but then example given in --help needs fixing, as it does suggest the other way.

metafunc.parametrize("image_format", image_format, scope="session")

def pytest_collection_modifyitems(items, config):
# Automatically mark tests based on fixtures they require.
# Check pytest.ini or pytest --markers for marker descriptions.
Expand Down Expand Up @@ -304,14 +317,21 @@ def host_no_ipv6(host):
if is_ipv6(host.hostname_or_ip):
pytest.skip(f"This test requires an IPv4 XCP-ng")

@pytest.fixture(scope="session")
def shared_sr(host):
sr = host.pool.first_shared_sr()
assert sr, "No shared SR available on hosts"
logging.info(">> Shared SR on host present: {} of type {}".format(sr.uuid, sr.get_type()))
yield sr

@pytest.fixture(scope='session')
def local_sr_on_hostA1(hostA1):
""" A local SR on the pool's master. """
srs = hostA1.local_vm_srs()
assert len(srs) > 0, "a local SR is required on the pool's master"
# use the first local SR found
sr = srs[0]
logging.info(">> local SR on hostA1 present : %s" % sr.uuid)
logging.info(">> local SR on hostA1 present: {} of type {}".format(sr.uuid, sr.get_type()))
yield sr

@pytest.fixture(scope='session')
Expand All @@ -321,7 +341,7 @@ def local_sr_on_hostA2(hostA2):
assert len(srs) > 0, "a local SR is required on the pool's second host"
# use the first local SR found
sr = srs[0]
logging.info(">> local SR on hostA2 present : %s" % sr.uuid)
logging.info(">> local SR on hostA2 present: {} of type {}".format(sr.uuid, sr.get_type()))
yield sr

@pytest.fixture(scope='session')
Expand All @@ -331,7 +351,7 @@ def local_sr_on_hostB1(hostB1):
assert len(srs) > 0, "a local SR is required on the second pool's master"
# use the first local SR found
sr = srs[0]
logging.info(">> local SR on hostB1 present : %s" % sr.uuid)
logging.info(">> local SR on hostB1 present: {} of type {}".format(sr.uuid, sr.get_type()))
yield sr

@pytest.fixture(scope='session')
Expand Down
4 changes: 2 additions & 2 deletions lib/basevm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging

from typing import TYPE_CHECKING, Any, Literal, Optional, overload
from typing import TYPE_CHECKING, Any, List, Literal, Optional, overload

if TYPE_CHECKING:
import lib.host
Expand Down Expand Up @@ -59,7 +59,7 @@ def name(self) -> str:
def _disk_list(self):
raise NotImplementedError()

def vdi_uuids(self, sr_uuid=None):
def vdi_uuids(self, sr_uuid: Optional[str] = None) -> List[str]:
output = self._disk_list()
if output == '':
return []
Expand Down
18 changes: 18 additions & 0 deletions lib/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def __init__(self, pool: 'lib.pool.Pool', hostname_or_ip):
self.uuid = self.inventory['INSTALLATION_UUID']
self.xcp_version = version.parse(self.inventory['PRODUCT_VERSION'])
self.xcp_version_short = f"{self.xcp_version.major}.{self.xcp_version.minor}"
self._dom0: Optional[VM] = None

def __str__(self):
return self.hostname_or_ip
Expand Down Expand Up @@ -711,3 +712,20 @@ def enable_hsts_header(self):
def disable_hsts_header(self):
self.ssh(['rm', '-f', f'{XAPI_CONF_DIR}/00-XCP-ng-tests-enable-hsts-header.conf'])
self.restart_toolstack(verify=True)

def get_dom0_uuid(self):
return self.inventory["CONTROL_DOMAIN_UUID"]

def get_dom0_vm(self) -> VM:
if not self._dom0:
self._dom0 = VM(self.get_dom0_uuid(), self)
return self._dom0

def get_sr_from_vdi_uuid(self, vdi_uuid: str) -> Optional[SR]:
sr_uuid = self.xe("vdi-param-get", {
"param-name": "sr-uuid",
"uuid": vdi_uuid,
})
if not sr_uuid:
return None
return SR(sr_uuid, self.pool)
12 changes: 12 additions & 0 deletions lib/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,18 @@

from lib.basevm import BaseVM

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from lib.host import Host
from lib.vm import VM


class Snapshot(BaseVM):
def __init__(self, uuid: str, host: "Host", vm: "VM"):
self.basevm = vm
super(Snapshot, self).__init__(uuid, host)

def _disk_list(self):
return self.host.xe('snapshot-disk-list', {'uuid': self.uuid, 'vbd-params': ''},
minimal=True)
Expand All @@ -21,3 +32,4 @@ def exists(self):
def revert(self):
logging.info("Revert to snapshot %s", self.uuid)
self.host.xe('snapshot-revert', {'uuid': self.uuid})
self.basevm.create_vdis_list() # We reset the base VM object VDIs list because it changed following the revert
27 changes: 22 additions & 5 deletions lib/sr.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@
import time

import lib.commands as commands
from lib.common import prefix_object_name, safe_split, strtobool, wait_for, wait_for_not
from lib.common import (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why this is reformatted

prefix_object_name,
safe_split,
strtobool,
wait_for,
wait_for_not,
)
from lib.vdi import VDI

from typing import Optional

class SR:
def __init__(self, uuid, pool):
self.uuid = uuid
self.pool = pool
self._is_shared = None # cached value for is_shared()
self._main_host = None # cached value for main_host()
self._type = None # cache value for get_type()

def pbd_uuids(self):
return safe_split(self.pool.master.xe('pbd-list', {'sr-uuid': self.uuid}, minimal=True))
Expand Down Expand Up @@ -152,13 +161,21 @@ def is_shared(self):
{'uuid': self.uuid, 'param-name': 'shared'}))
return self._is_shared

def create_vdi(self, name_label, virtual_size=64):
def get_type(self) -> str:
if self._type is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use functools.lru_cache decorator to simplify the caching (more a note for the future).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure lru_cache is relevant here, as it's an LRU cache that's used and updated based on the parameters given to the function: here, the cache will contain a maximum of one value, as we only pass self as an argument. It's a little overkill. 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is @functools.cache in this case :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO I think having own decorator to handle 0 arg (const_cache?) or continuing with an explicit private variable is better. @functools.cache uses a dict in its implementation to deal with arguments. (I think I don't like scripted languages with a tendency to consume more resources where we wouldn't do that in a binary :p )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorator is only applied at (byte-)compile time, so a clever python could get rid of the dict and incur zero runtime cost. No clue if it does 😉

self._type = self.pool.master.xe("sr-param-get", {"uuid": self.uuid, "param-name": "type"})
return self._type

def create_vdi(self, name_label: str, virtual_size: int = 64, image_format: Optional[str] = None) -> VDI:
logging.info("Create VDI %r on SR %s", name_label, self.uuid)
vdi_uuid = self.pool.master.xe('vdi-create', {
args = {
'name-label': prefix_object_name(name_label),
'virtual-size': str(virtual_size),
'sr-uuid': self.uuid
})
'sr-uuid': self.uuid,
}
if image_format:
args["sm-config:image-format"] = image_format
vdi_uuid = self.pool.master.xe('vdi-create', args)
return VDI(vdi_uuid, sr=self)

def run_quicktest(self):
Expand Down
12 changes: 7 additions & 5 deletions lib/vdi.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,7 @@ def __init__(self, uuid, *, host=None, sr=None):
# TODO: use a different approach when migration is possible
if sr is None:
assert host
sr_uuid = host.pool.get_vdi_sr_uuid(uuid)
# avoid circular import
# FIXME should get it from Host instead
from lib.sr import SR
self.sr = SR(sr_uuid, host.pool)
self.sr = host.get_sr_from_vdi_uuid(self.uuid)
else:
self.sr = sr

Expand All @@ -50,6 +46,12 @@ def readonly(self) -> bool:
def __str__(self):
return f"VDI {self.uuid} on SR {self.sr.uuid}"

def get_parent(self) -> Optional[str]:
return self.param_get("sm-config", key="vhd-parent", accept_unknown_key=True)

def get_image_format(self) -> Optional[str]:
return self.param_get("sm-config", key="image-format", accept_unknown_key=True)

@overload
def param_get(self, param_name: str, key: Optional[str] = ...,
accept_unknown_key: Literal[False] = ...) -> str:
Expand Down
61 changes: 59 additions & 2 deletions lib/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
)
from lib.snapshot import Snapshot
from lib.vbd import VBD
from lib.vdi import VDI
from lib.vif import VIF

from typing import TYPE_CHECKING, List, Literal, Optional, Union, overload
Expand All @@ -33,6 +34,7 @@ def __init__(self, uuid: str, host: 'Host'):
self.previous_host = None # previous host when migrated or being migrated
self.is_windows = self.param_get('platform', 'device_id', accept_unknown_key=True) == '0002'
self.is_uefi = self.param_get('HVM-boot-params', 'firmware', accept_unknown_key=True) == 'uefi'
self.create_vdis_list()

def power_state(self) -> str:
return self.param_get('power-state')
Expand Down Expand Up @@ -273,19 +275,74 @@ def migrate(self, target_host, sr=None, network=None):

self.previous_host = self.host
self.host = target_host
self.create_vdis_list()

def snapshot(self, ignore_vdis=None):
logging.info("Snapshot VM")
args = {'uuid': self.uuid, 'new-name-label': 'Snapshot of %s' % self.uuid}
if ignore_vdis:
args['ignore-vdi-uuids'] = ','.join(ignore_vdis)
return Snapshot(self.host.xe('vm-snapshot', args), self.host)
snap_uuid = self.host.xe('vm-snapshot', args)
return Snapshot(snap_uuid, self.host, self)

def checkpoint(self) -> Snapshot:
logging.info("Checkpoint VM")
return Snapshot(self.host.xe('vm-checkpoint', {'uuid': self.uuid,
'new-name-label': 'Checkpoint of %s' % self.uuid}),
self.host)
self.host, self)

def connect_vdi(self, vdi: VDI, device: str = "autodetect") -> str:
logging.info(f">> Plugging VDI {vdi.uuid} on VM {self.uuid}")
vbd_uuid = self.host.xe("vbd-create", {
"vdi-uuid": vdi.uuid,
"vm-uuid": self.uuid,
"device": device,
})
try:
self.host.xe("vbd-plug", {"uuid": vbd_uuid})
except commands.SSHCommandFailed:
self.host.xe("vbd-destroy", {"uuid": vbd_uuid})
raise

self.vdis.append(vdi)

return vbd_uuid

def disconnect_vdi(self, vdi: VDI):
logging.info(f"<< Unplugging VDI {vdi.uuid} from VM {self.uuid}")
assert vdi in self.vdis, "VDI {vdi.uuid} not in VM {self.uuid} VDI list"
vbd_uuid = self.host.xe("vbd-list", {
"vdi-uuid": vdi.uuid,
"vm-uuid": self.uuid
}, minimal=True)
try:
self.host.xe("vbd-unplug", {"uuid": vbd_uuid})
except commands.SSHCommandFailed as e:
if e.stdout == f"The device is not currently attached\ndevice: {vbd_uuid}":
logging.info(f"VBD {vbd_uuid} already unplugged")
else:
raise
self.host.xe("vbd-destroy", {"uuid": vbd_uuid})
self.vdis.remove(vdi)

def destroy_vdi(self, vdi_uuid: str) -> None:
for vdi in self.vdis:
if vdi.uuid == vdi_uuid:
self.vdis.remove(vdi)
super().destroy_vdi(vdi_uuid)
break

def create_vdis_list(self) -> None:
""" Used to redo the VDIs list of the VM when reverting a snapshot. """
try:
self.vdis = [VDI(vdi_uuid, host=self.host) for vdi_uuid in self.vdi_uuids()]
except commands.SSHCommandFailed as e:
# Doesn't work with Dom0 since `vm-disk-list` doesn't work on it so we create empty list
if e.stdout == "Error: No matching VMs found":
logging.info("Couldn't get disks list. We are Dom0. Continuing...")
self.vdis = []
else:
raise

def vifs(self):
_vifs = []
Expand Down
Empty file.
53 changes: 53 additions & 0 deletions tests/storage/coalesce/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import pytest

import logging

from lib.vdi import VDI

MAX_LENGTH = 1 * 1024 * 1024 * 1024 # 1GiB

@pytest.fixture(scope="module")
def vdi_on_local_sr(host, local_sr_on_hostA1, image_format):
sr = local_sr_on_hostA1
vdi = sr.create_vdi("testVDI", MAX_LENGTH, image_format=image_format)
logging.info(">> Created VDI {} of type {}".format(vdi.uuid, image_format))

yield vdi

logging.info("<< Destroying VDI {}".format(vdi.uuid))
vdi.destroy()

@pytest.fixture(scope="module")
def vdi_with_vbd_on_dom0(host, vdi_on_local_sr):
dom0 = host.get_dom0_vm()
dom0.connect_vdi(vdi_on_local_sr)

yield vdi_on_local_sr

dom0.disconnect_vdi(vdi_on_local_sr)

@pytest.fixture(scope="function")
def data_file_on_host(host):
filename = "/root/data.bin"
logging.info(f">> Creating data file {filename} on host")
size = 1 * 1024 * 1024 # 1MiB
assert size <= MAX_LENGTH, "Size of the data file bigger than the VDI size"

host.ssh(["dd", "if=/dev/urandom", f"of={filename}", f"bs={size}", "count=1"])

yield filename

logging.info("<< Deleting data file")
host.ssh(["rm", filename])

@pytest.fixture(scope="module")
def tapdev(local_sr_on_hostA1, vdi_with_vbd_on_dom0):
"""
A tapdev is a blockdevice allowing access to a VDI from the Dom0.

It is usually used to give access to the VDI to Qemu for emulating devices
before PV driver are loaded in the guest.
"""
Comment on lines +43 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That does not really explain how it is useful for testing.

Maybe naming it like tapdev_on_vdi_on_local_sr would be sufficient.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be mentioned somewhere that tapdev is used for a better test performance, to avoid instantiating a new VM to access the vdi

sr_uuid = local_sr_on_hostA1.uuid
vdi_uuid = vdi_with_vbd_on_dom0.uuid
yield f"/dev/sm/backend/{sr_uuid}/{vdi_uuid}"
Loading