Skip to content
Draft
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
15 changes: 14 additions & 1 deletion jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,19 @@
"nb_pools": 2,
"params": {},
"paths": ["tests/misc/test_pool.py"],
},
"limit-tests": {
"description": "Tests verifying we can hit our supported limits",
"requirements": [
"1 XCP-ng host >= 8.2"
],
"nb_pools": 1,
"params": {
# The test does not work on Alpine because of how it handles
# multiple interfaces on the same network, so use Debian instead
"--vm": "single/debian_uefi_vm",
},
"paths": ["tests/limits/test_vif_limit.py"],
}
}

Expand Down Expand Up @@ -653,7 +666,7 @@ def extract_tests(cmd):

print("*** Checking that all tests that use VMs have VM target markers (small_vm, etc.)... ", end="")
tests_missing_vm_markers = extract_tests(
["pytest", "--collect-only", "-q", "-m", "not no_vm and not (small_vm or multi_vm or big_vm)"]
["pytest", "--collect-only", "-q", "-m", "not no_vm and not (small_vm or multi_vm or big_vm or debian_uefi_vm)"]
)
if tests_missing_vm_markers:
error = True
Expand Down
9 changes: 5 additions & 4 deletions lib/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,11 @@ def create_vif(self, vif_num, *, network_uuid=None, network_name=None):
network_uuid = self.host.pool.network_named(network_name)
assert network_uuid, f"No UUID given, and network name {network_name!r} not found"
logging.info("Create VIF %d to network %r on VM %s", vif_num, network_uuid, self.uuid)
self.host.xe('vif-create', {'vm-uuid': self.uuid,
'device': str(vif_num),
'network-uuid': network_uuid,
})
vif_uuid = self.host.xe('vif-create', {'vm-uuid': self.uuid,
'device': str(vif_num),
'network-uuid': network_uuid,
})
return VIF(vif_uuid, self)

def is_running_on_host(self, host):
return self.is_running() and self.param_get('resident-on') == host.uuid
Expand Down
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ markers =
small_vm: tests that it is enough to run just once, using the smallest possible VM.
big_vm: tests that it would be good to run with a big VM.
multi_vms: tests that it would be good to run on a variety of VMs (includes `small_vm` but excludes `big_vm`).
debian_uefi_vm: tests that require a Debian UEFI VM

# * Other markers
reboot: tests that reboot one or more hosts.
Expand Down
Empty file added tests/limits/__init__.py
Empty file.
93 changes: 93 additions & 0 deletions tests/limits/test_vif_limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from pkgfixtures import host_with_saved_yum_state
import ipaddress
import logging
import os
import pytest
import tempfile

# Requirements:
# - one XCP-ng host (--host) >= 8.2
# - a VM (--vm)
# - the first network on the host can be used to reach the host

vif_limit = 16
interface_name = "enX"
vcpus = '8'

# There is a ResourceWarning due to background=True on an ssh call
# We do ensure the processes are killed
@pytest.mark.filterwarnings("ignore::ResourceWarning")
@pytest.mark.debian_uefi_vm
Copy link
Contributor

Choose a reason for hiding this comment

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

This would need the mark to be declared in this commit already.

@stormi I'm not yet 100% uptospeed with how VM selection work, but that pattern reminds me something - is it even going to work as expected?

Copy link
Author

Choose a reason for hiding this comment

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

sure, i'll reorder the commits

Copy link
Member

Choose a reason for hiding this comment

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

So, regarding VM selection, that's the first time we'd have a marker that asks for a specific distro. But that's not the first time that we have a test that wants a Unix VM with requirements not met by alpine (see vtpm tests).

IMO:

  • any requirements on the VM should be expressed in a fixture, that checks that the VM provided to the test is suitable. A marker doesn't do this. We could have a debian_vm fixture if we really need a debian VM, but we avoid writing tests for a given distro as much as possible. I'd rather have a more generic fixture, like "unix_vm_with_whatever_criteria_let_us_know_that_the_test_should_pass_with_it". That's something that the vtpm test does poorly, by the way (checks are in the test there, not in a fixture where they would belong).
  • If necessary for filtering tests in jobs, we can automatically generate markers based on fixture requirements. Check the unix_vm or windows_vm markers, for example. Here we could have a unix_vm_with_whatever_criteria_let_us_know_that_the_test_should_pass_with_it.
  • There does not need to be an exact match between VM identifiers defined in jobs.py (such as the existing debian_uefi_vm). We first express the requirements in the test, and then see how we can use the test in jobs.py. Here, making the job use debian_uefi_vm seems appropriate, but that's not the only solution we could choose. We could also want to run the test with a collection of OSes (various unixes, windows...) so that the test not only checks XAPI's and Xen's handling of the VIF limit, but also the OSes and PV drivers. Maybe @dinhngtu will have an opinion on this.

Another related topic: should the fixture checking the requirements make the test fail, or skip?

