Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f00ad13
feat(coalesce): Add tests for coalesce
Nambrok Mar 31, 2025
b27d85b
feat(coalesce): add Timeout for coalesce
Nambrok May 20, 2025
9cbacb4
fix(Host): use pre-parsed inventory for dom0
Nambrok May 20, 2025
508a945
feat(image_format): support image_format
Nambrok Jun 4, 2025
44a31a8
Add shared_sr + log type of SR
Nambrok Jun 4, 2025
b91bb64
coalesce: Fix vdi_on_local_sr fixture
Nambrok Jun 4, 2025
48f7508
Re-order test in class
Nambrok Jun 6, 2025
e498f4e
lib/basevm.py: add vdis related function
Nambrok Jul 2, 2025
453f339
feat(coalesce): modify coalesce fixture to use VM function
Nambrok Jul 2, 2025
bd8e7c8
coalesce: move function in a utils.py file
Nambrok Jul 2, 2025
b96297e
pool.py: remove unused import
Nambrok Jul 2, 2025
c661605
tests: Updated conftest to handle `image-format` as list of str (e.g.…
rushikeshjadhav Jul 7, 2025
5510d9a
tests/storage/ext: Updated ext to handle both vhd and qcow2 vdi image…
rushikeshjadhav Jul 7, 2025
77444ee
lib/host: Avoid using identical source and destination paths in execu…
rushikeshjadhav Jun 16, 2025
5ec3d5e
tests/storage/lvm: Updated lvm to handle both vhd and qcow2 vdi image…
rushikeshjadhav Jul 7, 2025
28438a8
tests/storage/xfs: Updated xfs to handle both vhd and qcow2 vdi image…
rushikeshjadhav Jul 7, 2025
8e95898
tests/storage/zfs: Updated zfs to handle both vhd and qcow2 vdi image…
rushikeshjadhav Jul 7, 2025
cfe0199
tests/storage/zfsvol: Updated zfsvol to handle both vhd and qcow2 vdi…
rushikeshjadhav Jul 8, 2025
c421cc4
tests/storage/largeblock: Updated largeblock to handle both vhd and q…
rushikeshjadhav Jul 9, 2025
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
28 changes: 25 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,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",
action="append",
default=[],
help="Format of VDI to execute tests on."
"Example: vhd,qcow2"
)

def pytest_configure(config):
global_config.ignore_ssh_banner = config.getoption('--ignore-ssh-banner')
Expand All @@ -98,6 +105,14 @@ 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) == 1:
image_format = image_format[0].split(",")
if not image_format:
image_format = ["vhd"]
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 @@ -296,14 +311,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 @@ -313,7 +335,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 @@ -323,7 +345,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
37 changes: 35 additions & 2 deletions lib/basevm.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging

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

import lib.commands as commands
if TYPE_CHECKING:
import lib.host

from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set
from lib.sr import SR
from lib.vdi import VDI

class BaseVM:
""" Base class for VM and Snapshot. """
Expand All @@ -19,6 +20,15 @@ def __init__(self, uuid: str, host: 'lib.host.Host'):
logging.info("New %s: %s", type(self).__name__, uuid)
self.uuid = uuid
self.host = host
try:
self.vdis = [VDI(vdi_uuid, host=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

@overload
def param_get(self, param_name: str, key: Optional[str] = ...,
Expand Down Expand Up @@ -56,11 +66,31 @@ def name(self) -> str:
assert isinstance(n, str)
return n

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,
})
self.host.xe("vbd-plug", {"uuid": vbd_uuid})

self.vdis.append(vdi)

return vbd_uuid

def disconnect_vdi(self, vdi: VDI):
logging.info(f"<< Unplugging VDI {vdi.uuid} from VM {self.uuid}")
vbd_uuid = self.host.xe("vbd-list", {"vdi-uuid": vdi.uuid, "vm-uuid": self.uuid}, minimal=True)
self.host.xe("vbd-unplug", {"uuid": vbd_uuid})
self.host.xe("vbd-destroy", {"uuid": vbd_uuid})

# @abstractmethod
def _disk_list(self):
raise NotImplementedError()

def vdi_uuids(self, sr_uuid=None):
def vdi_uuids(self, sr_uuid=None) -> List[str]:
output = self._disk_list()
if output == '':
return []
Expand All @@ -78,6 +108,9 @@ def vdi_uuids(self, sr_uuid=None):

def destroy_vdi(self, vdi_uuid: str) -> None:
self.host.xe('vdi-destroy', {'uuid': vdi_uuid})
for vdi in self.vdis:
if vdi.uuid == vdi_uuid:
self.vdis.remove(vdi)

def all_vdis_on_host(self, host):
for vdi_uuid in self.vdi_uuids():
Expand Down
25 changes: 22 additions & 3 deletions lib/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import subprocess
import tempfile
import uuid
from typing import Optional

from packaging import version
from typing import Dict, List, Literal, Optional, overload, TYPE_CHECKING, Union
Expand Down Expand Up @@ -55,6 +56,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 @@ -201,13 +203,13 @@ def execute_script(self, script_contents, shebang='sh', simple_output=True):
script.write('#!/usr/bin/env ' + shebang + '\n')
script.write(script_contents)
script.flush()
self.scp(script.name, script.name)
self.scp(script.name, "/tmp/" + os.path.basename(script.name))

try:
logging.debug(f"[{self}] # Will execute this temporary script:\n{script_contents.strip()}")
return self.ssh([script.name], simple_output=simple_output)
return self.ssh(["/tmp/" + os.path.basename(script.name)], simple_output=simple_output)
finally:
self.ssh(['rm', '-f', script.name])
self.ssh(['rm', '-f', "/tmp/" + os.path.basename(script.name)])

def _get_xensource_inventory(self) -> Dict[str, str]:
output = self.ssh(['cat', '/etc/xensource-inventory'])
Expand Down Expand Up @@ -699,3 +701,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) -> Optional[SR]:
sr_uuid = self.xe("vdi-param-get",
{"param-name": "sr-uuid",
"uuid": vdi_uuid,
})
if sr_uuid is None:
return None
return SR(sr_uuid, self.pool)
2 changes: 1 addition & 1 deletion lib/pool.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
import os
import traceback
from typing import Any, Dict, Optional, cast
from typing import Any, Dict, Optional

