Running the DPDK test crypto performance application is essential for
testing cryptodev performance in DTS. This application takes numerous
arguments and can be run in four modes; Throughput, Latency,
PMD-cyclecount, and Verify. The package to add in this commit allows for
this application to be run in any of these modes with user supplied
arguments.

Signed-off-by: Andrew Bailey <[email protected]>
---
 dts/api/cryptodev/__init__.py                | 132 +++++
 dts/api/cryptodev/config.py                  | 484 +++++++++++++++++++
 dts/api/cryptodev/types.py                   | 185 +++++++
 dts/framework/config/node.py                 |   2 +
 dts/framework/params/types.py                |  65 +++
 dts/framework/remote_session/dpdk_shell.py   |   5 +-
 dts/framework/test_run.py                    |   5 +
 dts/framework/test_suite.py                  |   2 +
 dts/framework/testbed_model/linux_session.py |  91 +++-
 dts/framework/testbed_model/node.py          |   4 +
 dts/framework/testbed_model/os_session.py    |  34 ++
 dts/framework/testbed_model/topology.py      |  90 +++-
 12 files changed, 1095 insertions(+), 4 deletions(-)
 create mode 100644 dts/api/cryptodev/__init__.py
 create mode 100644 dts/api/cryptodev/config.py
 create mode 100644 dts/api/cryptodev/types.py