In most cases, we'll want the test to fail. It indicates that we called the test with a VM that is not appropriate. However, if we want to include the test in a "multi" job, one that runs on many different VMs, and if the test can run with most unix VMs (for one version of the test) and windows VMs (for another version of the test) then skipping just for the few VMs that are not suitable (alpine...) could be a way. There's also the possibility to reuse existing VM collections that already exclude alpine in jobs.py/vm_data.py, such as the ones we use for testing guest tools (currently named tools_unix and tools_windows, but if we can define a common set of selection criteria, the name could change.

It's all a question of compromise between being specific and limiting the amount of jobs and different VM collections in CI.

(Another side note: in the future I'd like skipped tests to be considered as failures in Jenkins, unless they belong to a list of expected skips).

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 a bit lost on the PR's context. Is the per-image limitation coming from the guest kernel or something else? If it's something not statically-defined by the image itself, I think runtime detection + pytest.skip would be more appropriate.

Copy link
Member

Choose a reason for hiding this comment

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

The question for you was mostly whether you'd find useful to test Windows VMs with up to 16 VIFs.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it'd be useful.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why specifically UEFI?

Copy link
Author

Choose a reason for hiding this comment

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

the only thing this really needs to express is "non-alpine", preferably debian because some other distros might have different naming schemes for interfaces. uefi is not necessary

class TestVIFLimit:
def test_vif_limit(self, host_with_saved_yum_state, imported_vm):
host = host_with_saved_yum_state
vm = imported_vm
if (vm.is_running()):
logging.info("VM already running, shutting it down first")
vm.shutdown(verify=True)

network_uuid = vm.vifs()[0].param_get('network-uuid')
existing_vifs = len(vm.vifs())

logging.info(f'Get {vcpus} vCPUs for the VM')
vm.param_set('VCPUs-max', vcpus)
vm.param_set('VCPUs-at-startup', vcpus)
Comment on lines +33 to +34
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a permanent change done to the VM, we likely don't want that when it is not a throwaway VM. I'd think this is a good candidate for a fixture.

Copy link
Author

Choose a reason for hiding this comment

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

but the vm is destroyed at the end


logging.info('Create VIFs before starting the VM')
for i in range(existing_vifs, vif_limit):
vm.create_vif(i, network_uuid=network_uuid)
Comment on lines +36 to +38
Copy link
Contributor

Choose a reason for hiding this comment

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

similarly those should be disposed in cleanup phase

Copy link
Author

Choose a reason for hiding this comment

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

when the vm is destroyed, these are cleaned up as well.

Copy link
Member

@stormi stormi Jul 18, 2025

Choose a reason for hiding this comment

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

Though we currently give module scope to VM fixtures (so they are indeed destroyed between tests of different modules), we strive to always leave resources in the state where we found them.

There are exceptions:

  • tests and fixtures depending on higher level fixtures which create throw-away VM clones for the tests.
  • tests and fixtures depending on higher level fixtures which creates snapshots and revert to them after the tests.


vm.start()
vm.wait_for_os_booted()

logging.info('Verify the interfaces exist in the guest')
for i in range(0, vif_limit):
if vm.ssh_with_result([f'test -d /sys/class/net/{interface_name}{i}']).returncode != 0:
guest_error = vm.ssh_with_result(['dmesg | grep -B1 -A3 xen_netfront']).stdout
logging.error("dmesg:\n%s", guest_error)
assert False, "The interface does not exist in the guest, check dmesg output above for errors"

logging.info('Configure interfaces')
config = '\n'.join([f'iface {interface_name}{i} inet dhcp\n'
f'auto {interface_name}{i}'
for i in range(existing_vifs, vif_limit)])
vm.ssh([f'echo "{config}" >> /etc/network/interfaces'])

logging.info('Install iperf3 on VM and host')
if vm.ssh_with_result(['apt install iperf3 --assume-yes']).returncode != 0:
assert False, "Failed to install iperf3 on the VM"
host.yum_install(['iperf3'])

logging.info('Reconfigure VM networking')
if vm.ssh_with_result(['systemctl restart networking']).returncode != 0:
assert False, "Failed to configure networking"

# Test iperf on all interfaces in parallel
# Clean up on exceptions
try:
logging.info('Create separate iperf servers on the host')
with tempfile.NamedTemporaryFile('w') as host_script:
iperf_configs = [f'iperf3 -s -p {5100+i} &'
for i in range(0, vif_limit)]
host_script.write('\n'.join(iperf_configs))
host_script.flush()
host.scp(host_script.name, host_script.name)
host.ssh([f'nohup bash -c "bash {host_script.name}" < /dev/null &>/dev/null &'],
background=True)

logging.info('Start multiple iperfs on separate interfaces on the VM')
with tempfile.NamedTemporaryFile('w') as vm_script:
iperf_configs = [f'iperf3 --no-delay -c {host.hostname_or_ip} '
f'-p {5100+i} --bind-dev {interface_name}{i} '
f'--interval 0 --parallel 1 --time 30 &'
for i in range(0, vif_limit)]
vm_script.write('\n'.join(iperf_configs))
vm_script.flush()
vm.scp(vm_script.name, vm_script.name)
stdout = vm.ssh([f'bash {vm_script.name}'])

# TODO: log this into some performance time series DB
logging.info(stdout)
finally:
vm.ssh(['pkill iperf3 || true'])
host.ssh('killall iperf3')