from packaging import version

Expand Down
18 changes: 14 additions & 4 deletions lib/sr.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import time
from typing import Optional

import lib.commands as commands

Expand All @@ -12,6 +13,7 @@ def __init__(self, uuid, pool):
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 @@ -153,13 +155,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:
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
13 changes: 8 additions & 5 deletions lib/vdi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Optional

from lib.common import _param_add, _param_clear, _param_get, _param_remove, _param_set, strtobool
from typing import Literal, Optional, overload, TYPE_CHECKING
Expand All @@ -23,11 +24,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 @@ -48,6 +45,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
Empty file.
44 changes: 44 additions & 0 deletions tests/storage/coalesce/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import pytest
import logging

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()
vbd_uuid = dom0.connect_vdi(vdi_on_local_sr)

yield vdi_on_local_sr

dom0.disconnect_vdi(vdi_on_local_sr)

@pytest.fixture(scope="class")
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):
sr_uuid = local_sr_on_hostA1.uuid
vdi_uuid = vdi_with_vbd_on_dom0.uuid
yield f"/dev/sm/backend/{sr_uuid}/{vdi_uuid}"
50 changes: 50 additions & 0 deletions tests/storage/coalesce/test_coalesce.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import logging

from .utils import wait_for_vdi_coalesce, copy_data_to_tapdev, snapshot_vdi, compare_data

class Test:
def test_write_data(self, host, tapdev, data_file_on_host):
length = 1 * 1024 * 1024
offset = 0

logging.info("Copying data to tapdev")
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)

assert compare_data(host, tapdev, data_file_on_host, offset, length)

def test_coalesce(self, host, tapdev, vdi_with_vbd_on_dom0, data_file_on_host):
vdi = vdi_with_vbd_on_dom0
vdi_uuid = vdi.uuid
length = 1 * 1024 * 1024
offset = 0

vdi_snap = snapshot_vdi(host, vdi_uuid)

logging.info("Copying data to tapdev")
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)

logging.info("Removing VDI snapshot")
host.xe("vdi-destroy", {"uuid": vdi_snap})

wait_for_vdi_coalesce(vdi)

assert compare_data(host, tapdev, data_file_on_host, offset, length)

def test_clone_coalesce(self, host, tapdev, vdi_with_vbd_on_dom0, data_file_on_host):
vdi = vdi_with_vbd_on_dom0
vdi_uuid = vdi.uuid
length = 1 * 1024 * 1024
offset = 0

clone_uuid = host.xe("vdi-clone", {"uuid": vdi_uuid})
logging.info(f"Clone VDI {vdi_uuid}: {clone_uuid}")

logging.info("Copying data to tapdev")
copy_data_to_tapdev(host, data_file_on_host, tapdev, offset, length)

logging.info("Removing VDI clone")
host.xe("vdi-destroy", {"uuid": clone_uuid})

wait_for_vdi_coalesce(vdi)

assert compare_data(host, tapdev, data_file_on_host, offset, length)
Loading