diff --git a/dts/api/cryptodev/__init__.py b/dts/api/cryptodev/__init__.py
new file mode 100644
index 0000000000..d13fcc4a53
--- /dev/null
+++ b/dts/api/cryptodev/__init__.py
@@ -0,0 +1,132 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2025 University of New Hampshire
+
+"""Cryptodev-pmd non-interactive shell.
+
+Typical usage example in a TestSuite::
+
+    cryptodev = CryptodevPmd(CryptoPmdParams)
+    stats = cryptodev.run_app()
+"""
+
+import re
+from typing import TYPE_CHECKING, Any
+
+from typing_extensions import Unpack
+
+from api.cryptodev.config import CryptoPmdParams, TestType
+from api.cryptodev.types import (
+    CryptodevResults,
+    LatencyResults,
+    PmdCyclecountResults,
+    ThroughputResults,
+    VerifyResults,
+)
+from framework.context import get_ctx
+from framework.exception import RemoteCommandExecutionError, 
SkippedTestException
+from framework.remote_session.dpdk_shell import compute_eal_params
+
+if TYPE_CHECKING:
+    from framework.params.types import CryptoPmdParamsDict
+from pathlib import PurePath
+
+
+class Cryptodev:
+    """non-interactive cryptodev application.
+
+    Attributes:
+        _app_params: app parameters to pass to dpdk-test-crypto-perf 
application
+    """
+
+    _app_params: dict[str, Any]
+
+    def __init__(self, **app_params: Unpack["CryptoPmdParamsDict"]) -> None:
+        """Initialize the cryptodev application.
+
+        Args:
+            app_params: The application parameters as keyword arguments.
+
+        Raises:
+            ValueError: if the build environment is `None`
+        """
+        self._app_params = {}
+        for k, v in app_params.items():
+            if v is not None:
+                self._app_params[k] = (
+                    self.vector_directory.joinpath(str(v)) if k == "test_file" 
else v
+                )
+        dpdk = get_ctx().dpdk.build
+        if dpdk is None:
+            raise ValueError("No DPDK build environment exists.")
+        self._path = dpdk.get_app("test-crypto-perf")
+
+    @property
+    def path(self) -> PurePath:
+        """Get the path to the cryptodev application.
+
+        Returns:
+            The path to the cryptodev application.
+        """
+        return PurePath(self._path)
+
+    @property
+    def vector_directory(self) -> PurePath:
+        """Get the path to the cryptodev vector files.
+
+        Returns:
+            The path to the cryptodev vector files.
+        """
+        return 
get_ctx().dpdk_build.remote_dpdk_tree_path.joinpath("app/test-crypto-perf/data/")
+
+    def run_app(self, numvfs: int | None = 0) -> list[CryptodevResults]:
+        """Run the cryptodev application with the given app parameters.
+
+        Raises:
+            SkippedTestException: If the device type is not supported on the 
main session.
+            RemoteCommandExecutionError: If there is an error running the 
command.
+
+        Returns:
+            list[CryptodevResults]: The list of parsed results for the 
cryptodev application.
+        """
+        allowed_ports = get_ctx().topology.get_crypto_vfs(numvfs)
+        print(f"ALLOWED PORTS: {allowed_ports}")
+        try:
+            result = get_ctx().dpdk.run_dpdk_app(
+                self.path,
+                compute_eal_params(
+                    CryptoPmdParams(
+                        
allowed_ports=get_ctx().topology.get_crypto_vfs(numvfs),
+                        memory_channels=get_ctx().dpdk.config.memory_channels,
+                        **self._app_params,
+                    ),
+                ),
+                timeout=120,
+            )
+        except RemoteCommandExecutionError as e:
+            if "Crypto device type does not support capabilities requested" in 
e._command_stderr:
+                raise SkippedTestException(
+                    f"{self._app_params['devtype']} does not support the 
requested capabilities"
+                )
+            elif "Failed to initialise requested crypto device type" in 
e._command_stderr:
+                raise SkippedTestException(
+                    f"could not run application with devtype 
{self._app_params['devtype']}"
+                )
+            raise e
+
+        regex = r"^\s+\d+.*$"
+        parser_options = re.MULTILINE
+        parser: type[CryptodevResults]
+
+        match self._app_params["ptest"]:
+            case TestType.throughput:
+                parser = ThroughputResults
+            case TestType.latency:
+                regex = r"total operations:.*time[^\n]*"
+                parser_options |= re.DOTALL
+                parser = LatencyResults
+            case TestType.pmd_cyclecount:
+                parser = PmdCyclecountResults
+            case TestType.verify:
+                parser = VerifyResults
+
+        return [parser.parse(line) for line in re.findall(regex, 
result.stdout, parser_options)]
diff --git a/dts/api/cryptodev/config.py b/dts/api/cryptodev/config.py
new file mode 100644
index 0000000000..8b1a293018
--- /dev/null
+++ b/dts/api/cryptodev/config.py
@@ -0,0 +1,484 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2025 University of New Hampshire
+"""Module containing types and parameter classes for cryptodev-pmd 
application."""
+
+from dataclasses import dataclass, field
+from enum import auto
+from typing import Literal
+
+from framework.params import Params, Switch
+from framework.params.eal import EalParams
+from framework.utils import StrEnum
+
+Silent = Literal[""]
+
+
+class DeviceType(StrEnum):
+    """Enum for cryptodev device types.
+
+    Attributes:
+        crypto_aesni_gcm: AES-NI GCM device type.
+        crypto_aesni_mb: AES-NI MB device type.
+        crypto_armv8: ARMv8 device type.
+        crypto_cn10k: CN10K device type.
+        crypto_cn9k: CN9K device type.
+        crypto_dpaa_sec: DPAA SEC device type.
+        crypto_dpaa2_sec: DPAA2 SEC device type.
+        crypto_kasumi: KASUMI device type.
+        crypto_mvsam: MVSAM device type.
+        crypto_null: NULL device type.
+        crypto_octeontx: OCTEONTX device type.
+        crypto_openssl: OpenSSL device type.
+        crypto_qat: QAT device type.
+        crypto_scheduler: Scheduler device type.
+        crypto_snow3g: SNOW3G device type.
+        crypto_zuc: ZUC device type.
+    """
+
+    crypto_aesni_gcm = auto()
+    crypto_aesni_mb = auto()
+    crypto_armv8 = auto()
+    crypto_cn10k = auto()
+    crypto_cn9k = auto()
+    crypto_dpaa_sec = auto()
+    crypto_dpaa2_sec = auto()
+    crypto_kasumi = auto()
+    crypto_mvsam = auto()
+    crypto_null = auto()
+    crypto_octeontx = auto()
+    crypto_openssl = auto()
+    crypto_qat = auto()
+    crypto_scheduler = auto()
+    crypto_snow3g = auto()
+    crypto_zuc = auto()
+
+
+class OperationType(StrEnum):
+    """Enum for cryptodev operation types.
+
+    Attributes:
+        aead: AEAD operation type.
+        auth_only: Authentication only operation type.
+        auth_then_cipher: Authentication then cipher operation type.
+        cipher_only: Cipher only operation type.
+        cipher_then_auth: Cipher then authentication operation type.
+        docsis: DOCSIS operation type.
+        ecdsa_p192r1 = ECDSA P-192R1 operation type.
+        ecdsa_p224r1 = ECDSA P-224R1 operation type.
+        ecdsa_p256r1: ECDSA P-256R1 operation type.
+        ecdsa_p384r1 = ECDSA P-384R1 operation type.
+        ecdsa_p521r1 = ECDSA P-521R1 operation type.
+        eddsa_25519: EdDSA 25519 operation type.
+        modex: Modex operation type.
+        ipsec: IPsec operation type.
+        pdcp: PDCP operation type.
+        rsa: RSA operation type.
+        sm2: SM2 operation type.
+        tls_record: TLS record operation type.
+    """
+
+    aead = auto()
+    auth_only = "auth-only"
+    auth_then_cipher = "auth-then-cipher"
+    cipher_only = "cipher-only"
+    cipher_then_auth = "cipher-then-auth"
+    docsis = auto()
+    ecdsa_p192r1 = auto()
+    ecdsa_p224r1 = auto()
+    ecdsa_p256r1 = auto()
+    ecdsa_p384r1 = auto()
+    ecdsa_p521r1 = auto()
+    eddsa_25519 = auto()
+    modex = auto()
+    ipsec = auto()
+    pdcp = auto()
+    rsa = auto()
+    sm2 = auto()
+    tls_record = "tls-record"
+
+    def __str__(self) -> str:
+        """Override str to return the value."""
+        return self.value
+
+
+class CipherAlgorithm(StrEnum):
+    """Enum for cryptodev cipher algorithms.
+
+    Attributes:
+        aes_cbc: AES CBC cipher algorithm.
+        aes_ctr: AES CTR cipher algorithm.
+        aes_docsisbpi: AES DOCSIS BPI cipher algorithm.
+        aes_ecb: AES ECB cipher algorithm.
+        aes_f8: AES F8 cipher algorithm.
+        aes_xts: AES XTS cipher algorithm.
+        arc4: ARC4 cipher algorithm.
+        null: NULL cipher algorithm.
+        three_des_cbc: 3DES CBC cipher algorithm.
+        three_des_ctr: 3DES CTR cipher algorithm.
+        three_des_ecb: 3DES ECB cipher algorithm.
+        kasumi_f8: KASUMI F8 cipher algorithm.
+        snow3g_uea2: SNOW3G UEA2 cipher algorithm.
+        zuc_eea3: ZUC EEA3 cipher algorithm.
+    """
+
+    aes_cbc = "aes-cbc"
+    aes_ctr = "aes-ctr"
+    aes_docsisbpi = "aes-docsisbpi"
+    aes_ecb = "aes-ecb"
+    aes_f8 = "aes-f8"
+    aes_gcm = "aes-gcm"
+    aes_xts = "aes-xts"
+    arc4 = auto()
+    null = auto()
+    three_des_cbc = "3des-cbc"
+    three_des_ctr = "3des-ctr"
+    three_des_ecb = "3des-ecb"
+    kasumi_f8 = "kasumi-f8"
+    snow3g_uea2 = "snow3g-uea2"
+    zuc_eea3 = "zuc-eea3"
+
+    def __str__(self):
+        """Override str to return the value."""
+        return self.value
+
+
+class AuthenticationAlgorithm(StrEnum):
+    """Enum for cryptodev authentication algorithms.
+
+    Attributes:
+        aes_cbc_mac: AES CBC MAC authentication algorithm.
+        aes_cmac: AES CMAC authentication algorithm.
+        aes_gmac: AES GMAC authentication algorithm.
+        aes_xcbc_mac: AES XCBC MAC authentication algorithm.
+        kasumi_f9: KASUMI F9 authentication algorithm.
+        md5: MD5 authentication algorithm.
+        md5_hmac: MD5 HMAC authentication algorithm.
+        sha1: SHA1 authentication algorithm.
+        sha1_hmac: SHA1 HMAC authentication algorithm.
+        sha2_224: SHA2-224 authentication algorithm.
+        sha2_224_hmac: SHA2-224 HMAC authentication algorithm.
+        sha2_256: SHA2-256 authentication algorithm.
+        sha2_256_hmac: SHA2-256 HMAC authentication algorithm.
+        sha2_384: SHA2-384 authentication algorithm.
+        sha2_384_hmac: SHA2-384 HMAC authentication algorithm.
+        sha2_512: SHA2-512 authentication algorithm.
+        sha2_512_hmac: SHA2-512 HMAC authentication algorithm.
+        snow3g_uia2: SNOW3G UIA2 authentication algorithm.
+        zuc_eia3: ZUC EIA3 authentication algorithm.
+    """
+
+    aes_cbc_mac = "aes-cbc-mac"
+    aes_cmac = "aes-cmac"
+    aes_gmac = "aes-gmac"
+    aes_xcbc_mac = "aes-xcbc-mac"
+    kasumi_f9 = "kasumi-f9"
+    md5 = auto()
+    md5_hmac = "md5-hmac"
+    sha1 = auto()
+    sha1_hmac = "sha1-hmac"
+    sha2_224 = "sha2-224"
+    sha2_224_hmac = "sha2-224-hmac"
+    sha2_256 = "sha2-256"
+    sha2_256_hmac = "sha2-256-hmac"
+    sha2_384 = "sha2-384"
+    sha2_384_hmac = "sha2-384-hmac"
+    sha2_512 = "sha2-512"
+    sha2_512_hmac = "sha2-512-hmac"
+    snow3g_uia2 = "snow3g-uia2"
+    zuc_eia3 = "zuc-eia3"
+
+    def __str__(self) -> str:
+        """Override str to return the value."""
+        return self.value
+
+
+class TestType(StrEnum):
+    """Enum for cryptodev test types.
+
+    Attributes:
+        latency: Latency test type.
+        pmd_cyclecount: PMD cyclecount test type.
+        throughput: Throughput test type.
+        verify: Verify test type.
+    """
+
+    latency = auto()
+    pmd_cyclecount = "pmd-cyclecount"
+    throughput = auto()
+    verify = auto()
+
+    def __str__(self) -> str:
+        """Override str to return the value."""
+        return self.value
+
+
+class EncryptDecryptSwitch(StrEnum):
+    """Enum for cryptodev encrypt/decrypt operations.
+
+    Attributes:
+        decrypt: Decrypt operation.
+        encrypt: Encrypt operation.
+    """
+
+    decrypt = auto()
+    encrypt = auto()
+
+
+class AuthenticationOpMode(StrEnum):
+    """Enum for cryptodev authentication operation modes.
+
+    Attributes:
+        generate: Generate operation mode.
+        verify: Verify operation mode.
+    """
+
+    generate = auto()
+    verify = auto()
+
+
+class AeadAlgName(StrEnum):
+    """Enum for cryptodev AEAD algorithms.
+
+    Attributes:
+        aes_ccm: AES CCM algorithm.
+        aes_gcm: AES GCM algorithm.
+    """
+
+    aes_ccm = "aes-ccm"
+    aes_gcm = "aes-gcm"
+
+    def __str__(self):
+        """Override str to return the value."""
+        return self.value
+
+
+class AsymOpMode(StrEnum):
+    """Enum for cryptodev asymmetric operation modes.
+
+    Attributes:
+        decrypt: Decrypt operation mode.
+        encrypt: Encrypt operation mode.
+        sign: Sign operation mode.
+        verify: Verify operation mode.
+    """
+
+    decrypt = auto()
+    encrypt = auto()
+    sign = auto()
+    verify = auto()
+
+
+class PDCPDomain(StrEnum):
+    """Enum for cryptodev PDCP domains.
+
+    Attributes:
+        control: Control domain.
+        user: User domain.
+    """
+
+    control = auto()
+    user = auto()
+
+
+class RSAPrivKeyType(StrEnum):
+    """Enum for cryptodev RSA private key types.
+
+    Attributes:
+        exp: Exponent key type.
+        qt: QT key type.
+    """
+
+    exp = auto()
+    qt = auto()
+
+
+class TLSVersion(StrEnum):
+    """Enum for cryptodev TLS versions.
+
+    Attributes:
+        DTLS1_2: DTLS 1.2 version.
+        TLS1_2: TLS 1.2 version.
+        TLS1_3: TLS 1.3 version.
+    """
+
+    DTLS1_2 = "DTLS1.2"
+    TLS1_2 = "TLS1.2"
+    TLS1_3 = "TLS1.3"
+
+    def __str__(self):
+        """Override str to return the value."""
+        return self.value
+
+
+class RSAPrivKeytype(StrEnum):
+    """Enum for cryptodev RSA private key types.
+
+    Attributes:
+        exp: Exponent key type.
+        qt: QT key type.
+    """
+
+    exp = auto()
+    qt = auto()
+
+
+class BurstSizeRange:
+    """Class for burst size parameter.
+
+    Attributes:
+        burst_size: The burst size range, this list must be less than 32 
elements.
+    """
+
+    burst_size: int | list[int]
+
+    def __init__(self, min: int, inc: int, max: int) -> None:
+        """Initialize the burst size range.
+
+        Raises:
+            ValueError: If the burst size range is more than 32 elements.
+        """
+        if (max - min) % inc > 32:
+            raise ValueError("Burst size range must be less than 32 elements.")
+        self.burst_size = list(range(min, max + 1, inc))
+
+
+class ListWrapper:
+    """Class for wrapping a list of integers.
+
+    One of the arguments for the cryptodev application is a list of integers. 
However, when
+    passing a list directly, it causes a syntax error in the command line. 
This class wraps
+    a list of integers and converts it to a comma-separated string for a 
proper command.
+    """
+
+    def __init__(self, value: list[int]) -> None:
+        """Initialize the list wrapper."""
+        self.value = value
+
+    def __str__(self) -> str:
+        """Convert the list to a comma-separated string."""
+        return ",".join(str(v) for v in self.value)
+
+
+@dataclass(slots=True, kw_only=True)
+class CryptoPmdParams(EalParams):
+    """Parameters for cryptodev-pmd application.
+
+    Attributes:
+        aead_aad_sz: set the size of AEAD AAD.
+        aead_algo: set AEAD algorithm name from class `AeadAlgName`.
+        aead_iv_sz: set the size of AEAD iv.
+        aead_key_sz: set the size of AEAD key.
+        aead_op: set AEAD operation mode from class `EncryptDecryptSwitch`.
+        asym_op: set asymmetric operation mode from class `AsymOpMode`.
+        auth_algo: set authentication algorithm name.
+        auth_aad_sz: set the size of authentication AAD.
+        auth_iv_sz: set the size of authentication iv.
+        auth_key_sz: set the size of authentication key.
+        auth_op: set authentication operation mode from class 
`AuthenticationOpMode`.
+        buffer_sz: Set the size of a single packet (plaintext or ciphertext in 
it).
+            burst_sz: Set the number of packets per burst. This can be set as 
a single value or
+            range of values defined by class `BurstSizeRange`. Default is 16.
+        burst_sz: Set the number of packets per burst. This can be set as a 
single value or
+            range of values defined by class `BurstSizeRange`. Default is 16.
+        cipher_algo: Set cipher algorithm name from class `CipherAlgorithm`.
+        cipher_iv_sz: set the size of cipher iv.
+        cipher_key_sz: set the size of cipher key.
+        cipher_op: set cipher operation mode from class `EncryptDecryptSwitch`.
+        csv_friendly: Enable test result output CSV friendly rather than human 
friendly.
+        desc_nb: set the number of descriptors for each crypto device.
+        devtype: Set the device name from class `DeviceType`.
+        digest_sz: set the size of digest.
+        docsis_hdr_sz: set DOCSIS header size(n) in bytes.
+        enable_sdap: enable service data adaptation protocol.
+        imix: Set the distribution of packet sizes. A list of weights must be 
passed, containing the
+            same number of items than buffer-sz, so each item in this list 
will be the weight of the
+            packet size on the same position in the buffer-sz parameter (a 
list has to be passed in
+            that parameter).
+        low_prio_qp_mask: set low priority queue pairs set in the hexadecimal 
mask. This is an
+            optional parameter, if not set all queue pairs will be on the same 
high priority.
+        modex_len: set modex length for asymmetric crypto perf test. Supported 
lengths are 60,
+            128, 255, 448.  Default length is 128.
+        optype: Set operation type from class `OpType`.
+        out_of_place: Enable out-of-place crypto operations mode.
+        pdcp_sn_sz:  set PDCP sequebce number size(n) in bits. Valid values of 
n are 5/7/12/15/18.
+        pdcp_domain: Set PDCP domain to specify short_mac/control/user plane 
from class
+            `PDCPDomain`.
+        pdcp_ses_hfn_en: enable fixed session based HFN instead of per packet 
HFN.
+        pmd_cyclecount_delay_pmd: Add a delay (in milliseconds) between 
enqueue and dequeue in
+            pmd-cyclecount benchmarking mode (useful when benchmarking 
hardware acceleration).
+        pool_sz: Set the number if mbufs to be allocated in the mbuf pool.
+        ptest: Set performance throughput test type from class `TestType`.
+        rsa_modlen: Set RSA modulus length (in bits) for asymmetric crypto 
perf test.
+            To be used with RSA asymmetric crypto ops.Supported lengths are 
1024, 2048, 4096, 8192.
+            Default length is 1024.
+        rsa_priv_keytype: set RSA private key type from class 
`RSAPrivKeytype`. To be used with RSA
+            asymmetric crypto ops.
+        segment_sz: Set the size of the segment to use, for Scatter Gather 
List testing. Use list of
+            values in buffer-sz in descending order if segment-sz is used. By 
default, it is set to
+            the size of the maximum buffer size, including the digest size, so 
a single segment is
+            created.
+        sessionless: Enable session-less crypto operations mode.
+        shared_session: Enable sharing sessions between all queue pairs on a 
single crypto PMD. This
+            can be useful for benchmarking this setup, or finding and 
debugging concurrency errors
+            that can occur while using sessions on multiple lcores 
simultaneously.
+        silent: Disable options dump.
+        test_file: Set test vector file path. See the Test Vector File chapter.
+        test_name: Set specific test name section in the test vector file.
+        tls_version: Set TLS/DTLS protocol version for perf test from class 
`TLSVersion`.
+            Default is TLS1.2.
+        total_ops: Set the number of total operations performed.
+    """
+
+    aead_aad_sz: int | None = field(default=None, 
metadata=Params.long("aead-aad-sz"))
+    aead_algo: AeadAlgName | None = field(default=None, 
metadata=Params.long("aead-algo"))
+    aead_iv_sz: int | None = field(default=None, 
metadata=Params.long("aead-iv-sz"))
+    aead_key_sz: int | None = field(default=None, 
metadata=Params.long("aead-key-sz"))
+    aead_op: EncryptDecryptSwitch | None = field(default=None, 
metadata=Params.long("aead-op"))
+    asym_op: AsymOpMode | None = field(default=None, 
metadata=Params.long("asym-op"))
+    auth_algo: AuthenticationAlgorithm | None = field(
+        default=None, metadata=Params.long("auth-algo")
+    )
+    auth_iv_sz: int | None = field(default=None, 
metadata=Params.long("auth-iv-sz"))
+    auth_key_sz: int | None = field(default=None, 
metadata=Params.long("auth-key-sz"))
+    auth_op: AuthenticationOpMode | None = field(default=None, 
metadata=Params.long("auth-op"))
+    buffer_sz: BurstSizeRange | ListWrapper | int | None = field(
+        default=None, metadata=Params.long("buffer-sz")
+    )
+    burst_sz: BurstSizeRange | ListWrapper | int | None = field(
+        default=None, metadata=Params.long("burst-sz")
+    )
+    cipher_algo: CipherAlgorithm | None = field(default=None, 
metadata=Params.long("cipher-algo"))
+    cipher_iv_sz: int | None = field(default=None, 
metadata=Params.long("cipher-iv-sz"))
+    cipher_key_sz: int | None = field(default=None, 
metadata=Params.long("cipher-key-sz"))
+    cipher_op: EncryptDecryptSwitch | None = field(default=None, 
metadata=Params.long("cipher-op"))
+    csv_friendly: Switch = field(default=None, 
metadata=Params.long("csv-friendly"))
+    desc_nb: int | None = field(default=None, metadata=Params.long("desc-nb"))
+    devtype: DeviceType = field(metadata=Params.long("devtype"))
+    digest_sz: int | None = field(default=None, 
metadata=Params.long("digest-sz"))
+    docsis_hdr_sz: int | None = field(default=None, 
metadata=Params.long("docsis-hdr-sz"))
+    enable_sdap: Switch = None
+    imix: int | None = field(default=None, metadata=Params.long("imix"))
+    low_prio_qp_mask: int | None = field(default=None, 
metadata=Params.convert_value(hex))
+    modex_len: int | None = field(default=None, 
metadata=Params.long("modex-len"))
+    optype: OperationType | None = field(default=None, 
metadata=Params.long("optype"))
+    out_of_place: Switch = None
+    pdcp_sn_sz: int | None = None
+    pdcp_domain: PDCPDomain | None = field(default=None, 
metadata=Params.long("pdcp-domain"))
+    pdcp_ses_hfn_en: Switch | None = field(default=None, 
metadata=Params.long("pdcp-ses-hfn-en"))
+    pmd_cyclecount_delay_pmd: int | None = field(
+        default=None, metadata=Params.long("pmd-cyclecount-delay-pmd")
+    )
+    pool_sz: int | None = field(default=None, metadata=Params.long("pool-sz"))
+    ptest: TestType = field(default=TestType.throughput, 
metadata=Params.long("ptest"))
+    rsa_modlen: int | None = field(default=None, 
metadata=Params.long("rsa-modlen"))
+    rsa_priv_keytype: RSAPrivKeytype | None = field(
+        default=None, metadata=Params.long("rsa-priv-keytype")
+    )
+    segment_sz: int | None = field(default=None, 
metadata=Params.long("segment-sz"))
+    sessionless: Switch = None
+    shared_session: Switch = None
+    silent: Silent | None = field(default="", metadata=Params.long("silent"))
+    test_file: str | None = field(default=None, 
metadata=Params.long("test-file"))
+    test_name: str | None = field(default=None, 
metadata=Params.long("test-name"))
+    tls_version: TLSVersion | None = field(default=None, 
metadata=Params.long("tls-version"))
+    total_ops: int | None = field(default=100000, 
metadata=Params.long("total-ops"))
diff --git a/dts/api/cryptodev/types.py b/dts/api/cryptodev/types.py
new file mode 100644
index 0000000000..df73a86fa4
--- /dev/null
+++ b/dts/api/cryptodev/types.py
@@ -0,0 +1,185 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2025 University of New Hampshire
+
+"""Cryptodev types module.
+
+Exposes types used in the Cryptodev API.
+"""
+
+from dataclasses import dataclass, field
+
+from framework.parser import TextParser
+
+
+@dataclass
+class CryptodevResults(TextParser):
+    """Base class for all cryptodev results."""
+
+    buffer_size: int
+
+    def __iter__(self):
+        """Iteration method to parse result objects.
+
+        Yields:
+            tuple[str, int | float]: a field name and its value.
+        """
+        for field_name in self.__dataclass_fields__:
+            yield field_name, getattr(self, field_name)
+
+
+@dataclass
+class ThroughputResults(CryptodevResults):
+    """A parser for throughput test output."""
+
+    #:
+    lcore_id: int = field(metadata=TextParser.find_int(r"\s*(\d+)"))
+    #: buffer size used in the run
+    buffer_size: int = field(
+        metadata=TextParser.find_int(r"\s+(?:\d+\s+)(\d+)"),
+    )
+    #: burst size used in the run
+    burst_size: int = field(
+        metadata=TextParser.find_int(r"\s+(?:\d+\s+){2}(\d+)"),
+    )
+    #: total packets enqueued
+    enqueued: int = 
field(metadata=TextParser.find_int(r"\s+(?:\d+\s+){3}(\d+)"))
+    #: total packets dequeued
+    dequeued: int = 
field(metadata=TextParser.find_int(r"\s+(?:\d+\s+){4}(\d+)"))
+    #: packets that failed enqueue
+    failed_enqueue: int = 
field(metadata=TextParser.find_int(r"\s+(?:\d+\s+){5}(\d+)"))
+    #: packets that failed dequeue
+    failed_dequeue: int = 
field(metadata=TextParser.find_int(r"\s+(?:\d+\s+){6}(\d+)"))
+    #: mega operations per second
+    mops: float = 
field(metadata=TextParser.find_float(r"\s+(?:\d+\s+){7}([\d.]+)"))
+    #: gigabits per second
+    gbps: float = 
field(metadata=TextParser.find_float(r"\s+(?:\d+\s+){7}(?:[\d.]+\s+)([\d.]+)"))
+    #: cpu cycles per buffer
+    cycles_per_buffer: float = field(
+        
metadata=TextParser.find_float(r"\s+(?:\d+\s+){7}(?:[\d.]+\s+){2}([\d.]+)")
+    )
+
+
+@dataclass
+class LatencyResults(CryptodevResults):
+    """A parser for latency test output."""
+
+    #: buffer size ran with app
+    buffer_size: int = field(
+        metadata=TextParser.find_int(r"Buf(?:.*\n\s+\d+\s+)?(?:fer 
size:\s+)?(\d+)"),
+    )
+    #: burst size ran with app
+    burst_size: int = field(
+        metadata=TextParser.find_int(rf"Burst(?:.*\n\s+\d+\s+){2}?(?: 
size:\s+)?(\d+)"),
+    )
+    #: total operations ran
+    total_ops: int = field(metadata=TextParser.find_int(r"total 
operations:\s+(\d+)"))
+    #: number of bursts
+    num_of_bursts: int = field(metadata=TextParser.find_int(r"Number of 
bursts:\s+(\d+)"))
+    #: minimum enqueued packets
+    min_enqueued: int = 
field(metadata=TextParser.find_int(r"enqueued\s+(?:\d+\s+){2}(\d+)"))
+    #: maximum enqueued packets
+    max_enqueued: int = 
field(metadata=TextParser.find_int(r"enqueued\s+(?:\d+\s+){3}(\d+)"))
+    #: average enqueued packets
+    avg_enqueued: int = 
field(metadata=TextParser.find_int(r"enqueued\s+(?:\d+\s+)(\d+)"))
+    #: total enqueued packets
+    total_enqueued: int = 
field(metadata=TextParser.find_int(r"enqueued\s+(\d+)"))
+    #: minimum dequeued packets
+    min_dequeued: int = 
field(metadata=TextParser.find_int(r"dequeued\s+(?:\d+\s+){2}(\d+)"))
+    #: maximum dequeued packets
+    max_dequeued: int = 
field(metadata=TextParser.find_int(r"dequeued\s+(?:\d+\s+){3}(\d+)"))
+    #: average dequeued packets
+    avg_dequeued: int = 
field(metadata=TextParser.find_int(r"dequeued\s+(?:\d+\s+)(\d+)"))
+    #: total dequeued packets
+    total_dequeued: int = 
field(metadata=TextParser.find_int(r"dequeued\s+(\d+)"))
+    #: minimum cycles per buffer
+    min_cycles: float = 
field(metadata=TextParser.find_float(r"cycles\s+(?:[\d.]+\s+){3}([\d.]+)"))
+    #: maximum cycles per buffer
+    max_cycles: float = 
field(metadata=TextParser.find_float(r"cycles\s+(?:[\d.]+\s+){2}([\d.]+)"))
+    #: average cycles per buffer
+    avg_cycles: float = 
field(metadata=TextParser.find_float(r"cycles\s+(?:[\d.]+\s+)([\d.]+)"))
+    #: total cycles per buffer
+    total_cycles: float = 
field(metadata=TextParser.find_float(r"cycles\s+([\d.]+)"))
+    #: mimum time in microseconds
+    min_time_us: float = field(
+        metadata=TextParser.find_float(r"time 
\[us\]\s+(?:[\d.]+\s+){3}([\d.]+)")
+    )
+    #: maximum time in microseconds
+    max_time_us: float = field(
+        metadata=TextParser.find_float(r"time 
\[us\]\s+(?:[\d.]+\s+){2}([\d.]+)")
+    )
+    #: average time in microseconds
+    avg_time_us: float = field(
+        metadata=TextParser.find_float(r"time \[us\]\s+(?:[\d.]+\s+)([\d.]+)")
+    )
+    #: total time in microseconds
+    total_time_us: float = field(metadata=TextParser.find_float(r"time 
\[us\]\s+([\d.]+)"))
+
+
+@dataclass
+class PmdCyclecountResults(CryptodevResults):
+    """A parser for PMD cycle count test output."""
+
+    #:
+    lcore_id: int = 
field(metadata=TextParser.find_int(r"lcore\s+(?:id.*\n\s+)?(\d+)"))
+    #: buffer size used with app run
+    buffer_size: int = field(
+        metadata=TextParser.find_int(r"Buf(?:.*\n\s+(?:\d+\s+))?(?:fer 
size:\s+)?(\d+)"),
+    )
+    #: burst size used with app run
+    burst_size: int = field(
+        metadata=TextParser.find_int(r"Burst(?:.*\n\s+(?:\d+\s+){2})?(?: 
size:\s+)?(\d+)"),
+    )
+    #: packets enqueued
+    enqueued: int = 
field(metadata=TextParser.find_int(r"Enqueued.*\n\s+(?:\d+\s+){3}(\d+)"))
+    #: packets dequeued
+    dequeued: int = 
field(metadata=TextParser.find_int(r"Dequeued.*\n\s+(?:\d+\s+){4}(\d+)"))
+    #: number of enqueue packet retries
+    enqueue_retries: int = field(
+        metadata=TextParser.find_int(r"Enq Retries.*\n\s+(?:\d+\s+){5}(\d+)")
+    )
+    #: number of dequeue packet retries
+    dequeue_retries: int = field(
+        metadata=TextParser.find_int(r"Deq Retries.*\n\s+(?:\d+\s+){6}(\d+)")
+    )
+    #: number of cycles per operation
+    cycles_per_operation: float = field(
+        
metadata=TextParser.find_float(r"Cycles/Op.*\n\s+(?:\d+\s+){7}([\d.]+)")
+    )
+    #: number of cycles per enqueue
+    cycles_per_enqueue: float = field(
+        
metadata=TextParser.find_float(r"Cycles/Enq.*\n\s+(?:\d+\s+){7}(?:[\d.]+\s+)([\d.]+)")
+    )
+    #: number of cycles per dequeue
+    cycles_per_dequeue: float = field(
+        
metadata=TextParser.find_float(r"Cycles/Deq.*\n\s+(?:\d+\s+){7}(?:[\d.]+\s+){2}([\d.]+)"),
+    )
+
+
+@dataclass
+class VerifyResults(CryptodevResults):
+    """A parser for verify test output."""
+
+    #:
+    lcore_id: int = 
field(metadata=TextParser.find_int(r"lcore\s+(?:id.*\n\s+)?(\d+)"))
+    #: buffer size ran with app
+    buffer_size: int = field(
+        metadata=TextParser.find_int(r"Buf(?:.*\n\s+(?:\d+\s+))?(?:fer 
size:\s+)?(\d+)"),
+    )
+    #: burst size ran with app
+    burst_size: int = field(
+        metadata=TextParser.find_int(r"Burst(?:.*\n\s+(?:\d+\s+){2})?(?: 
size:\s+)?(\d+)"),
+    )
+    #: number of packets enqueued
+    enqueued: int = 
field(metadata=TextParser.find_int(r"Enqueued.*\n\s+(?:\d+\s+){3}(\d+)"))
+    #: number of packets dequeued
+    dequeued: int = 
field(metadata=TextParser.find_int(r"Dequeued.*\n\s+(?:\d+\s+){4}(\d+)"))
+    #: number of packets enqueue failed
+    failed_enqueued: int = field(
+        metadata=TextParser.find_int(r"Failed Enq.*\n\s+(?:\d+\s+){5}(\d+)")
+    )
+    #: number of packets dequeue failed
+    failed_dequeued: int = field(
+        metadata=TextParser.find_int(r"Failed Deq.*\n\s+(?:\d+\s+){6}(\d+)")
+    )
+    #: total number of failed operations
+    failed_ops: int = field(metadata=TextParser.find_int(r"Failed 
Ops.*\n\s+(?:\d+\s+){7}(\d+)"))
diff --git a/dts/framework/config/node.py b/dts/framework/config/node.py
index 438a1bdc8f..cc1ba9b664 100644
--- a/dts/framework/config/node.py
+++ b/dts/framework/config/node.py
@@ -70,6 +70,8 @@ class NodeConfiguration(FrozenModel):
     hugepages: HugepageConfiguration | None = Field(None, 
alias="hugepages_2mb")
     #: The ports that can be used in testing.
     ports: list[PortConfig] = Field(min_length=1)
+    #: The pci info used by crypto devices
+    cryptodevs: list[PortConfig] = Field(default=[], min_length=0)
 
     @model_validator(mode="after")
     def verify_unique_port_names(self) -> Self:
diff --git a/dts/framework/params/types.py b/dts/framework/params/types.py
index 5bc4bd37d9..3c7650474c 100644
--- a/dts/framework/params/types.py
+++ b/dts/framework/params/types.py
@@ -15,6 +15,23 @@ def create_testpmd(**kwargs: Unpack[TestPmdParamsDict]):
 from pathlib import PurePath
 from typing import TypedDict
 
+from api.cryptodev.config import (
+    AeadAlgName,
+    AsymOpMode,
+    AuthenticationAlgorithm,
+    AuthenticationOpMode,
+    BurstSizeRange,
+    CipherAlgorithm,
+    DeviceType,
+    EncryptDecryptSwitch,
+    ListWrapper,
+    OperationType,
+    PDCPDomain,
+    RSAPrivKeytype,
+    Silent,
+    TestType,
+    TLSVersion,
+)
 from api.testpmd.config import (
     AnonMempoolAllocationMode,
     EthPeer,
@@ -55,6 +72,54 @@ class EalParamsDict(TypedDict, total=False):
     other_eal_param: Params | None
 
 
+class CryptoPmdParamsDict(EalParamsDict, total=False):
+    """:class:`TypedDict` equivalent of 
:class:`~.cryptodev.CryptoPmdParams`."""
+
+    aead_aad_sz: int | None
+    aead_algo: AeadAlgName | None
+    aead_iv_sz: int | None
+    aead_key_sz: int | None
+    aead_op: EncryptDecryptSwitch | None
+    asym_op: AsymOpMode | None
+    auth_algo: AuthenticationAlgorithm | None
+    auth_iv_sz: int | None
+    auth_key_sz: int | None
+    auth_op: AuthenticationOpMode | None
+    buffer_sz: BurstSizeRange | ListWrapper | int | None
+    burst_sz: BurstSizeRange | ListWrapper | int | None
+    cipher_algo: CipherAlgorithm | None
+    cipher_iv_sz: int | None
+    cipher_key_sz: int | None
+    cipher_op: EncryptDecryptSwitch | None
+    csv_friendly: Switch
+    desc_nb: int | None
+    devtype: DeviceType | None
+    digest_sz: int | None
+    docsis_hdr_sz: int | None
+    enable_sdap: Switch
+    imix: int | None
+    low_prio_qp_mask: int | None
+    modex_len: int | None
+    optype: OperationType | None
+    out_of_place: Switch
+    pdcp_sn_sz: int | None
+    pdcp_domain: PDCPDomain | None
+    pdcp_ses_hfn_en: Switch | None
+    pmd_cyclecount_delay_pmd: int | None
+    pool_sz: int | None
+    ptest: TestType
+    rsa_modlen: int | None
+    rsa_priv_keytype: RSAPrivKeytype | None
+    segment_sz: int | None
+    sessionless: Switch
+    shared_session: Switch
+    silent: Silent | None
+    test_file: str | None
+    test_name: str | None
+    tls_version: TLSVersion | None
+    total_ops: int | None
+
+
 class TestPmdParamsDict(EalParamsDict, total=False):
     """:class:`TypedDict` equivalent of :class:`~.testpmd.TestPmdParams`."""
 
diff --git a/dts/framework/remote_session/dpdk_shell.py 
b/dts/framework/remote_session/dpdk_shell.py
index 51b97d4ff6..b94d336d4e 100644
--- a/dts/framework/remote_session/dpdk_shell.py
+++ b/dts/framework/remote_session/dpdk_shell.py
@@ -46,7 +46,10 @@ def compute_eal_params(
     params.prefix = prefix
 
     if params.allowed_ports is None:
-        params.allowed_ports = ctx.topology.sut_dpdk_ports
+        if ctx.topology.crypto_vf_ports:
+            params.allowed_ports = [ctx.topology.crypto_vf_ports[0]]
+        else:
+            params.allowed_ports = ctx.topology.sut_dpdk_ports
 
     return params
 
diff --git a/dts/framework/test_run.py b/dts/framework/test_run.py
index ff0a12c9ce..ada628c59e 100644
--- a/dts/framework/test_run.py
+++ b/dts/framework/test_run.py
@@ -349,6 +349,9 @@ def next(self) -> State | None:
 
         if test_run.config.use_virtual_functions:
             test_run.ctx.topology.instantiate_vf_ports()
+        if test_run.ctx.sut_node.cryptodevs and test_run.config.crypto:
+            test_run.ctx.topology.instantiate_crypto_vf_ports()
+            test_run.ctx.topology.configure_cryptodevs("dpdk")
 
         test_run.ctx.topology.configure_ports("sut", "dpdk")
         if test_run.ctx.func_tg:
@@ -442,6 +445,8 @@ def next(self) -> State | None:
         """Next state."""
         if self.test_run.config.use_virtual_functions:
             self.test_run.ctx.topology.delete_vf_ports()
+        if self.test_run.ctx.sut_node.cryptodevs:
+            self.test_run.ctx.topology.delete_crypto_vf_ports()
 
         self.test_run.ctx.shell_pool.terminate_current_pool()
         if self.test_run.ctx.func_tg and self.test_run.ctx.func_tg.is_setup:
diff --git a/dts/framework/test_suite.py b/dts/framework/test_suite.py
index 9c57e343ac..856dae0a85 100644
--- a/dts/framework/test_suite.py
+++ b/dts/framework/test_suite.py
@@ -174,6 +174,8 @@ def filter_test_cases(
                     perf_test_cases.add(test_case)
                 case TestCaseType.FUNCTIONAL:
                     func_test_cases.add(test_case)
+                case TestCaseType.CRYPTO:
+                    pass
 
         if test_case_sublist_copy:
             raise ConfigurationError(
diff --git a/dts/framework/testbed_model/linux_session.py 
b/dts/framework/testbed_model/linux_session.py
index 711d4d97c3..fb6081ab37 100644
--- a/dts/framework/testbed_model/linux_session.py
+++ b/dts/framework/testbed_model/linux_session.py
@@ -10,6 +10,7 @@
 """
 
 import json
+import re
 from collections.abc import Iterable
 from functools import cached_property
 from pathlib import PurePath
@@ -193,7 +194,8 @@ def bind_ports_to_driver(self, ports: list[Port], 
driver_name: str) -> None:
             verify=True,
         )
 
-        del self._lshw_net_info
+        if self._lshw_net_info:
+            del self._lshw_net_info
 
     def bring_up_link(self, ports: Iterable[Port]) -> None:
         """Overrides :meth:`~.os_session.OSSession.bring_up_link`."""
@@ -223,6 +225,57 @@ def devbind_script_path(self) -> PurePath:
         """
         raise InternalError("Accessed devbind script path before setup.")
 
+    def load_vfio(self, pf_port: Port) -> None:
+        """Overrides :meth:'~os_session.OSSession,load_vfio`."""
+        cmd_result = self.send_command(f"lspci -nn -s {pf_port.pci}")
+        device = re.search(r":([0-9a-fA-F]{4})\]", cmd_result.stdout)
+        if device and device.group(1) in ["37c8", "0435", "19e2"]:
+            self.send_command(
+                "modprobe -r vfio_iommu_type1; modprobe -r vfio_pci",
+                privileged=True,
+            )
+            self.send_command(
+                "modprobe -r vfio_virqfd; modprobe -r vfio",
+                privileged=True,
+            )
+            self.send_command(
+                "modprobe vfio-pci disable_denylist=1 enable_sriov=1", 
privileged=True
+            )
+            self.send_command(
+                "echo 1 | tee 
/sys/module/vfio/parameters/enable_unsafe_noiommu_mode",
+                privileged=True,
+            )
+        else:
+            self.send_command("modprobe vfio-pci")
+        self.refresh_lshw()
+
+    def create_crypto_vfs(self, pf_port: list[Port]) -> None:
+        """Overrides :meth:`~os_session.OSSession.create_crypto_vfs`.
+
+        Raises:
+            InternalError: If there are existing VFs which have to be deleted.
+        """
+        for port in pf_port:
+            self.delete_crypto_vfs(port)
+        for port in pf_port:
+            sys_bus_path = 
f"/sys/bus/pci/drivers/{port.config.os_driver}/{port.pci}".replace(
+                ":", "\\:"
+            )
+            curr_num_vfs = int(
+                self.send_command(f"cat {sys_bus_path}/sriov_numvfs", 
privileged=True).stdout
+            )
+            if 0 < curr_num_vfs:
+                raise InternalError("There are existing VFs on the port which 
must be deleted.")
+            num_vfs = int(
+                self.send_command(f"cat {sys_bus_path}/sriov_totalvfs", 
privileged=True).stdout
+            )
+            self.send_command(
+                f"echo {num_vfs} | sudo tee {sys_bus_path}/sriov_numvfs", 
privileged=True
+            )
+
+        # self.send_command(f"modprobe {driver}", privileged=True)
+        self.refresh_lshw()
+
     def create_vfs(self, pf_port: Port) -> None:
         """Overrides :meth:`~.os_session.OSSession.create_vfs`.
 
@@ -239,6 +292,28 @@ def create_vfs(self, pf_port: Port) -> None:
             self.send_command(f"echo 1 | sudo tee 
{sys_bus_path}/sriov_numvfs", privileged=True)
             self.refresh_lshw()
 
+    def delete_crypto_vfs(self, pf_port: Port) -> None:
+        """Overrides :meth:`~.os_session.OSSession.delete_crypto_vfs`."""
+        sys_bus_path = 
f"/sys/bus/pci/drivers/{pf_port.config.os_driver}/{pf_port.pci}".replace(
+            ":", "\\:"
+        )
+        try:
+            curr_num_vfs = int(
+                self.send_command(f"cat {sys_bus_path}/sriov_numvfs", 
privileged=True).stdout
+            )
+            if curr_num_vfs == 0:
+                return self._logger.debug(f"No VFs found on port 
{pf_port.pci}, skipping deletion")
+        except ValueError:
+            pass
+        self.send_command(
+            f"dpdk-devbind.py -u {pf_port.pci}".replace(":", "\\:"), 
privileged=True, timeout=30
+        )
+        self.send_command(
+            f"echo 1 | sudo tee 
/sys/bus/pci/devices/{pf_port.pci}/remove".replace(":", "\\:"),
+            privileged=True,
+        )
+        self.send_command("echo 1 | sudo tee /sys/bus/pci/rescan", 
privileged=True)
+
     def delete_vfs(self, pf_port: Port) -> None:
         """Overrides :meth:`~.os_session.OSSession.delete_vfs`."""
         sys_bus_path = f"/sys/bus/pci/devices/{pf_port.pci}".replace(":", 
"\\:")
@@ -250,6 +325,20 @@ def delete_vfs(self, pf_port: Port) -> None:
         else:
             self.send_command(f"echo 0 | sudo tee 
{sys_bus_path}/sriov_numvfs", privileged=True)
 
+    def get_pci_addr_of_crypto_vfs(self, pf_port: Port) -> list[str]:
+        """Overrides 
:meth:`~.os_session.OSSession.get_pci_addr_of_crypto_vfs`."""
+        sys_bus_path = 
f"/sys/bus/pci/drivers/{pf_port.config.os_driver}/{pf_port.pci}".replace(
+            ":", "\\:"
+        )
+        curr_num_vfs = int(self.send_command(f"cat 
{sys_bus_path}/sriov_numvfs").stdout)
+        if curr_num_vfs > 0:
+            pci_addrs = self.send_command(
+                f"readlink {sys_bus_path}/virtfn*",
+                privileged=True,
+            )
+            return [pci.replace("../", "") for pci in 
pci_addrs.stdout.splitlines()]
+        return []
+
     def get_pci_addr_of_vfs(self, pf_port: Port) -> list[str]:
         """Overrides :meth:`~.os_session.OSSession.get_pci_addr_of_vfs`."""
         sys_bus_path = f"/sys/bus/pci/devices/{pf_port.pci}".replace(":", 
"\\:")
diff --git a/dts/framework/testbed_model/node.py 
b/dts/framework/testbed_model/node.py
index 6c3e63a2a1..a4c1f8f22c 100644
--- a/dts/framework/testbed_model/node.py
+++ b/dts/framework/testbed_model/node.py
@@ -54,6 +54,7 @@ class Node:
     arch: Architecture
     lcores: list[LogicalCore]
     ports: list[Port]
+    cryptodevs: list[Port]
     _logger: DTSLogger
     _other_sessions: list[OSSession]
     _node_info: OSSessionInfo | None
@@ -82,6 +83,9 @@ def __init__(self, node_config: NodeConfiguration) -> None:
         self._other_sessions = []
         self._setup = False
         self.ports = [Port(self, port_config) for port_config in 
self.config.ports]
+        self.cryptodevs = [
+            Port(self, cryptodev_config) for cryptodev_config in 
self.config.cryptodevs
+        ]
         self._logger.info(f"Created node: {self.name}")
 
     def setup(self) -> None:
diff --git a/dts/framework/testbed_model/os_session.py 
b/dts/framework/testbed_model/os_session.py
index b94c3e527b..4a4fc1f34a 100644
--- a/dts/framework/testbed_model/os_session.py
+++ b/dts/framework/testbed_model/os_session.py
@@ -615,6 +615,18 @@ def configure_port_mtu(self, mtu: int, port: Port) -> None:
             port: Port to set `mtu` on.
         """
 
+    @abstractmethod
+    def load_vfio(self, pf_port: Port) -> None:
+        """Load the vfio module according to the device type of the given 
port."""
+
+    @abstractmethod
+    def create_crypto_vfs(self, pf_ports: list[Port]) -> None:
+        """Creatues virtual functions for each port in 'pf_ports'.
+
+        Checks how many virtual functions each crypto device in 'pf_ports' 
supports, and creates
+            that number of VFs on the port.
+        """
+
     @abstractmethod
     def create_vfs(self, pf_port: Port) -> None:
         """Creates virtual functions for `pf_port`.
@@ -630,6 +642,16 @@ def create_vfs(self, pf_port: Port) -> None:
             maximum for `pf_port`.
         """
 
+    @abstractmethod
+    def delete_crypto_vfs(self, pf_port: Port) -> None:
+        """Deletes virtual functions for crypto device 'pf_port'.
+
+        Checks how many virtual functions are currently active and removes all 
if any exist.
+
+        Args:
+            pf_port: The crypto device port to delete virtual functions on.
+        """
+
     @abstractmethod
     def delete_vfs(self, pf_port: Port) -> None:
         """Deletes virtual functions for `pf_port`.
@@ -656,3 +678,15 @@ def get_pci_addr_of_vfs(self, pf_port: Port) -> list[str]:
             A list containing all of the PCI addresses of the VFs on the port. 
If the port has no
             VFs then the list will be empty.
         """
+
+    @abstractmethod
+    def get_pci_addr_of_crypto_vfs(self, pf_port: Port) -> list[str]:
+        """Find the PCI addresses of all virtual functions (VFs) of a crypto 
device on `pf_port`.
+
+        Args:
+            pf_port: The port to find the VFs on.
+
+        Returns:
+            A list containing all of the PCI addresses of the VFs on the port. 
If the port has no
+            VFs then the list will be empty.
+        """
diff --git a/dts/framework/testbed_model/topology.py 
b/dts/framework/testbed_model/topology.py
index 13b4b58a74..10805f8b48 100644
--- a/dts/framework/testbed_model/topology.py
+++ b/dts/framework/testbed_model/topology.py
@@ -8,6 +8,7 @@
 The link information then implies what type of topology is available.
 """
 
+import re
 from collections import defaultdict
 from collections.abc import Iterator
 from dataclasses import dataclass
@@ -58,6 +59,8 @@ class Topology:
     tg_ports: list[Port]
     pf_ports: list[Port]
     vf_ports: list[Port]
+    crypto_pf_ports: list[Port]
+    crypto_vf_ports: list[Port]
 
     @classmethod
     def from_port_links(cls, port_links: Iterator[PortLink]) -> Self:
@@ -85,7 +88,7 @@ def from_port_links(cls, port_links: Iterator[PortLink]) -> 
Self:
                     msg = "More than two links in a topology are not 
supported."
                     raise ConfigurationError(msg)
 
-        return cls(type, sut_ports, tg_ports, [], [])
+        return cls(type, sut_ports, tg_ports, [], [], [], [])
 
     def node_and_ports_from_id(self, node_identifier: NodeIdentifier) -> 
tuple[Node, list[Port]]:
         """Retrieve node and its ports for the current topology.
@@ -105,6 +108,40 @@ def node_and_ports_from_id(self, node_identifier: 
NodeIdentifier) -> tuple[Node,
                 msg = f"Invalid node `{node_identifier}` given."
                 raise InternalError(msg)
 
+    def get_crypto_vfs(self, numvfs: int | None = None) -> list[Port]:
+        """Retrieve virtual functions from active crypto vfs.
+
+        If numvfs is `None`, returns all crypto virtual functions. Otherwise, 
returns
+        `numvfs` number of virtual functions per physical function rounded 
down. If a number of
+        `numvfs` is less than the number of physical functions, one virtual 
function is returned.
+
+        Args:
+            numvfs: Number of virtual functions to return if provided.
+
+        Returns:
+            List containing the requested number of vfs, evenly distributed 
among
+                physcal functions rounded down.
+        """
+        return_list = []
+        device = 1
+        if numvfs is None:
+            print("returning all")
+            return_list.extend(self.crypto_vf_ports)
+            return return_list
+        mod = numvfs // len(self.crypto_pf_ports)
+        if numvfs < len(self.crypto_pf_ports):
+            print("Returning first")
+            return_list.extend([self.crypto_vf_ports[0]])
+            return return_list
+        for i in range(mod):
+            if i % 8 == 0 and i != 0:
+                device += 1
+            return_list.extend(
+                filter(lambda x: re.search(rf"0000:\d+:0{device}.{i}", x.pci), 
self.crypto_vf_ports)
+            )
+        print(f"Returning: {return_list}")
+        return return_list
+
     def setup(self) -> None:
         """Setup topology ports.
 
@@ -145,6 +182,34 @@ def _setup_ports(self, node_identifier: NodeIdentifier) -> 
None:
                     f"for port {port.name} in node {node.name}."
                 )
 
+    def instantiate_crypto_vf_ports(self) -> None:
+        """Create max number of virtual functions allowed on the SUT node.
+
+        Raises:
+            InternalError: If crypto virtual functions could not be created on 
a port.
+        """
+        from framework.context import get_ctx
+
+        ctx = get_ctx()
+
+        for port in ctx.sut_node.cryptodevs:
+            self.crypto_pf_ports.append(port)
+        self.delete_crypto_vf_ports()
+
+        ctx.sut_node.main_session.create_crypto_vfs(self.crypto_pf_ports)
+        for port in self.crypto_pf_ports:
+            addr_list = 
ctx.sut_node.main_session.get_pci_addr_of_crypto_vfs(port)
+            if addr_list == []:
+                raise InternalError(f"Failed to create crypto virtual function 
on port {port.pci}")
+            for addr in addr_list:
+                vf_config = PortConfig(
+                    name=f"{port.name}-crypto-vf-{addr}",
+                    pci=addr,
+                    os_driver_for_dpdk=port.config.os_driver_for_dpdk,
+                    os_driver=port.config.os_driver,
+                )
+                self.crypto_vf_ports.append(Port(node=port.node, 
config=vf_config))
+
     def instantiate_vf_ports(self) -> None:
         """Create, setup, and add virtual functions to the list of ports on 
the SUT node.
 
@@ -189,6 +254,27 @@ def delete_vf_ports(self) -> None:
         self.sut_ports.clear()
         self.sut_ports.extend(self.pf_ports)
 
+    def delete_crypto_vf_ports(self) -> None:
+        """Delete crypto virtual functions from the SUT node during test run 
teardown."""
+        from framework.context import get_ctx
+
+        ctx = get_ctx()
+
+        for port in self.crypto_pf_ports:
+            ctx.sut_node.main_session.delete_crypto_vfs(port)
+        self.sut_ports.clear()
+        self.sut_ports.extend(self.pf_ports)
+
+    def configure_cryptodevs(self, driver: DriverKind):
+        """Configure the crypto device virtual functoins on the sut and bind 
to specified driver.
+
+        Args:
+            driver: The driver to bind the cryptofunctions
+        """
+        from framework.context import get_ctx
+
+        self._bind_ports_to_drivers(get_ctx().sut_node, self.crypto_vf_ports, 
driver)
+
     def configure_ports(
         self, node_identifier: NodeIdentifier, drivers: DriverKind | 
tuple[DriverKind, ...]
     ) -> None:
@@ -227,7 +313,7 @@ def _bind_ports_to_drivers(
         for port_id, port in enumerate(ports):
             driver_kind = drivers[port_id] if isinstance(drivers, tuple) else 
drivers
             desired_driver = port.driver_by_kind(driver_kind)
-            if port.current_driver != desired_driver:
+            if port in self.crypto_vf_ports or port.current_driver != 
desired_driver:
                 driver_to_ports[desired_driver].append(port)
 
         for driver_name, ports in driver_to_ports.items():
-- 
2.50.1

Reply via email to