diff --git a/providers/base/bin/sriov.py b/providers/base/bin/sriov.py
new file mode 100755
index 0000000000..4143b093da
--- /dev/null
+++ b/providers/base/bin/sriov.py
@@ -0,0 +1,258 @@
+#!/usr/bin/env python3
+"""
+Scripts used to test sriov network functions
+Copyright (C) 2025 Canonical Ltd.
+
+Author
+ Michael Reed .
+"""
+
+import argparse
+import logging
+import os
+import sys
+from checkbox_support.lxd_support import LXD, LXDVM
+
+# Map vendor IDs to vendor names
+VENDOR_INFO = {
+ "0x8086": ("Intel"),
+ "0x15b3": ("Mellanox"),
+ "0x14e4": ("Broadcom"),
+}
+
+# Number of virutal interfaces created for SRIOV Enabled Interfaces
+NUM_OF_VIRTUAL_IFACES = 1
+
+
+def get_release_to_test():
+ try:
+ import distro
+
+ if distro.id() == "ubuntu-core":
+ return "{}.04".format(distro.version())
+ return distro.version()
+ except (ImportError):
+ import lsb_release
+
+ return lsb_release.get_distro_information()["RELEASE"]
+
+
+def check_ubuntu_version():
+ logging.info("Check for 24.04 or greater")
+ try:
+ version = get_release_to_test()
+
+ if float(version) < 24.04:
+ raise ValueError("Ubuntu 24.04 or greater is required, \
+ but found {}.".format(version))
+ else:
+ logging.info("The system is 24.04 or greater, proceed")
+
+ except (Exception, ValueError) as e:
+ logging.error("Error checking Ubuntu version: {}".format(e))
+
+
+def check_interface_vendor(interface):
+ """
+ Find the vendor of the network interface
+ """
+ vendor_id_path = "/sys/class/net/{}/device/vendor".format(interface)
+
+ try:
+ if not os.path.exists(vendor_id_path):
+ raise FileNotFoundError(
+ "Vendor ID path {} not found".format(vendor_id_path))
+
+ with open(vendor_id_path, "r", encoding="utf-8") as file:
+ vendor_id = file.read().strip()
+
+ if vendor_id not in VENDOR_INFO:
+ raise ValueError("{} has an unknown vendor ID {}".
+ format(interface, vendor_id))
+
+ vendor_name = VENDOR_INFO[vendor_id]
+ if vendor_name == "Broadcom":
+ raise NotImplementedError(
+ "Broadcom SRIOV testing is not supported at this time")
+
+ logging.info("The interface %s is a(n) %s NIC", interface, vendor_name)
+
+ except (FileNotFoundError, ValueError, NotImplementedError) as e:
+ logging.info("An error occurred: {}".format(e))
+ sys.exit(1)
+
+
+def is_sriov_capable(interface):
+ """
+ Check if the specified network interface is SR-IOV capable and
+ configured to support at least one Virtual Function.
+ """
+ sriov_path = "/sys/class/net/{}/device/sriov_numvfs".format(interface)
+ num_vfs = NUM_OF_VIRTUAL_IFACES
+
+ try:
+ # Check if the interface supports SR-IOV
+ logging.info("checking if sriov_numvfs exits")
+ if not os.path.exists(sriov_path):
+ raise FileNotFoundError(
+ "SR-IOV not supported or interface {} does not exist.".format(
+ interface
+ )
+ )
+ else:
+ logging.info("SR-IOV before change {} VFs on interface {}."
+ .format(num_vfs, interface))
+ # First, disable VFs before changing the number to avoid issues
+ logging.info("Setting numvfs to zero")
+ with open(sriov_path, "w", encoding="utf-8") as f:
+ f.write("0")
+
+ # Set the desired number of VFs
+ logging.info("Setting numvfs to %d", num_vfs)
+ with open(sriov_path, "w", encoding="utf-8") as f:
+ f.write(str(num_vfs))
+
+ logging.info(
+ "SR-IOV enabled with {} VFs on interface {}.".format(
+ num_vfs, interface
+ )
+ )
+
+ except (IOError, FileNotFoundError) as e:
+ logging.info("Failed to enable SR-IOV on {}: {}".format(interface, e))
+ sys.exit(1)
+
+ except Exception as e:
+ logging.info("An error occurred: {}".format(e))
+ sys.exit(1)
+
+
+def test_lxd_sriov(args):
+ logging.info("Starting lxd SRIOV Test")
+ verify_cmds = 'bash -c "lspci | grep Virtual"'
+ options = ["--network", "lab_sriov"]
+ network_cmd = "lxc network create lab_sriov --type=sriov parent={}".format(
+ args.interface
+ )
+
+ check_ubuntu_version()
+ check_interface_vendor(args.interface)
+ is_sriov_capable(args.interface)
+
+ with LXD(args.template, args.rootfs) as instance:
+ logging.info("Create sriov network for lxc")
+ instance.run("lxc network delete lab_sriov", ignore_errors=True)
+ instance.run(network_cmd)
+
+ logging.info("Launching container: %s", instance.name)
+ instance.launch(options)
+
+ logging.info("Waiting for %s to be up", instance.name)
+ instance.wait_until_running()
+
+ instance.run(verify_cmds, on_guest=True)
+
+ instance.run("lxc network delete lab_sriov")
+
+
+def test_lxd_vm_sriov(args):
+ logging.info("Starting lxd vm SRIOV")
+ verify_cmds = 'bash -c "lspci | grep Virtual"'
+ options = ["-c", "security.secureboot=false", "--network", "lab_sriov"]
+ network_cmd = "lxc network create lab_sriov --type=sriov parent={}".format(
+ args.interface
+ )
+
+ check_ubuntu_version()
+ check_interface_vendor(args.interface)
+ is_sriov_capable(args.interface)
+
+ with LXDVM(args.template, args.image) as instance:
+ logging.info("Create sriov network for lxc vm")
+ instance.run("lxc network delete lab_sriov", ignore_errors=True)
+ instance.run(network_cmd)
+
+ logging.info("Launching virtual machine: %s", instance.name)
+ instance.launch(options)
+
+ logging.info("Waiting for %s to be up", instance.name)
+ instance.wait_until_running()
+
+ logging.info("running a simple command")
+ instance.run(verify_cmds, on_guest=True)
+
+ instance.run("lxc network delete lab_sriov")
+
+
+def main():
+
+ parser = argparse.ArgumentParser(description="SRIOV Test")
+ subparsers = parser.add_subparsers()
+
+ # Main cli options
+ lxd_sriov_parser = subparsers.add_parser(
+ "lxd", help=("Run the SRIOV test on LXD validation test")
+ )
+ lxd_vm_sriov_parser = subparsers.add_parser(
+ "lxdvm", help=("Run the SRIOV test on VM validation test")
+ )
+
+ parser.add_argument(
+ "--debug",
+ dest="log_level",
+ action="store_const",
+ const=logging.DEBUG,
+ default=logging.INFO,
+ )
+
+ parser.add_argument(
+ "--interface", type=str, default=None, help="SRIOV Interface"
+ )
+
+ # Sub test options
+ lxd_sriov_parser.add_argument(
+ "--template", type=str, default=os.getenv("LXD_TEMPLATE")
+ )
+
+ lxd_sriov_parser.add_argument(
+ "--rootfs", type=str, default=os.getenv("LXD_ROOTFS")
+ )
+
+ lxd_sriov_parser.set_defaults(func=test_lxd_sriov)
+
+ # Sub test options
+ lxd_vm_sriov_parser.add_argument(
+ "--template", type=str, default=os.getenv("LXD_TEMPLATE")
+ )
+
+ lxd_vm_sriov_parser.add_argument(
+ "--image", type=str, default=os.getenv("KVM_IMAGE")
+ )
+
+ lxd_vm_sriov_parser.set_defaults(func=test_lxd_vm_sriov)
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.log_level)
+
+ # silence normal output from requests module
+ logging.getLogger("requests").setLevel(logging.WARNING)
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
+
+ args.func(args)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/providers/base/tests/test_sriov.py b/providers/base/tests/test_sriov.py
new file mode 100644
index 0000000000..39928cac3a
--- /dev/null
+++ b/providers/base/tests/test_sriov.py
@@ -0,0 +1,234 @@
+#!/usr/bin/env python3
+"""
+Unit Tests for sriov.py
+Copyright (C) 2025 Canonical Ltd.
+
+Author
+ Michael Reed
+
+"""
+
+from unittest import TestCase
+from unittest.mock import MagicMock, patch, mock_open
+import sriov
+
+
+class TestSriovFunctions(TestCase):
+ @patch("sriov.distro.version", return_value="24.04")
+ @patch("sriov.logging.info")
+ def test_check_ubuntu_version_valid(self, mock_logging, mock_version):
+ sriov.check_ubuntu_version()
+ mock_logging.assert_called_with(
+ "The system is 24.04 or greater, proceed"
+ )
+
+ @patch("sriov.distro.version", return_value="22.04")
+ @patch("sriov.logging.info")
+ @patch("sriov.sys.exit")
+ def test_check_ubuntu_version_invalid(
+ self, mock_exit, mock_logging, mock_version
+ ):
+ sriov.check_ubuntu_version()
+ mock_logging.assert_called_with(
+ "24.04 or greater is required, this is 22.04"
+ )
+ mock_exit.assert_called_once_with(1)
+
+ @patch("distro.version", side_effect=Exception("Mocked exception"))
+ @patch("sys.exit")
+ @patch("logging.info")
+ def test_check_ubuntu_version_exception(
+ self, mock_logging, mock_exit, mock_distro_version
+ ):
+ sriov.check_ubuntu_version()
+
+ # Verify that the exception was logged
+ mock_logging.assert_any_call("An error occurred: Mocked exception")
+
+ # Verify that sys.exit(1) was called
+ mock_exit.assert_called_once_with(1)
+
+ @patch("os.path.exists", return_value=True)
+ @patch("builtins.open", new_callable=mock_open, read_data="0x8086")
+ @patch("sriov.logging.info")
+ def test_check_interface_vendor_intel(
+ self, mock_logging, mock_open, mock_exists
+ ):
+ sriov.check_interface_vendor("eth0")
+ mock_logging.assert_called_with("The interface eth0 is a(n) Intel NIC")
+
+ @patch("os.path.exists", return_value=True)
+ @patch("builtins.open", new_callable=mock_open, read_data="0x15b3")
+ @patch("sriov.logging.info")
+ def test_check_interface_vendor_mellanox(
+ self, mock_logging, mock_open, mock_exists
+ ):
+ sriov.check_interface_vendor("eth0")
+ mock_logging.assert_called_with(
+ "The interface eth0 is a(n) Mellanox NIC"
+ )
+
+ @patch("os.path.exists", return_value=True)
+ @patch("builtins.open", new_callable=mock_open, read_data="0x14e4")
+ @patch("sriov.logging.info")
+ @patch("sriov.sys.exit")
+ def test_check_interface_vendor_broadcom(
+ self, mock_exit, mock_logging, mock_open, mock_exists
+ ):
+ sriov.check_interface_vendor("eth0")
+ mock_logging.assert_called_with(
+ "Broadcom SRIOV testing is not supported at this time"
+ )
+ mock_exit.assert_called_once_with(1)
+
+ @patch("os.path.exists", return_value=True)
+ @patch("builtins.open", new_callable=mock_open, read_data="0x0000")
+ @patch("sriov.logging.info")
+ @patch("sriov.sys.exit")
+ def test_check_interface_vendor_unknown(
+ self, mock_exit, mock_logging, mock_open, mock_exists
+ ):
+ sriov.check_interface_vendor("eth0")
+ mock_logging.assert_called_with(
+ "eth0 has an unknown vendor ID 0x0000"
+ )
+ mock_exit.assert_called_once_with(1)
+
+ @patch("os.path.exists", return_value=True)
+ @patch("builtins.open", side_effect=Exception("File read error"))
+ @patch("sys.exit") # Mock sys.exit to prevent actual exit
+ def test_check_interface_vendor_exception(
+ self, mock_exit, mock_open, mock_exists
+ ):
+ with self.assertLogs(level="INFO") as log:
+ sriov.check_interface_vendor("eth0")
+
+ mock_exit.assert_called_once_with(1)
+ self.assertIn("An error occurred: File read error", log.output[0])
+
+ @patch("os.path.exists", return_value=True)
+ @patch("builtins.open", new_callable=mock_open)
+ @patch("sriov.logging.info")
+ def test_is_sriov_capable(self, mock_logging, mock_open, mock_exists):
+ sriov.is_sriov_capable("eth0")
+ mock_logging.assert_any_call(
+ "SR-IOV enabled with 1 VFs on interface eth0."
+ )
+
+ @patch("os.path.exists", return_value=True)
+ @patch("builtins.open", side_effect=IOError("Permission denied"))
+ @patch("sys.exit")
+ def test_sriov_ioerror(self, mock_exit, mock_open, mock_exists):
+ """Test when opening the file raises IOError."""
+ sriov.is_sriov_capable("eth0")
+ mock_exit.assert_called_once_with(1)
+
+ @patch("os.path.exists", return_value=True)
+ @patch("builtins.open", side_effect=FileNotFoundError("File not found"))
+ @patch("sys.exit")
+ def test_sriov_filenotfound(self, mock_exit, mock_open, mock_exists):
+ """Test when the sriov file is missing, raising FileNotFoundError."""
+ sriov.is_sriov_capable("eth0")
+ mock_exit.assert_called_once_with(1)
+
+ @patch("os.path.exists", return_value=True)
+ @patch("builtins.open", side_effect=Exception("Unknown error"))
+ @patch("sys.exit")
+ def test_sriov_general_exception(self, mock_exit, mock_open, mock_exists):
+ """Test when a general exception occurs in is_sriov_capable."""
+ sriov.is_sriov_capable("eth0")
+ mock_exit.assert_called_once_with(1)
+
+ @patch("sriov.check_ubuntu_version")
+ @patch("sriov.check_interface_vendor")
+ @patch("sriov.is_sriov_capable")
+ @patch("sriov.LXD")
+ @patch("sriov.logging.info")
+ def test_test_lxd_sriov(
+ self,
+ mock_logging,
+ mock_lxd,
+ mock_sriov_capable,
+ mock_check_vendor,
+ mock_check_version,
+ ):
+ mock_instance = MagicMock()
+ mock_lxd.return_value.__enter__.return_value = mock_instance
+ args = MagicMock()
+ args.interface = "eth0"
+ args.template = "template"
+ args.rootfs = "rootfs"
+
+ sriov.test_lxd_sriov(args)
+
+ mock_check_version.assert_called_once()
+ mock_check_vendor.assert_called_once_with("eth0")
+ mock_sriov_capable.assert_called_once_with("eth0")
+ mock_instance.run.assert_any_call(
+ "lxc network create lab_sriov --type=sriov parent=eth0"
+ )
+ mock_instance.launch.assert_called_once()
+ mock_instance.wait_until_running.assert_called_once()
+ mock_instance.run.assert_any_call(
+ 'bash -c "lspci | grep Virtual"', on_guest=True
+ )
+ mock_instance.run.assert_any_call("lxc network delete lab_sriov")
+
+ @patch("sriov.check_ubuntu_version")
+ @patch("sriov.check_interface_vendor")
+ @patch("sriov.is_sriov_capable")
+ @patch("sriov.LXDVM")
+ @patch("sriov.logging.info")
+ def test_test_lxd_vm_sriov(
+ self,
+ mock_logging,
+ mock_lxdvm,
+ mock_sriov_capable,
+ mock_check_vendor,
+ mock_check_version,
+ ):
+ mock_instance = MagicMock()
+ mock_lxdvm.return_value.__enter__.return_value = mock_instance
+ args = MagicMock()
+ args.interface = "eth0"
+ args.template = "template"
+ args.image = "image"
+
+ sriov.test_lxd_vm_sriov(args)
+
+ mock_check_version.assert_called_once()
+ mock_check_vendor.assert_called_once_with("eth0")
+ mock_sriov_capable.assert_called_once_with("eth0")
+ mock_instance.run.assert_any_call(
+ "lxc network create lab_sriov --type=sriov parent=eth0"
+ )
+ mock_instance.launch.assert_called_once()
+ mock_instance.wait_until_running.assert_called_once()
+ mock_instance.run.assert_any_call(
+ 'bash -c "lspci | grep Virtual"', on_guest=True
+ )
+ mock_instance.run.assert_any_call("lxc network delete lab_sriov")
+
+ @patch("sys.argv", ["sriov.py", "lxd", "--interface", "eth0"])
+ @patch("sriov.test_lxd_sriov")
+ @patch("sriov.logging.basicConfig")
+ def test_main(self, mock_logging_config, mock_test_lxd_sriov):
+ with patch("sys.exit") as mock_exit:
+ sriov.main()
+ mock_test_lxd_sriov.assert_called_once()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/providers/base/units/sriov/category.pxu b/providers/base/units/sriov/category.pxu
new file mode 100644
index 0000000000..b5d36c6316
--- /dev/null
+++ b/providers/base/units/sriov/category.pxu
@@ -0,0 +1,3 @@
+unit: category
+id: sriov
+_name: SRIOV Tests
diff --git a/providers/base/units/sriov/jobs.pxu b/providers/base/units/sriov/jobs.pxu
new file mode 100644
index 0000000000..58a1db63c5
--- /dev/null
+++ b/providers/base/units/sriov/jobs.pxu
@@ -0,0 +1,37 @@
+unit: template
+template-resource: device
+template-filter: device.category == 'NETWORK'
+plugin: shell
+category_id: sriov
+id: sriov/verify_lxd_vm_sriov{__index__}_{interface}
+template-id: sriov/verify_lxd_vm_sriov__index___interface
+environ: LXD_TEMPLATE KVM_IMAGE
+estimated_duration: 60.0
+requires:
+ executable.name == 'lxc'
+ package.name == 'lxd-installer' or snap.name == 'lxd'
+user: root
+command: sriov.py --debug --interface {interface} lxdvm
+_purpose:
+ Verifies that sriov network device can created with an LXD Virtual Machine
+_summary:
+ Verify SRIOV NIC with LXD Virtual Machine
+
+unit: template
+template-resource: device
+template-filter: device.category == 'NETWORK'
+plugin: shell
+category_id: sriov
+id: sriov/verify_lxd_sriov{__index__}_{interface}
+template-id: sriov/verify_lxd_sriov__index___interface
+environ: LXD_TEMPLATE LXD_ROOTFS
+estimated_duration: 60.0
+requires:
+ executable.name == 'lxc'
+ package.name == 'lxd' or package.name == 'lxd-installer' or snap.name == 'lxd'
+user: root
+command: sriov.py --debug --interface {interface} lxd
+_purpose:
+ Verifies that an SRIOV network device can be created with an LXD container
+_summary:
+ Verify SRIOV NIC with LXD container
diff --git a/providers/base/units/sriov/test-plan.pxu b/providers/base/units/sriov/test-plan.pxu
new file mode 100644
index 0000000000..f6e9251c8f
--- /dev/null
+++ b/providers/base/units/sriov/test-plan.pxu
@@ -0,0 +1,19 @@
+id: sriov-only
+_name: SRIOV Only Test Plan (Only runs SRIOV tests)
+unit: test plan
+_description:
+ This test plan is intended to be used for retesting of SRIOV
+ capabilities only. It does not provide any testing of other hardware
+ and should only be run at the direction of the Cert Team when re-testing
+ of virtualization capabilites is requried.
+nested_part:
+ com.canonical.certification::server-info-attachment-automated
+ com.canonical.certification::server-miscellaneous
+include:
+ sriov/verify_lxd_sriov-.* certification-status=blocker
+ sriov/verify_lxd_vm_sriov-.* certification-status=blocker
+bootstrap_include:
+ device
+ executable
+ package
+ snap