diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 03:01:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 03:01:46 +0000 |
commit | f8fe689a81f906d1b91bb3220acde2a4ecb14c5b (patch) | |
tree | 26484e9d7e2c67806c2d1760196ff01aaa858e8c /src/VBox/ValidationKit/testboxscript/testboxscript_real.py | |
parent | Initial commit. (diff) | |
download | virtualbox-f8fe689a81f906d1b91bb3220acde2a4ecb14c5b.tar.xz virtualbox-f8fe689a81f906d1b91bb3220acde2a4ecb14c5b.zip |
Adding upstream version 6.0.4-dfsg.upstream/6.0.4-dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/VBox/ValidationKit/testboxscript/testboxscript_real.py')
-rwxr-xr-x | src/VBox/ValidationKit/testboxscript/testboxscript_real.py | 1056 |
1 files changed, 1056 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testboxscript/testboxscript_real.py b/src/VBox/ValidationKit/testboxscript/testboxscript_real.py new file mode 100755 index 00000000..11312ea9 --- /dev/null +++ b/src/VBox/ValidationKit/testboxscript/testboxscript_real.py @@ -0,0 +1,1056 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: testboxscript_real.py $ + +""" +TestBox Script - main(). +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2019 Oracle Corporation + +This file is part of VirtualBox Open Source Edition (OSE), as +available from http://www.virtualbox.org. This file is free software; +you can redistribute it and/or modify it under the terms of the GNU +General Public License (GPL) as published by the Free Software +Foundation, in version 2 as it comes in the "COPYING" file of the +VirtualBox OSE distribution. VirtualBox OSE is distributed in the +hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL) only, as it comes in the "COPYING.CDDL" file of the +VirtualBox OSE distribution, in which case the provisions of the +CDDL are applicable instead of those of the GPL. + +You may elect to license modified versions of this file under the +terms and conditions of either the GPL or the CDDL or both. +""" +__version__ = "$Revision: 127855 $" + + +# Standard python imports. +import math +import multiprocessing +import os +from optparse import OptionParser # pylint: disable=deprecated-module +import platform +import random +import shutil +import sys +import tempfile +import time +import uuid + +# Only the main script needs to modify the path. +try: __file__ +except: __file__ = sys.argv[0]; +g_ksTestScriptDir = os.path.dirname(os.path.abspath(__file__)); +g_ksValidationKitDir = os.path.dirname(g_ksTestScriptDir); +sys.path.extend([g_ksTestScriptDir, g_ksValidationKitDir]); + +# Validation Kit imports. +from common import constants; +from common import utils; +import testboxcommons; +from testboxcommons import TestBoxException; +from testboxcommand import TestBoxCommand; +from testboxconnection import TestBoxConnection; +from testboxscript import TBS_EXITCODE_SYNTAX, TBS_EXITCODE_FAILURE; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + long = int; # pylint: disable=redefined-builtin,invalid-name + + +class TestBoxScriptException(Exception): + """ For raising exceptions during TestBoxScript.__init__. """ + pass; + + +class TestBoxScript(object): + """ + Implementation of the test box script. + Communicate with test manager and perform offered actions. + """ + + ## @name Class Constants. + # @{ + + # Scratch space round value (MB). + kcMbScratchSpaceRounding = 64 + # Memory size round value (MB). + kcMbMemoryRounding = 4 + # A NULL UUID in string form. + ksNullUuid = '00000000-0000-0000-0000-000000000000'; + # The minimum dispatch loop delay. + kcSecMinDelay = 12; + # The maximum dispatch loop delay (inclusive). + kcSecMaxDelay = 24; + # The minimum sign-on delay. + kcSecMinSignOnDelay = 30; + # The maximum sign-on delay (inclusive). + kcSecMaxSignOnDelay = 60; + + # Keys for config params + VALUE = 'value' + FN = 'fn' # pylint: disable=C0103 + + ## @} + + + def __init__(self, oOptions): + """ + Initialize internals + """ + self._oOptions = oOptions; + self._sTestBoxHelper = None; + + # Signed-on state + self._cSignOnAttempts = 0; + self._fSignedOn = False; + self._fNeedReSignOn = False; + self._fFirstSignOn = True; + self._idTestBox = None; + self._sTestBoxName = ''; + self._sTestBoxUuid = self.ksNullUuid; # convenience, assigned below. + + # Command processor. + self._oCommand = TestBoxCommand(self); + + # + # Scratch dir setup. Use /var/tmp instead of /tmp because we may need + # many many GBs for some test scenarios and /tmp can be backed by swap + # or be a fast+small disk of some kind, while /var/tmp is normally + # larger, if slower. /var/tmp is generally not cleaned up on reboot, + # /tmp often is, this would break host panic / triple-fault detection. + # + if self._oOptions.sScratchRoot is None: + if utils.getHostOs() in ('win', 'os2', 'haiku', 'dos'): + # We need *lots* of space, so avoid /tmp as it may be a memory + # file system backed by the swap file, or worse. + self._oOptions.sScratchRoot = tempfile.gettempdir(); + else: + self._oOptions.sScratchRoot = '/var/tmp'; + sSubDir = 'testbox'; + try: + sSubDir = '%s-%u' % (sSubDir, os.getuid()); # pylint: disable=E1101 + except: + pass; + self._oOptions.sScratchRoot = os.path.join(self._oOptions.sScratchRoot, sSubDir); + + self._sScratchSpill = os.path.join(self._oOptions.sScratchRoot, 'scratch'); + self._sScratchScripts = os.path.join(self._oOptions.sScratchRoot, 'scripts'); + self._sScratchState = os.path.join(self._oOptions.sScratchRoot, 'state'); # persistant storage. + + for sDir in [self._oOptions.sScratchRoot, self._sScratchSpill, self._sScratchScripts, self._sScratchState]: + if not os.path.isdir(sDir): + os.makedirs(sDir, 0o700); + + # We count consecutive reinitScratch failures and will reboot the + # testbox after a while in the hope that it will correct the issue. + self._cReinitScratchErrors = 0; + + # + # Mount builds and test resources if requested. + # + self.mountShares(); + + # + # Sign-on parameters: Packed into list of records of format: + # { <Parameter ID>: { <Current value>, <Check function> } } + # + self._ddSignOnParams = \ + { + constants.tbreq.ALL_PARAM_TESTBOX_UUID: { self.VALUE: self._getHostSystemUuid(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_OS: { self.VALUE: utils.getHostOs(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_OS_VERSION: { self.VALUE: utils.getHostOsVersion(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_CPU_ARCH: { self.VALUE: utils.getHostArch(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_CPU_VENDOR: { self.VALUE: self._getHostCpuVendor(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_CPU_NAME: { self.VALUE: self._getHostCpuName(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_CPU_REVISION: { self.VALUE: self._getHostCpuRevision(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_HAS_HW_VIRT: { self.VALUE: self._hasHostHwVirt(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_HAS_NESTED_PAGING:{ self.VALUE: self._hasHostNestedPaging(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_HAS_64_BIT_GUEST: { self.VALUE: self._can64BitGuest(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_HAS_IOMMU: { self.VALUE: self._hasHostIoMmu(), self.FN: None }, + #constants.tbreq.SIGNON_PARAM_WITH_RAW_MODE: { self.VALUE: self._withRawModeSupport(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_SCRIPT_REV: { self.VALUE: self._getScriptRev(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_REPORT: { self.VALUE: self._getHostReport(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_PYTHON_VERSION: { self.VALUE: self._getPythonHexVersion(), self.FN: None }, + constants.tbreq.SIGNON_PARAM_CPU_COUNT: { self.VALUE: None, self.FN: multiprocessing.cpu_count }, + constants.tbreq.SIGNON_PARAM_MEM_SIZE: { self.VALUE: None, self.FN: self._getHostMemSize }, + constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE: { self.VALUE: None, self.FN: self._getFreeScratchSpace }, + } + for sItem in self._ddSignOnParams: + if self._ddSignOnParams[sItem][self.FN] is not None: + self._ddSignOnParams[sItem][self.VALUE] = self._ddSignOnParams[sItem][self.FN]() + + testboxcommons.log('Starting Test Box script (%s)' % (self._getScriptRev(),)); + testboxcommons.log('Test Manager URL: %s' % self._oOptions.sTestManagerUrl,) + testboxcommons.log('Scratch root path: %s' % self._oOptions.sScratchRoot,) + for sItem in self._ddSignOnParams: + testboxcommons.log('Sign-On value %18s: %s' % (sItem, self._ddSignOnParams[sItem][self.VALUE])); + + # + # The System UUID is the primary identification of the machine, so + # refuse to cooperate if it's NULL. + # + self._sTestBoxUuid = self.getSignOnParam(constants.tbreq.ALL_PARAM_TESTBOX_UUID); + if self._sTestBoxUuid == self.ksNullUuid: + raise TestBoxScriptException('Couldn\'t determine the System UUID, please use --system-uuid to specify it.'); + + # + # Export environment variables, clearing any we don't know yet. + # + for sEnvVar in self._oOptions.asEnvVars: + iEqual = sEnvVar.find('='); + if iEqual == -1: # No '=', remove it. + if sEnvVar in os.environ: + del os.environ[sEnvVar]; + elif iEqual > 0: # Set it. + os.environ[sEnvVar[:iEqual]] = sEnvVar[iEqual+1:]; + else: # Starts with '=', bad user. + raise TestBoxScriptException('Invalid -E argument: "%s"' % (sEnvVar,)); + + os.environ['TESTBOX_PATH_BUILDS'] = self._oOptions.sBuildsPath; + os.environ['TESTBOX_PATH_RESOURCES'] = self._oOptions.sTestRsrcPath; + os.environ['TESTBOX_PATH_SCRATCH'] = self._sScratchSpill; + os.environ['TESTBOX_PATH_SCRIPTS'] = self._sScratchScripts; + os.environ['TESTBOX_PATH_UPLOAD'] = self._sScratchSpill; ## @todo drop the UPLOAD dir? + os.environ['TESTBOX_HAS_HW_VIRT'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_HAS_HW_VIRT); + os.environ['TESTBOX_HAS_NESTED_PAGING'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_HAS_NESTED_PAGING); + os.environ['TESTBOX_HAS_IOMMU'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_HAS_IOMMU); + os.environ['TESTBOX_SCRIPT_REV'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_SCRIPT_REV); + os.environ['TESTBOX_CPU_COUNT'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_CPU_COUNT); + os.environ['TESTBOX_MEM_SIZE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_MEM_SIZE); + os.environ['TESTBOX_SCRATCH_SIZE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE); + #TODO: os.environ['TESTBOX_WITH_RAW_MODE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_WITH_RAW_MODE); + os.environ['TESTBOX_WITH_RAW_MODE'] = str(self._withRawModeSupport()); + os.environ['TESTBOX_MANAGER_URL'] = self._oOptions.sTestManagerUrl; + os.environ['TESTBOX_UUID'] = self._sTestBoxUuid; + os.environ['TESTBOX_REPORTER'] = 'remote'; + os.environ['TESTBOX_NAME'] = ''; + os.environ['TESTBOX_ID'] = ''; + os.environ['TESTBOX_TEST_SET_ID'] = ''; + os.environ['TESTBOX_TIMEOUT'] = '0'; + os.environ['TESTBOX_TIMEOUT_ABS'] = '0'; + + if utils.getHostOs() == 'win': + os.environ['COMSPEC'] = os.path.join(os.environ['SystemRoot'], 'System32', 'cmd.exe'); + # Currently omitting any kBuild tools. + + def mountShares(self): + """ + Mounts the shares. + Raises exception on failure. + """ + self._mountShare(self._oOptions.sBuildsPath, self._oOptions.sBuildsServerType, self._oOptions.sBuildsServerName, + self._oOptions.sBuildsServerShare, + self._oOptions.sBuildsServerUser, self._oOptions.sBuildsServerPasswd, + self._oOptions.sBuildsServerMountOpt, 'builds'); + self._mountShare(self._oOptions.sTestRsrcPath, self._oOptions.sTestRsrcServerType, self._oOptions.sTestRsrcServerName, + self._oOptions.sTestRsrcServerShare, + self._oOptions.sTestRsrcServerUser, self._oOptions.sTestRsrcServerPasswd, + self._oOptions.sTestRsrcServerMountOpt, 'testrsrc'); + return True; + + def _mountShare(self, sMountPoint, sType, sServer, sShare, sUser, sPassword, sMountOpt, sWhat): + """ + Mounts the specified share if needed. + Raises exception on failure. + """ + # Only mount if the type is specified. + if sType is None: + return True; + + # Test if already mounted. + sTestFile = os.path.join(sMountPoint + os.path.sep, sShare + '-new.txt'); + if os.path.isfile(sTestFile): + return True; + + # + # Platform specific mount code. + # + sHostOs = utils.getHostOs() + if sHostOs in ('darwin', 'freebsd'): + if sMountOpt != '': + sMountOpt = ',' + sMountOpt + utils.sudoProcessCall(['/sbin/umount', sMountPoint]); + utils.sudoProcessCall(['/bin/mkdir', '-p', sMountPoint]); + utils.sudoProcessCall(['/usr/sbin/chown', str(os.getuid()), sMountPoint]); # pylint: disable=E1101 + if sType == 'cifs': + # Note! no smb://server/share stuff here, 10.6.8 didn't like it. + utils.processOutputChecked(['/sbin/mount_smbfs', + '-o', + 'automounted,nostreams,soft,noowners,noatime,rdonly' + sMountOpt, + '-f', '0555', '-d', '0555', + '//%s:%s@%s/%s' % (sUser, sPassword, sServer, sShare), + sMountPoint]); + else: + raise TestBoxScriptException('Unsupported server type %s.' % (sType,)); + + elif sHostOs == 'linux': + if sMountOpt != '': + sMountOpt = ',' + sMountOpt + utils.sudoProcessCall(['/bin/umount', sMountPoint]); + utils.sudoProcessCall(['/bin/mkdir', '-p', sMountPoint]); + if sType == 'cifs': + utils.sudoProcessOutputChecked(['/bin/mount', '-t', 'cifs', + '-o', + 'user=' + sUser + + ',password=' + sPassword + + ',sec=ntlmv2' + + ',uid=' + str(os.getuid()) # pylint: disable=E1101 + + ',gid=' + str(os.getgid()) # pylint: disable=E1101 + + ',nounix,file_mode=0555,dir_mode=0555,soft,ro' + + sMountOpt, + '//%s/%s' % (sServer, sShare), + sMountPoint]); + elif sType == 'nfs': + utils.sudoProcessOutputChecked(['/bin/mount', '-t', 'nfs', + '-o', 'soft,ro' + sMountOpt, + '%s:%s' % (sServer, sShare if sShare.find('/') >= 0 else ('/export/' + sShare)), + sMountPoint]); + + else: + raise TestBoxScriptException('Unsupported server type %s.' % (sType,)); + + elif sHostOs == 'solaris': + if sMountOpt != '': + sMountOpt = ',' + sMountOpt + utils.sudoProcessCall(['/sbin/umount', sMountPoint]); + utils.sudoProcessCall(['/bin/mkdir', '-p', sMountPoint]); + if sType == 'cifs': + ## @todo This stuff doesn't work on wei01-x4600b.de.oracle.com running 11.1. FIXME! + oPasswdFile = tempfile.TemporaryFile(); + oPasswdFile.write(sPassword + '\n'); + oPasswdFile.flush(); + utils.sudoProcessOutputChecked(['/sbin/mount', '-F', 'smbfs', + '-o', + 'user=' + sUser + + ',uid=' + str(os.getuid()) # pylint: disable=E1101 + + ',gid=' + str(os.getgid()) # pylint: disable=E1101 + + ',fileperms=0555,dirperms=0555,noxattr,ro' + + sMountOpt, + '//%s/%s' % (sServer, sShare), + sMountPoint], + stdin = oPasswdFile); + oPasswdFile.close(); + elif sType == 'nfs': + utils.sudoProcessOutputChecked(['/sbin/mount', '-F', 'nfs', + '-o', 'noxattr,ro' + sMountOpt, + '%s:%s' % (sServer, sShare if sShare.find('/') >= 0 else ('/export/' + sShare)), + sMountPoint]); + + else: + raise TestBoxScriptException('Unsupported server type %s.' % (sType,)); + + + elif sHostOs == 'win': + if sType != 'cifs': + raise TestBoxScriptException('Only CIFS mounts are supported on Windows.'); + utils.processCall(['net', 'use', sMountPoint, '/d']); + utils.processOutputChecked(['net', 'use', sMountPoint, + '\\\\' + sServer + '\\' + sShare, + sPassword, + '/USER:' + sUser,]); + else: + raise TestBoxScriptException('Unsupported host %s' % (sHostOs,)); + + # + # Re-test. + # + if not os.path.isfile(sTestFile): + raise TestBoxException('Failed to mount %s (%s[%s]) at %s: %s not found' + % (sWhat, sServer, sShare, sMountPoint, sTestFile)); + + return True; + + ## @name Signon property releated methods. + # @{ + + def _getHelperOutput(self, sCmd): + """ + Invokes TestBoxHelper to obtain information hard to access from python. + """ + if self._sTestBoxHelper is None: + if not utils.isRunningFromCheckout(): + # See VBoxTestBoxScript.zip for layout. + self._sTestBoxHelper = os.path.join(g_ksValidationKitDir, utils.getHostOs(), utils.getHostArch(), \ + 'TestBoxHelper'); + else: # Only for in-tree testing, so don't bother be too accurate right now. + sType = os.environ.get('KBUILD_TYPE', os.environ.get('BUILD_TYPE', 'debug')); + self._sTestBoxHelper = os.path.join(g_ksValidationKitDir, os.pardir, os.pardir, os.pardir, 'out', \ + utils.getHostOsDotArch(), sType, 'testboxscript', \ + utils.getHostOs(), utils.getHostArch(), \ + 'TestBoxHelper'); + if utils.getHostOs() in ['win', 'os2']: + self._sTestBoxHelper += '.exe'; + + return utils.processOutputChecked([self._sTestBoxHelper, sCmd]).strip(); + + def _getHelperOutputTristate(self, sCmd, fDunnoValue): + """ + Invokes TestBoxHelper to obtain information hard to access from python. + """ + sValue = self._getHelperOutput(sCmd); + sValue = sValue.lower(); + if sValue == 'true': + return True; + if sValue == 'false': + return False; + if sValue != 'dunno' and sValue != 'none': + raise TestBoxException('Unexpected response "%s" to helper command "%s"' % (sValue, sCmd)); + return fDunnoValue; + + + @staticmethod + def _isUuidGood(sUuid): + """ + Checks if the UUID looks good. + + There are systems with really bad UUIDs, for instance + "03000200-0400-0500-0006-000700080009". + """ + if sUuid == TestBoxScript.ksNullUuid: + return False; + sUuid = sUuid.lower(); + for sDigit in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']: + if sUuid.count(sDigit) > 16: + return False; + return True; + + def _getHostSystemUuid(self): + """ + Get the system UUID string from the System, return null-uuid if + unable to get retrieve it. + """ + if self._oOptions.sSystemUuid is not None: + return self._oOptions.sSystemUuid; + + sUuid = self.ksNullUuid; + + # + # Try get at the firmware UUID. + # + if utils.getHostOs() == 'linux': + # NOTE: This requires to have kernel option enabled: + # Firmware Drivers -> Export DMI identification via sysfs to userspace + if os.path.exists('/sys/devices/virtual/dmi/id/product_uuid'): + try: + sVar = utils.sudoProcessOutputChecked(['cat', '/sys/devices/virtual/dmi/id/product_uuid']); + sUuid = str(uuid.UUID(sVar.strip())); + except: + pass; + ## @todo consider dmidecoder? What about EFI systems? + + elif utils.getHostOs() == 'win': + # Windows: WMI + try: + import win32com.client; # pylint: disable=F0401 + oWmi = win32com.client.Dispatch('WbemScripting.SWbemLocator'); + oWebm = oWmi.ConnectServer('.', 'root\\cimv2'); + for oItem in oWebm.ExecQuery('SELECT * FROM Win32_ComputerSystemProduct'): + if oItem.UUID != None: + sUuid = str(uuid.UUID(oItem.UUID)); + except: + pass; + + elif utils.getHostOs() == 'darwin': + try: + sVar = utils.processOutputChecked(['/bin/sh', '-c', + '/usr/sbin/ioreg -k IOPlatformUUID' \ + + '| /usr/bin/grep IOPlatformUUID' \ + + '| /usr/bin/head -1']); + sVar = sVar.strip()[-(len(self.ksNullUuid) + 1):-1]; + sUuid = str(uuid.UUID(sVar)); + except: + pass; + + elif utils.getHostOs() == 'solaris': + # Solaris: The smbios util. + try: + sVar = utils.processOutputChecked(['/bin/sh', '-c', + '/usr/sbin/smbios ' \ + + '| /usr/xpg4/bin/sed -ne \'s/^.*UUID: *//p\'' \ + + '| /usr/bin/head -1']); + sUuid = str(uuid.UUID(sVar.strip())); + except: + pass; + + if self._isUuidGood(sUuid): + return sUuid; + + # + # Try add the MAC address. + # uuid.getnode may provide it, or it may return a random number... + # + lMacAddr = uuid.getnode(); + sNode = '%012x' % (lMacAddr,) + if lMacAddr == uuid.getnode() and lMacAddr != 0 and len(sNode) == 12: + return sUuid[:-12] + sNode; + + return sUuid; + + def _getHostCpuVendor(self): + """ + Get the CPUID vendor string on intel HW. + """ + return self._getHelperOutput('cpuvendor'); + + def _getHostCpuName(self): + """ + Get the CPU name/description string. + """ + return self._getHelperOutput('cpuname'); + + def _getHostCpuRevision(self): + """ + Get the CPU revision (family/model/stepping) value. + """ + return self._getHelperOutput('cpurevision'); + + def _hasHostHwVirt(self): + """ + Check if the host supports AMD-V or VT-x + """ + if self._oOptions.fHasHwVirt is None: + self._oOptions.fHasHwVirt = self._getHelperOutput('cpuhwvirt'); + return self._oOptions.fHasHwVirt; + + def _hasHostNestedPaging(self): + """ + Check if the host supports nested paging. + """ + if not self._hasHostHwVirt(): + return False; + if self._oOptions.fHasNestedPaging is None: + self._oOptions.fHasNestedPaging = self._getHelperOutputTristate('nestedpaging', False); + return self._oOptions.fHasNestedPaging; + + def _can64BitGuest(self): + """ + Check if the we (VBox) can run 64-bit guests. + """ + if not self._hasHostHwVirt(): + return False; + if self._oOptions.fCan64BitGuest is None: + self._oOptions.fCan64BitGuest = self._getHelperOutputTristate('longmode', True); + return self._oOptions.fCan64BitGuest; + + def _hasHostIoMmu(self): + """ + Check if the host has an I/O MMU of the VT-d kind. + """ + if not self._hasHostHwVirt(): + return False; + if self._oOptions.fHasIoMmu is None: + ## @todo Any way to figure this one out on any host OS? + self._oOptions.fHasIoMmu = False; + return self._oOptions.fHasIoMmu; + + def _withRawModeSupport(self): + """ + Check if the testbox is configured with raw-mode support or not. + """ + if self._oOptions.fWithRawMode is None: + self._oOptions.fWithRawMode = True; + return self._oOptions.fWithRawMode; + + def _getHostReport(self): + """ + Generate a report about the host hardware and software. + """ + return self._getHelperOutput('report'); + + + def _getHostMemSize(self): + """ + Gets the amount of physical memory on the host (and accessible to the + OS, i.e. don't report stuff over 4GB if Windows doesn't wanna use it). + Unit: MiB. + """ + cMbMemory = long(self._getHelperOutput('memsize').strip()) / (1024 * 1024); + + # Round it. + cMbMemory = long(math.floor(cMbMemory / self.kcMbMemoryRounding)) * self.kcMbMemoryRounding; + return cMbMemory; + + def _getFreeScratchSpace(self): + """ + Get free space on the volume where scratch directory is located and + return it in bytes rounded down to nearest 64MB + (currently works on Linux only) + Unit: MiB. + """ + if platform.system() == 'Windows': + import ctypes + cTypeMbFreeSpace = ctypes.c_ulonglong(0) + ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(self._oOptions.sScratchRoot), None, None, + ctypes.pointer(cTypeMbFreeSpace)) + cMbFreeSpace = cTypeMbFreeSpace.value + else: + stats = os.statvfs(self._oOptions.sScratchRoot); # pylint: disable=E1101 + cMbFreeSpace = stats.f_frsize * stats.f_bfree + + # Convert to MB + cMbFreeSpace = long(cMbFreeSpace) /(1024 * 1024) + + # Round free space size + cMbFreeSpace = long(math.floor(cMbFreeSpace / self.kcMbScratchSpaceRounding)) * self.kcMbScratchSpaceRounding; + return cMbFreeSpace; + + def _getScriptRev(self): + """ + The script (subversion) revision number. + """ + sRev = '@VBOX_SVN_REV@'; + sRev = sRev.strip(); # just in case... + try: + _ = int(sRev); + except: + return __version__[11:-1].strip(); + return sRev; + + def _getPythonHexVersion(self): + """ + The python hex version number. + """ + uHexVersion = getattr(sys, 'hexversion', None); + if uHexVersion is None: + uHexVersion = (sys.version_info[0] << 24) | (sys.version_info[1] << 16) | (sys.version_info[2] << 8); + if sys.version_info[3] == 'final': + uHexVersion |= 0xf0; + return uHexVersion; + + # @} + + def openTestManagerConnection(self): + """ + Opens up a connection to the test manager. + + Raises exception on failure. + """ + return TestBoxConnection(self._oOptions.sTestManagerUrl, self._idTestBox, self._sTestBoxUuid); + + def getSignOnParam(self, sName): + """ + Returns a sign-on parameter value as string. + Raises exception if the name is incorrect. + """ + return str(self._ddSignOnParams[sName][self.VALUE]); + + def getPathState(self): + """ + Get the path to the state dir in the scratch area. + """ + return self._sScratchState; + + def getPathScripts(self): + """ + Get the path to the scripts dir (TESTBOX_PATH_SCRIPTS) in the scratch area. + """ + return self._sScratchScripts; + + def getPathSpill(self): + """ + Get the path to the spill dir (TESTBOX_PATH_SCRATCH) in the scratch area. + """ + return self._sScratchSpill; + + def getPathBuilds(self): + """ + Get the path to the builds. + """ + return self._oOptions.sBuildsPath; + + def getTestBoxId(self): + """ + Get the TestBox ID for state saving purposes. + """ + return self._idTestBox; + + def getTestBoxName(self): + """ + Get the TestBox name for state saving purposes. + """ + return self._sTestBoxName; + + def _reinitScratch(self, fnLog, fUseTheForce): + """ + Wipes the scratch directories and re-initializes them. + + No exceptions raise, returns success indicator instead. + """ + if fUseTheForce is None: + fUseTheForce = self._fFirstSignOn; + + class ErrorCallback(object): # pylint: disable=R0903 + """ + Callbacks + state for the cleanup. + """ + def __init__(self): + self.fRc = True; + def onErrorCallback(self, sFnName, sPath, aXcptInfo): + """ Logs error during shutil.rmtree operation. """ + fnLog('Error removing "%s": fn=%s %s' % (sPath, sFnName, aXcptInfo[1])); + self.fRc = False; + oRc = ErrorCallback(); + + # + # Cleanup. + # + for sName in os.listdir(self._oOptions.sScratchRoot): + sFullName = os.path.join(self._oOptions.sScratchRoot, sName); + try: + if os.path.isdir(sFullName): + shutil.rmtree(sFullName, False, oRc.onErrorCallback); + else: + os.remove(sFullName); + if os.path.exists(sFullName): + raise Exception('Still exists after deletion, weird.'); + except Exception as oXcpt: + if fUseTheForce is True \ + and utils.getHostOs() not in ['win', 'os2'] \ + and len(sFullName) >= 8 \ + and sFullName[0] == '/' \ + and sFullName[1] != '/' \ + and sFullName.find('/../') < 0: + fnLog('Problems deleting "%s" (%s) using the force...' % (sFullName, oXcpt)); + try: + if os.path.isdir(sFullName): + iRc = utils.sudoProcessCall(['/bin/rm', '-Rf', sFullName]) + else: + iRc = utils.sudoProcessCall(['/bin/rm', '-f', sFullName]) + if iRc != 0: + raise Exception('exit code %s' % iRc); + if os.path.exists(sFullName): + raise Exception('Still exists after forced deletion, weird^2.'); + except: + fnLog('Error sudo deleting "%s": %s' % (sFullName, oXcpt)); + oRc.fRc = False; + else: + fnLog('Error deleting "%s": %s' % (sFullName, oXcpt)); + oRc.fRc = False; + + # Display files left behind. + def dirEnumCallback(sName, oStat): + """ callback for dirEnumerateTree """ + fnLog(u'%s %s' % (utils.formatFileStat(oStat) if oStat is not None else '????????????', sName)); + utils.dirEnumerateTree(self._oOptions.sScratchRoot, dirEnumCallback); + + # + # Re-create the directories. + # + for sDir in [self._oOptions.sScratchRoot, self._sScratchSpill, self._sScratchScripts, self._sScratchState]: + if not os.path.isdir(sDir): + try: + os.makedirs(sDir, 0o700); + except Exception as oXcpt: + fnLog('Error creating "%s": %s' % (sDir, oXcpt)); + oRc.fRc = False; + + if oRc.fRc is True: + self._cReinitScratchErrors = 0; + else: + self._cReinitScratchErrors += 1; + return oRc.fRc; + + def reinitScratch(self, fnLog = testboxcommons.log, fUseTheForce = None, cRetries = 0, cMsDelay = 5000): + """ + Wipes the scratch directories and re-initializes them. + + Will retry according to the cRetries and cMsDelay parameters. Windows + forces us to apply this hack as it ships with services asynchronously + scanning files after they execute, thus racing us cleaning up after a + test. On testboxwin3 we had frequent trouble with aelupsvc.dll keeping + vts_rm.exe kind of open, somehow preventing us from removing the + directory containing it, despite not issuing any errors deleting the + file itself. The service is called "Application Experience", which + feels like a weird joke here. + + No exceptions raise, returns success indicator instead. + """ + fRc = self._reinitScratch(fnLog, fUseTheForce) + while fRc is False and cRetries > 0: + time.sleep(cMsDelay / 1000.0); + fnLog('reinitScratch: Retrying...'); + fRc = self._reinitScratch(fnLog, fUseTheForce) + cRetries -= 1; + return fRc; + + + def _doSignOn(self): + """ + Worker for _maybeSignOn that does the actual signing on. + """ + assert not self._oCommand.isRunning(); + + # Reset the siged-on state. + testboxcommons.log('Signing-on...') + self._fSignedOn = False + self._idTestBox = None + self._cSignOnAttempts += 1; + + # Assemble SIGN-ON request parameters and send the request. + dParams = {}; + for sParam in self._ddSignOnParams: + dParams[sParam] = self._ddSignOnParams[sParam][self.VALUE]; + oResponse = TestBoxConnection.sendSignOn(self._oOptions.sTestManagerUrl, dParams); + + # Check response. + try: + sResult = oResponse.getStringChecked(constants.tbresp.ALL_PARAM_RESULT); + if sResult != constants.tbresp.STATUS_ACK: + raise TestBoxException('Result is %s' % (sResult,)); + oResponse.checkParameterCount(3); + idTestBox = oResponse.getIntChecked(constants.tbresp.SIGNON_PARAM_ID, 1, 0x7ffffffe); + sTestBoxName = oResponse.getStringChecked(constants.tbresp.SIGNON_PARAM_NAME); + except TestBoxException as err: + testboxcommons.log('Failed to sign-on: %s' % (str(err),)) + testboxcommons.log('Server response: %s' % (oResponse.toString(),)); + return False; + + # Successfully signed on, update the state. + self._fSignedOn = True; + self._fNeedReSignOn = False; + self._cSignOnAttempts = 0; + self._idTestBox = idTestBox; + self._sTestBoxName = sTestBoxName; + + # Update the environment. + os.environ['TESTBOX_ID'] = str(self._idTestBox); + os.environ['TESTBOX_NAME'] = sTestBoxName; + os.environ['TESTBOX_CPU_COUNT'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_CPU_COUNT); + os.environ['TESTBOX_MEM_SIZE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_MEM_SIZE); + os.environ['TESTBOX_SCRATCH_SIZE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE); + + testboxcommons.log('Successfully signed-on with Test Box ID #%s and given the name "%s"' \ + % (self._idTestBox, self._sTestBoxName)); + + # Set up the scratch area. + self.reinitScratch(fUseTheForce = self._fFirstSignOn, cRetries = 2); + + self._fFirstSignOn = False; + return True; + + def _maybeSignOn(self): + """ + Check if Test Box parameters were changed + and do sign-in in case of positive result + """ + + # Skip sign-on check if background command is currently in + # running state (avoid infinite signing on). + if self._oCommand.isRunning(): + return None; + + # Refresh sign-on parameters, changes triggers sign-on. + fNeedSignOn = (True if not self._fSignedOn or self._fNeedReSignOn else False) + for item in self._ddSignOnParams: + if self._ddSignOnParams[item][self.FN] is None: + continue + + sOldValue = self._ddSignOnParams[item][self.VALUE] + self._ddSignOnParams[item][self.VALUE] = self._ddSignOnParams[item][self.FN]() + if sOldValue != self._ddSignOnParams[item][self.VALUE]: + fNeedSignOn = True + testboxcommons.log('Detected %s parameter change: %s -> %s' % + (item, sOldValue, self._ddSignOnParams[item][self.VALUE])) + + if fNeedSignOn: + self._doSignOn(); + return None; + + def dispatch(self): + """ + Receive orders from Test Manager and execute them + """ + + (self._idTestBox, self._sTestBoxName, self._fSignedOn) = self._oCommand.resumeIncompleteCommand(); + self._fNeedReSignOn = self._fSignedOn; + if self._fSignedOn: + os.environ['TESTBOX_ID'] = str(self._idTestBox); + os.environ['TESTBOX_NAME'] = self._sTestBoxName; + + while True: + # Make sure we're signed on before trying to do anything. + self._maybeSignOn(); + while not self._fSignedOn: + iFactor = 1 if self._cSignOnAttempts < 100 else 4; + time.sleep(random.randint(self.kcSecMinSignOnDelay * iFactor, self.kcSecMaxSignOnDelay * iFactor)); + self._maybeSignOn(); + + # Retrieve and handle command from the TM. + (oResponse, oConnection) = TestBoxConnection.requestCommandWithConnection(self._oOptions.sTestManagerUrl, + self._idTestBox, + self._sTestBoxUuid, + self._oCommand.isRunning()); + if oResponse is not None: + self._oCommand.handleCommand(oResponse, oConnection); + if oConnection is not None: + if oConnection.isConnected(): + self._oCommand.flushLogOnConnection(oConnection); + oConnection.close(); + + # Automatically reboot if scratch init fails. + #if self._cReinitScratchErrors > 8 and self.reinitScratch(cRetries = 3) is False: + # testboxcommons.log('Scratch does not initialize cleanly after %d attempts, rebooting...' + # % ( self._cReinitScratchErrors, )); + # self._oCommand.doReboot(); + + # delay a wee bit before looping. + ## @todo We shouldn't bother the server too frequently. We should try combine the test reporting done elsewhere + # with the command retrieval done here. I believe tinderclient.pl is capable of doing that. + iFactor = 1; + if self._cReinitScratchErrors > 0: + iFactor = 4; + time.sleep(random.randint(self.kcSecMinDelay * iFactor, self.kcSecMaxDelay * iFactor)); + + # Not reached. + + + @staticmethod + def main(): + """ + Main function a la C/C++. Returns exit code. + """ + + # + # Parse arguments. + # + sDefShareType = 'nfs' if utils.getHostOs() == 'solaris' else 'cifs'; + if utils.getHostOs() in ('win', 'os2'): + sDefTestRsrc = 'T:'; + sDefBuilds = 'U:'; + elif utils.getHostOs() == 'darwin': + sDefTestRsrc = '/Volumes/testrsrc'; + sDefBuilds = '/Volumes/builds'; + else: + sDefTestRsrc = '/mnt/testrsrc'; + sDefBuilds = '/mnt/builds'; + + class MyOptionParser(OptionParser): + """ We need to override the exit code on --help, error and so on. """ + def __init__(self, *args, **kwargs): + OptionParser.__init__(self, *args, **kwargs); + def exit(self, status = 0, msg = None): + OptionParser.exit(self, TBS_EXITCODE_SYNTAX, msg); + + parser = MyOptionParser(version=__version__[11:-1].strip()); + for sMixed, sDefault, sDesc in [('Builds', sDefBuilds, 'builds'), ('TestRsrc', sDefTestRsrc, 'test resources') ]: + sLower = sMixed.lower(); + sPrefix = 's' + sMixed; + parser.add_option('--' + sLower + '-path', + dest=sPrefix + 'Path', metavar='<abs-path>', default=sDefault, + help='Where ' + sDesc + ' can be found'); + parser.add_option('--' + sLower + '-server-type', + dest=sPrefix + 'ServerType', metavar='<nfs|cifs>', default=sDefShareType, + help='The type of server, cifs (default) or nfs. If empty, we won\'t try mount anything.'); + parser.add_option('--' + sLower + '-server-name', + dest=sPrefix + 'ServerName', metavar='<server>', + default='vboxstor.de.oracle.com' if sLower == 'builds' else 'teststor.de.oracle.com', + help='The name of the server with the builds.'); + parser.add_option('--' + sLower + '-server-share', + dest=sPrefix + 'ServerShare', metavar='<share>', default=sLower, + help='The name of the builds share.'); + parser.add_option('--' + sLower + '-server-user', + dest=sPrefix + 'ServerUser', metavar='<user>', default='guestr', + help='The user name to use when accessing the ' + sDesc + ' share.'); + parser.add_option('--' + sLower + '-server-passwd', '--' + sLower + '-server-password', + dest=sPrefix + 'ServerPasswd', metavar='<password>', default='guestr', + help='The password to use when accessing the ' + sDesc + ' share.'); + parser.add_option('--' + sLower + '-server-mountopt', + dest=sPrefix + 'ServerMountOpt', metavar='<mountopt>', default='', + help='The mount options to use when accessing the ' + sDesc + ' share.'); + + parser.add_option("--test-manager", metavar="<url>", + dest="sTestManagerUrl", + help="Test Manager URL", + default="http://tindertux.de.oracle.com/testmanager") + parser.add_option("--scratch-root", metavar="<abs-path>", + dest="sScratchRoot", + help="Path to the scratch directory", + default=None) + parser.add_option("--system-uuid", metavar="<uuid>", + dest="sSystemUuid", + help="The system UUID of the testbox, used for uniquely identifiying the machine", + default=None) + parser.add_option("--hwvirt", + dest="fHasHwVirt", action="store_true", default=None, + help="Hardware virtualization available in the CPU"); + parser.add_option("--no-hwvirt", + dest="fHasHwVirt", action="store_false", default=None, + help="Hardware virtualization not available in the CPU"); + parser.add_option("--nested-paging", + dest="fHasNestedPaging", action="store_true", default=None, + help="Nested paging is available"); + parser.add_option("--no-nested-paging", + dest="fHasNestedPaging", action="store_false", default=None, + help="Nested paging is not available"); + parser.add_option("--64-bit-guest", + dest="fCan64BitGuest", action="store_true", default=None, + help="Host can execute 64-bit guests"); + parser.add_option("--no-64-bit-guest", + dest="fCan64BitGuest", action="store_false", default=None, + help="Host cannot execute 64-bit guests"); + parser.add_option("--io-mmu", + dest="fHasIoMmu", action="store_true", default=None, + help="I/O MMU available"); + parser.add_option("--no-io-mmu", + dest="fHasIoMmu", action="store_false", default=None, + help="No I/O MMU available"); + parser.add_option("--raw-mode", + dest="fWithRawMode", action="store_true", default=None, + help="Use raw-mode on this host."); + parser.add_option("--no-raw-mode", + dest="fWithRawMode", action="store_false", default=None, + help="Disables raw-mode tests on this host."); + parser.add_option("--pidfile", + dest="sPidFile", default=None, + help="For the parent script, ignored."); + parser.add_option("-E", "--putenv", metavar = "<variable>=<value>", action = "append", + dest = "asEnvVars", default = [], + help = "Sets an environment variable. Can be repeated."); + + (oOptions, args) = parser.parse_args() + # Check command line + if args != []: + parser.print_help(); + return TBS_EXITCODE_SYNTAX; + + if oOptions.sSystemUuid is not None: + uuid.UUID(oOptions.sSystemUuid); + if not oOptions.sTestManagerUrl.startswith('http://') \ + and not oOptions.sTestManagerUrl.startswith('https://'): + print('Syntax error: Invalid test manager URL "%s"' % (oOptions.sTestManagerUrl,)); + return TBS_EXITCODE_SYNTAX; + + for sPrefix in ['sBuilds', 'sTestRsrc']: + sType = getattr(oOptions, sPrefix + 'ServerType'); + if sType is None or not sType.strip(): + setattr(oOptions, sPrefix + 'ServerType', None); + elif sType not in ['cifs', 'nfs']: + print('Syntax error: Invalid server type "%s"' % (sType,)); + return TBS_EXITCODE_SYNTAX; + + + # + # Instantiate the testbox script and start dispatching work. + # + try: + oTestBoxScript = TestBoxScript(oOptions); + except TestBoxScriptException as oXcpt: + print('Error: %s' % (oXcpt,)); + return TBS_EXITCODE_SYNTAX; + oTestBoxScript.dispatch(); + + # Not supposed to get here... + return TBS_EXITCODE_FAILURE; + + + +if __name__ == '__main__': + sys.exit(TestBoxScript.main()); + |