178534506SJuraj Linkeš# SPDX-License-Identifier: BSD-3-Clause 278534506SJuraj Linkeš# Copyright(c) 2010-2014 Intel Corporation 378534506SJuraj Linkeš# Copyright(c) 2023 PANTHEON.tech s.r.o. 488489c05SJeremy Spewock# Copyright(c) 2023 University of New Hampshire 5fd8cd8eeSLuca Vizzarro# Copyright(c) 2024 Arm Limited 678534506SJuraj Linkeš 76ef07151SJuraj Linkeš"""System under test (DPDK + hardware) node. 86ef07151SJuraj Linkeš 96ef07151SJuraj LinkešA system under test (SUT) is the combination of DPDK 106ef07151SJuraj Linkešand the hardware we're testing with DPDK (NICs, crypto and other devices). 116ef07151SJuraj LinkešAn SUT node is where this SUT runs. 126ef07151SJuraj Linkeš""" 136ef07151SJuraj Linkeš 146ef07151SJuraj Linkeš 15680d8a24SJuraj Linkešimport os 16680d8a24SJuraj Linkešimport tarfile 17c020b7ceSJuraj Linkešimport time 18680d8a24SJuraj Linkešfrom pathlib import PurePath 19680d8a24SJuraj Linkeš 2088489c05SJeremy Spewockfrom framework.config import ( 2188489c05SJeremy Spewock BuildTargetConfiguration, 2288489c05SJeremy Spewock BuildTargetInfo, 2388489c05SJeremy Spewock NodeInfo, 24634bed13SJuraj Linkeš SutNodeConfiguration, 25a24f9604SJuraj Linkeš TestRunConfiguration, 2688489c05SJeremy Spewock) 27967fc62bSLuca Vizzarrofrom framework.params.eal import EalParams 282b2f5a8aSLuca Vizzarrofrom framework.remote_session.remote_session import CommandResult 29680d8a24SJuraj Linkešfrom framework.settings import SETTINGS 30b8bdc4c5SJuraj Linkešfrom framework.utils import MesonArgs 31680d8a24SJuraj Linkeš 3278534506SJuraj Linkešfrom .node import Node 33*bfad0948SLuca Vizzarrofrom .os_session import OSSession 34840b1e01SJuraj Linkešfrom .virtual_device import VirtualDevice 3578534506SJuraj Linkeš 3678534506SJuraj Linkeš 3778534506SJuraj Linkešclass SutNode(Node): 386ef07151SJuraj Linkeš """The system under test node. 396ef07151SJuraj Linkeš 406ef07151SJuraj Linkeš The SUT node extends :class:`Node` with DPDK specific features: 416ef07151SJuraj Linkeš 426ef07151SJuraj Linkeš * DPDK build, 436ef07151SJuraj Linkeš * Gathering of DPDK build info, 446ef07151SJuraj Linkeš * The running of DPDK apps, interactively or one-time execution, 456ef07151SJuraj Linkeš * DPDK apps cleanup. 466ef07151SJuraj Linkeš 476ef07151SJuraj Linkeš The :option:`--tarball` command line argument and the :envvar:`DTS_DPDK_TARBALL` 486ef07151SJuraj Linkeš environment variable configure the path to the DPDK tarball 496ef07151SJuraj Linkeš or the git commit ID, tag ID or tree ID to test. 506ef07151SJuraj Linkeš 516ef07151SJuraj Linkeš Attributes: 52a24f9604SJuraj Linkeš config: The SUT node configuration. 53a24f9604SJuraj Linkeš virtual_devices: The virtual devices used on the node. 5478534506SJuraj Linkeš """ 55680d8a24SJuraj Linkeš 56634bed13SJuraj Linkeš config: SutNodeConfiguration 57a24f9604SJuraj Linkeš virtual_devices: list[VirtualDevice] 58*bfad0948SLuca Vizzarro dpdk_prefix_list: list[str] 59*bfad0948SLuca Vizzarro dpdk_timestamp: str 60680d8a24SJuraj Linkeš _build_target_config: BuildTargetConfiguration | None 61b8bdc4c5SJuraj Linkeš _env_vars: dict 62680d8a24SJuraj Linkeš _remote_tmp_dir: PurePath 63680d8a24SJuraj Linkeš __remote_dpdk_dir: PurePath | None 64680d8a24SJuraj Linkeš _app_compile_timeout: float 65c020b7ceSJuraj Linkeš _dpdk_kill_session: OSSession | None 6688489c05SJeremy Spewock _dpdk_version: str | None 6788489c05SJeremy Spewock _node_info: NodeInfo | None 6888489c05SJeremy Spewock _compiler_version: str | None 6968010b2aSJeremy Spewock _path_to_devbind_script: PurePath | None 70680d8a24SJuraj Linkeš 71634bed13SJuraj Linkeš def __init__(self, node_config: SutNodeConfiguration): 726ef07151SJuraj Linkeš """Extend the constructor with SUT node specifics. 736ef07151SJuraj Linkeš 746ef07151SJuraj Linkeš Args: 756ef07151SJuraj Linkeš node_config: The SUT node's test run configuration. 766ef07151SJuraj Linkeš """ 77f614e737SJuraj Linkeš super().__init__(node_config) 78a24f9604SJuraj Linkeš self.virtual_devices = [] 79*bfad0948SLuca Vizzarro self.dpdk_prefix_list = [] 80680d8a24SJuraj Linkeš self._build_target_config = None 81b8bdc4c5SJuraj Linkeš self._env_vars = {} 82680d8a24SJuraj Linkeš self._remote_tmp_dir = self.main_session.get_remote_tmp_dir() 83680d8a24SJuraj Linkeš self.__remote_dpdk_dir = None 84680d8a24SJuraj Linkeš self._app_compile_timeout = 90 85c020b7ceSJuraj Linkeš self._dpdk_kill_session = None 86*bfad0948SLuca Vizzarro self.dpdk_timestamp = ( 87c020b7ceSJuraj Linkeš f"{str(os.getpid())}_{time.strftime('%Y%m%d%H%M%S', time.localtime())}" 88c020b7ceSJuraj Linkeš ) 8988489c05SJeremy Spewock self._dpdk_version = None 9088489c05SJeremy Spewock self._node_info = None 9188489c05SJeremy Spewock self._compiler_version = None 9268010b2aSJeremy Spewock self._path_to_devbind_script = None 93cecfe0aaSJuraj Linkeš self._logger.info(f"Created node: {self.name}") 94680d8a24SJuraj Linkeš 95680d8a24SJuraj Linkeš @property 96680d8a24SJuraj Linkeš def _remote_dpdk_dir(self) -> PurePath: 976ef07151SJuraj Linkeš """The remote DPDK dir. 986ef07151SJuraj Linkeš 996ef07151SJuraj Linkeš This internal property should be set after extracting the DPDK tarball. If it's not set, 1006ef07151SJuraj Linkeš that implies the DPDK setup step has been skipped, in which case we can guess where 1016ef07151SJuraj Linkeš a previous build was located. 1026ef07151SJuraj Linkeš """ 103680d8a24SJuraj Linkeš if self.__remote_dpdk_dir is None: 104680d8a24SJuraj Linkeš self.__remote_dpdk_dir = self._guess_dpdk_remote_dir() 105680d8a24SJuraj Linkeš return self.__remote_dpdk_dir 106680d8a24SJuraj Linkeš 107680d8a24SJuraj Linkeš @_remote_dpdk_dir.setter 108680d8a24SJuraj Linkeš def _remote_dpdk_dir(self, value: PurePath) -> None: 109680d8a24SJuraj Linkeš self.__remote_dpdk_dir = value 110680d8a24SJuraj Linkeš 111680d8a24SJuraj Linkeš @property 112680d8a24SJuraj Linkeš def remote_dpdk_build_dir(self) -> PurePath: 1136ef07151SJuraj Linkeš """The remote DPDK build directory. 1146ef07151SJuraj Linkeš 1156ef07151SJuraj Linkeš This is the directory where DPDK was built. 1166ef07151SJuraj Linkeš We assume it was built in a subdirectory of the extracted tarball. 1176ef07151SJuraj Linkeš """ 118680d8a24SJuraj Linkeš if self._build_target_config: 119680d8a24SJuraj Linkeš return self.main_session.join_remote_path( 120680d8a24SJuraj Linkeš self._remote_dpdk_dir, self._build_target_config.name 121680d8a24SJuraj Linkeš ) 122680d8a24SJuraj Linkeš else: 123680d8a24SJuraj Linkeš return self.main_session.join_remote_path(self._remote_dpdk_dir, "build") 124680d8a24SJuraj Linkeš 125680d8a24SJuraj Linkeš @property 126680d8a24SJuraj Linkeš def dpdk_version(self) -> str: 1276ef07151SJuraj Linkeš """Last built DPDK version.""" 128680d8a24SJuraj Linkeš if self._dpdk_version is None: 129517b4b26SJuraj Linkeš self._dpdk_version = self.main_session.get_dpdk_version(self._remote_dpdk_dir) 130680d8a24SJuraj Linkeš return self._dpdk_version 131680d8a24SJuraj Linkeš 13288489c05SJeremy Spewock @property 13388489c05SJeremy Spewock def node_info(self) -> NodeInfo: 1346ef07151SJuraj Linkeš """Additional node information.""" 13588489c05SJeremy Spewock if self._node_info is None: 13688489c05SJeremy Spewock self._node_info = self.main_session.get_node_info() 13788489c05SJeremy Spewock return self._node_info 13888489c05SJeremy Spewock 13988489c05SJeremy Spewock @property 14088489c05SJeremy Spewock def compiler_version(self) -> str: 1416ef07151SJuraj Linkeš """The node's compiler version.""" 14288489c05SJeremy Spewock if self._compiler_version is None: 14388489c05SJeremy Spewock if self._build_target_config is not None: 14488489c05SJeremy Spewock self._compiler_version = self.main_session.get_compiler_version( 14588489c05SJeremy Spewock self._build_target_config.compiler.name 14688489c05SJeremy Spewock ) 14788489c05SJeremy Spewock else: 14888489c05SJeremy Spewock self._logger.warning( 149517b4b26SJuraj Linkeš "Failed to get compiler version because _build_target_config is None." 15088489c05SJeremy Spewock ) 15188489c05SJeremy Spewock return "" 15288489c05SJeremy Spewock return self._compiler_version 15388489c05SJeremy Spewock 15468010b2aSJeremy Spewock @property 15568010b2aSJeremy Spewock def path_to_devbind_script(self) -> PurePath: 1566ef07151SJuraj Linkeš """The path to the dpdk-devbind.py script on the node.""" 15768010b2aSJeremy Spewock if self._path_to_devbind_script is None: 15868010b2aSJeremy Spewock self._path_to_devbind_script = self.main_session.join_remote_path( 15968010b2aSJeremy Spewock self._remote_dpdk_dir, "usertools", "dpdk-devbind.py" 16068010b2aSJeremy Spewock ) 16168010b2aSJeremy Spewock return self._path_to_devbind_script 16268010b2aSJeremy Spewock 16388489c05SJeremy Spewock def get_build_target_info(self) -> BuildTargetInfo: 1646ef07151SJuraj Linkeš """Get additional build target information. 1656ef07151SJuraj Linkeš 1666ef07151SJuraj Linkeš Returns: 1676ef07151SJuraj Linkeš The build target information, 1686ef07151SJuraj Linkeš """ 16988489c05SJeremy Spewock return BuildTargetInfo( 17088489c05SJeremy Spewock dpdk_version=self.dpdk_version, compiler_version=self.compiler_version 17188489c05SJeremy Spewock ) 17288489c05SJeremy Spewock 173680d8a24SJuraj Linkeš def _guess_dpdk_remote_dir(self) -> PurePath: 174680d8a24SJuraj Linkeš return self.main_session.guess_dpdk_remote_dir(self._remote_tmp_dir) 175680d8a24SJuraj Linkeš 176a24f9604SJuraj Linkeš def set_up_test_run(self, test_run_config: TestRunConfiguration) -> None: 177a24f9604SJuraj Linkeš """Extend the test run setup with vdev config. 1786ef07151SJuraj Linkeš 179a24f9604SJuraj Linkeš Args: 180a24f9604SJuraj Linkeš test_run_config: A test run configuration according to which 181a24f9604SJuraj Linkeš the setup steps will be taken. 182680d8a24SJuraj Linkeš """ 183a24f9604SJuraj Linkeš super().set_up_test_run(test_run_config) 184a24f9604SJuraj Linkeš for vdev in test_run_config.vdevs: 185a24f9604SJuraj Linkeš self.virtual_devices.append(VirtualDevice(vdev)) 186a24f9604SJuraj Linkeš 187a24f9604SJuraj Linkeš def tear_down_test_run(self) -> None: 188a24f9604SJuraj Linkeš """Extend the test run teardown with virtual device teardown.""" 189a24f9604SJuraj Linkeš super().tear_down_test_run() 190a24f9604SJuraj Linkeš self.virtual_devices = [] 191a24f9604SJuraj Linkeš 192a24f9604SJuraj Linkeš def set_up_build_target(self, build_target_config: BuildTargetConfiguration) -> None: 193a24f9604SJuraj Linkeš """Set up DPDK the SUT node and bind ports. 194a24f9604SJuraj Linkeš 195a24f9604SJuraj Linkeš DPDK setup includes setting all internals needed for the build, the copying of DPDK tarball 196a24f9604SJuraj Linkeš and then building DPDK. The drivers are bound to those that DPDK needs. 197a24f9604SJuraj Linkeš 198a24f9604SJuraj Linkeš Args: 199a24f9604SJuraj Linkeš build_target_config: The build target test run configuration according to which 200a24f9604SJuraj Linkeš the setup steps will be taken. 201a24f9604SJuraj Linkeš """ 202680d8a24SJuraj Linkeš self._configure_build_target(build_target_config) 203680d8a24SJuraj Linkeš self._copy_dpdk_tarball() 204680d8a24SJuraj Linkeš self._build_dpdk() 20568010b2aSJeremy Spewock self.bind_ports_to_driver() 20668010b2aSJeremy Spewock 207a24f9604SJuraj Linkeš def tear_down_build_target(self) -> None: 208a24f9604SJuraj Linkeš """Reset DPDK variables and bind port driver to the OS driver.""" 209a24f9604SJuraj Linkeš self._env_vars = {} 210a24f9604SJuraj Linkeš self._build_target_config = None 211a24f9604SJuraj Linkeš self.__remote_dpdk_dir = None 212a24f9604SJuraj Linkeš self._dpdk_version = None 213a24f9604SJuraj Linkeš self._compiler_version = None 21468010b2aSJeremy Spewock self.bind_ports_to_driver(for_dpdk=False) 215680d8a24SJuraj Linkeš 216517b4b26SJuraj Linkeš def _configure_build_target(self, build_target_config: BuildTargetConfiguration) -> None: 2176ef07151SJuraj Linkeš """Populate common environment variables and set build target config.""" 218b8bdc4c5SJuraj Linkeš self._env_vars = {} 219680d8a24SJuraj Linkeš self._build_target_config = build_target_config 220517b4b26SJuraj Linkeš self._env_vars.update(self.main_session.get_dpdk_build_env_vars(build_target_config.arch)) 221680d8a24SJuraj Linkeš self._env_vars["CC"] = build_target_config.compiler.name 222680d8a24SJuraj Linkeš if build_target_config.compiler_wrapper: 223680d8a24SJuraj Linkeš self._env_vars["CC"] = ( 224517b4b26SJuraj Linkeš f"'{build_target_config.compiler_wrapper} {build_target_config.compiler.name}'" 225517b4b26SJuraj Linkeš ) # fmt: skip 226680d8a24SJuraj Linkeš 227680d8a24SJuraj Linkeš @Node.skip_setup 228680d8a24SJuraj Linkeš def _copy_dpdk_tarball(self) -> None: 2296ef07151SJuraj Linkeš """Copy to and extract DPDK tarball on the SUT node.""" 230680d8a24SJuraj Linkeš self._logger.info("Copying DPDK tarball to SUT.") 231b8bdc4c5SJuraj Linkeš self.main_session.copy_to(SETTINGS.dpdk_tarball_path, self._remote_tmp_dir) 232680d8a24SJuraj Linkeš 233680d8a24SJuraj Linkeš # construct remote tarball path 234680d8a24SJuraj Linkeš # the basename is the same on local host and on remote Node 235680d8a24SJuraj Linkeš remote_tarball_path = self.main_session.join_remote_path( 236680d8a24SJuraj Linkeš self._remote_tmp_dir, os.path.basename(SETTINGS.dpdk_tarball_path) 237680d8a24SJuraj Linkeš ) 238680d8a24SJuraj Linkeš 239680d8a24SJuraj Linkeš # construct remote path after extracting 240680d8a24SJuraj Linkeš with tarfile.open(SETTINGS.dpdk_tarball_path) as dpdk_tar: 241680d8a24SJuraj Linkeš dpdk_top_dir = dpdk_tar.getnames()[0] 242680d8a24SJuraj Linkeš self._remote_dpdk_dir = self.main_session.join_remote_path( 243680d8a24SJuraj Linkeš self._remote_tmp_dir, dpdk_top_dir 244680d8a24SJuraj Linkeš ) 245680d8a24SJuraj Linkeš 246680d8a24SJuraj Linkeš self._logger.info( 247680d8a24SJuraj Linkeš f"Extracting DPDK tarball on SUT: " 248680d8a24SJuraj Linkeš f"'{remote_tarball_path}' into '{self._remote_dpdk_dir}'." 249680d8a24SJuraj Linkeš ) 250680d8a24SJuraj Linkeš # clean remote path where we're extracting 251680d8a24SJuraj Linkeš self.main_session.remove_remote_dir(self._remote_dpdk_dir) 252680d8a24SJuraj Linkeš 253680d8a24SJuraj Linkeš # then extract to remote path 254517b4b26SJuraj Linkeš self.main_session.extract_remote_tarball(remote_tarball_path, self._remote_dpdk_dir) 255680d8a24SJuraj Linkeš 256680d8a24SJuraj Linkeš @Node.skip_setup 257680d8a24SJuraj Linkeš def _build_dpdk(self) -> None: 2586ef07151SJuraj Linkeš """Build DPDK. 2596ef07151SJuraj Linkeš 2606ef07151SJuraj Linkeš Uses the already configured target. Assumes that the tarball has 261680d8a24SJuraj Linkeš already been copied to and extracted on the SUT node. 262680d8a24SJuraj Linkeš """ 263680d8a24SJuraj Linkeš self.main_session.build_dpdk( 264680d8a24SJuraj Linkeš self._env_vars, 265680d8a24SJuraj Linkeš MesonArgs(default_library="static", enable_kmods=True, libdir="lib"), 266680d8a24SJuraj Linkeš self._remote_dpdk_dir, 267680d8a24SJuraj Linkeš self.remote_dpdk_build_dir, 268680d8a24SJuraj Linkeš ) 269680d8a24SJuraj Linkeš 270680d8a24SJuraj Linkeš def build_dpdk_app(self, app_name: str, **meson_dpdk_args: str | bool) -> PurePath: 2716ef07151SJuraj Linkeš """Build one or all DPDK apps. 2726ef07151SJuraj Linkeš 2736ef07151SJuraj Linkeš Requires DPDK to be already built on the SUT node. 2746ef07151SJuraj Linkeš 2756ef07151SJuraj Linkeš Args: 2766ef07151SJuraj Linkeš app_name: The name of the DPDK app to build. 2776ef07151SJuraj Linkeš When `app_name` is ``all``, build all example apps. 2786ef07151SJuraj Linkeš meson_dpdk_args: The arguments found in ``meson_options.txt`` in root DPDK directory. 2796ef07151SJuraj Linkeš Do not use ``-D`` with them. 2806ef07151SJuraj Linkeš 2816ef07151SJuraj Linkeš Returns: 2826ef07151SJuraj Linkeš The directory path of the built app. If building all apps, return 283680d8a24SJuraj Linkeš the path to the examples directory (where all apps reside). 284680d8a24SJuraj Linkeš """ 285680d8a24SJuraj Linkeš self.main_session.build_dpdk( 286680d8a24SJuraj Linkeš self._env_vars, 287680d8a24SJuraj Linkeš MesonArgs(examples=app_name, **meson_dpdk_args), # type: ignore [arg-type] 288680d8a24SJuraj Linkeš # ^^ https://github.com/python/mypy/issues/11583 289680d8a24SJuraj Linkeš self._remote_dpdk_dir, 290680d8a24SJuraj Linkeš self.remote_dpdk_build_dir, 291680d8a24SJuraj Linkeš rebuild=True, 292680d8a24SJuraj Linkeš timeout=self._app_compile_timeout, 293680d8a24SJuraj Linkeš ) 294680d8a24SJuraj Linkeš 295680d8a24SJuraj Linkeš if app_name == "all": 296517b4b26SJuraj Linkeš return self.main_session.join_remote_path(self.remote_dpdk_build_dir, "examples") 297680d8a24SJuraj Linkeš return self.main_session.join_remote_path( 298680d8a24SJuraj Linkeš self.remote_dpdk_build_dir, "examples", f"dpdk-{app_name}" 299680d8a24SJuraj Linkeš ) 300c020b7ceSJuraj Linkeš 301c020b7ceSJuraj Linkeš def kill_cleanup_dpdk_apps(self) -> None: 3026ef07151SJuraj Linkeš """Kill all dpdk applications on the SUT, then clean up hugepages.""" 303c020b7ceSJuraj Linkeš if self._dpdk_kill_session and self._dpdk_kill_session.is_alive(): 304c020b7ceSJuraj Linkeš # we can use the session if it exists and responds 305*bfad0948SLuca Vizzarro self._dpdk_kill_session.kill_cleanup_dpdk_apps(self.dpdk_prefix_list) 306c020b7ceSJuraj Linkeš else: 307c020b7ceSJuraj Linkeš # otherwise, we need to (re)create it 308c020b7ceSJuraj Linkeš self._dpdk_kill_session = self.create_session("dpdk_kill") 309*bfad0948SLuca Vizzarro self.dpdk_prefix_list = [] 310c020b7ceSJuraj Linkeš 311444833c0SJuraj Linkeš def run_dpdk_app( 312fd8cd8eeSLuca Vizzarro self, app_path: PurePath, eal_params: EalParams, timeout: float = 30 313444833c0SJuraj Linkeš ) -> CommandResult: 3146ef07151SJuraj Linkeš """Run DPDK application on the remote node. 3156ef07151SJuraj Linkeš 3166ef07151SJuraj Linkeš The application is not run interactively - the command that starts the application 3176ef07151SJuraj Linkeš is executed and then the call waits for it to finish execution. 3186ef07151SJuraj Linkeš 3196ef07151SJuraj Linkeš Args: 3206ef07151SJuraj Linkeš app_path: The remote path to the DPDK application. 321fd8cd8eeSLuca Vizzarro eal_params: EAL parameters to run the DPDK application with. 3226ef07151SJuraj Linkeš timeout: Wait at most this long in seconds for `command` execution to complete. 3236ef07151SJuraj Linkeš 3246ef07151SJuraj Linkeš Returns: 3256ef07151SJuraj Linkeš The result of the DPDK app execution. 326444833c0SJuraj Linkeš """ 327444833c0SJuraj Linkeš return self.main_session.send_command( 328fd8cd8eeSLuca Vizzarro f"{app_path} {eal_params}", timeout, privileged=True, verify=True 329444833c0SJuraj Linkeš ) 330444833c0SJuraj Linkeš 331d99250aaSJuraj Linkeš def configure_ipv4_forwarding(self, enable: bool) -> None: 3326ef07151SJuraj Linkeš """Enable/disable IPv4 forwarding on the node. 3336ef07151SJuraj Linkeš 3346ef07151SJuraj Linkeš Args: 3356ef07151SJuraj Linkeš enable: If :data:`True`, enable the forwarding, otherwise disable it. 3366ef07151SJuraj Linkeš """ 337d99250aaSJuraj Linkeš self.main_session.configure_ipv4_forwarding(enable) 338d99250aaSJuraj Linkeš 33968010b2aSJeremy Spewock def bind_ports_to_driver(self, for_dpdk: bool = True) -> None: 34068010b2aSJeremy Spewock """Bind all ports on the SUT to a driver. 34168010b2aSJeremy Spewock 34268010b2aSJeremy Spewock Args: 3436ef07151SJuraj Linkeš for_dpdk: If :data:`True`, binds ports to os_driver_for_dpdk. 3446ef07151SJuraj Linkeš If :data:`False`, binds to os_driver. 34568010b2aSJeremy Spewock """ 34668010b2aSJeremy Spewock for port in self.ports: 34768010b2aSJeremy Spewock driver = port.os_driver_for_dpdk if for_dpdk else port.os_driver 34868010b2aSJeremy Spewock self.main_session.send_command( 34968010b2aSJeremy Spewock f"{self.path_to_devbind_script} -b {driver} --force {port.pci}", 35068010b2aSJeremy Spewock privileged=True, 35168010b2aSJeremy Spewock verify=True, 35268010b2aSJeremy Spewock ) 353