diff options
Diffstat (limited to '')
-rwxr-xr-x | src/VBox/ValidationKit/testdriver/vboxinstaller.py | 1251 |
1 files changed, 1251 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testdriver/vboxinstaller.py b/src/VBox/ValidationKit/testdriver/vboxinstaller.py new file mode 100755 index 00000000..6c72ac91 --- /dev/null +++ b/src/VBox/ValidationKit/testdriver/vboxinstaller.py @@ -0,0 +1,1251 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +VirtualBox Installer Wrapper Driver. + +This installs VirtualBox, starts a sub driver which does the real testing, +and then uninstall VirtualBox afterwards. This reduces the complexity of the +other VBox test drivers. +""" + +__copyright__ = \ +""" +Copyright (C) 2010-2023 Oracle and/or its affiliates. + +This file is part of VirtualBox base platform packages, as +available from https://www.virtualbox.org. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, in version 3 of the +License. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, see <https://www.gnu.org/licenses>. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL), a copy of it is provided in the "COPYING.CDDL" file included +in the VirtualBox 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. + +SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +""" +__version__ = "$Revision: 155244 $" + + +# Standard Python imports. +import os +import sys +import re +import socket +import tempfile +import time + +# Only the main script needs to modify the path. +try: __file__ +except: __file__ = sys.argv[0]; +g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))); +sys.path.append(g_ksValidationKitDir); + +# Validation Kit imports. +from common import utils, webutils; +from common.constants import rtexitcode; +from testdriver import reporter; +from testdriver.base import TestDriverBase; + + + +class VBoxInstallerTestDriver(TestDriverBase): + """ + Implementation of a top level test driver. + """ + + + ## State file indicating that we've skipped installation. + ksVar_Skipped = 'vboxinstaller-skipped'; + + + def __init__(self): + TestDriverBase.__init__(self); + self._asSubDriver = []; # The sub driver and it's arguments. + self._asBuildUrls = []; # The URLs passed us on the command line. + self._asBuildFiles = []; # The downloaded file names. + self._fUnpackedBuildFiles = False; + self._fAutoInstallPuelExtPack = True; + self._fKernelDrivers = True; + self._fWinForcedInstallTimestampCA = True; + self._fInstallMsCrt = False; # By default we don't install the Microsoft CRT (only needed once). + + # + # Base method we override + # + + def showUsage(self): + rc = TestDriverBase.showUsage(self); + # 0 1 2 3 4 5 6 7 8 + # 012345678901234567890123456789012345678901234567890123456789012345678901234567890 + reporter.log(''); + reporter.log('vboxinstaller Options:'); + reporter.log(' --vbox-build <url[,url2[,...]]>'); + reporter.log(' Comma separated list of URL to file to download and install or/and'); + reporter.log(' unpack. URLs without a schema are assumed to be files on the'); + reporter.log(' build share and will be copied off it.'); + reporter.log(' --no-puel-extpack'); + reporter.log(' Indicates that the PUEL extension pack should not be installed if found.'); + reporter.log(' The default is to install it when found in the vbox-build.'); + reporter.log(' --no-kernel-drivers'); + reporter.log(' Indicates that the kernel drivers should not be installed on platforms'); + reporter.log(' where this is optional. The default is to install them.'); + reporter.log(' --forced-win-install-timestamp-ca, --no-forced-win-install-timestamp-ca'); + reporter.log(' Whether to force installation of the legacy Windows timestamp CA.'); + reporter.log(' If not forced, it will only installed on the hosts that needs it.'); + reporter.log(' Default: --no-forced-win-install-timestamp-ca'); + reporter.log(' --win-install-mscrt, --no-win-install-mscrt'); + reporter.log(' Whether to install the MS Visual Studio Redistributable.'); + reporter.log(' Default: --no-win-install-mscrt'); + reporter.log(' --'); + reporter.log(' Indicates the end of our parameters and the start of the sub'); + reporter.log(' testdriver and its arguments.'); + return rc; + + def parseOption(self, asArgs, iArg): + """ + Parse our arguments. + """ + if asArgs[iArg] == '--': + # End of our parameters and start of the sub driver invocation. + iArg = self.requireMoreArgs(1, asArgs, iArg); + assert not self._asSubDriver; + self._asSubDriver = asArgs[iArg:]; + self._asSubDriver[0] = self._asSubDriver[0].replace('/', os.path.sep); + iArg = len(asArgs) - 1; + elif asArgs[iArg] == '--vbox-build': + # List of files to copy/download and install. + iArg = self.requireMoreArgs(1, asArgs, iArg); + self._asBuildUrls = asArgs[iArg].split(','); + elif asArgs[iArg] == '--no-puel-extpack': + self._fAutoInstallPuelExtPack = False; + elif asArgs[iArg] == '--puel-extpack': + self._fAutoInstallPuelExtPack = True; + elif asArgs[iArg] == '--no-kernel-drivers': + self._fKernelDrivers = False; + elif asArgs[iArg] == '--kernel-drivers': + self._fKernelDrivers = True; + elif asArgs[iArg] == '--no-forced-win-install-timestamp-ca': + self._fWinForcedInstallTimestampCA = False; + elif asArgs[iArg] == '--forced-win-install-timestamp-ca': + self._fWinForcedInstallTimestampCA = True; + elif asArgs[iArg] == '--no-win-install-mscrt': + self._fInstallMsCrt = False; + elif asArgs[iArg] == '--win-install-mscrt': + self._fInstallMsCrt = True; + else: + return TestDriverBase.parseOption(self, asArgs, iArg); + return iArg + 1; + + def completeOptions(self): + # + # Check that we've got what we need. + # + if not self._asBuildUrls: + reporter.error('No build files specified ("--vbox-build file1[,file2[...]]")'); + return False; + if not self._asSubDriver: + reporter.error('No sub testdriver specified. (" -- test/stuff/tdStuff1.py args")'); + return False; + + # + # Construct _asBuildFiles as an array parallel to _asBuildUrls. + # + for sUrl in self._asBuildUrls: + sDstFile = os.path.join(self.sScratchPath, webutils.getFilename(sUrl)); + self._asBuildFiles.append(sDstFile); + + return TestDriverBase.completeOptions(self); + + def actionExtract(self): + reporter.error('vboxinstall does not support extracting resources, you have to do that using the sub testdriver.'); + return False; + + def actionCleanupBefore(self): + """ + Kills all VBox process we see. + + This is only supposed to execute on a testbox so we don't need to go + all complicated wrt other users. + """ + return self._killAllVBoxProcesses(); + + def actionConfig(self): + """ + Install VBox and pass on the configure request to the sub testdriver. + """ + fRc = self._installVBox(); + if fRc is None: + self._persistentVarSet(self.ksVar_Skipped, 'true'); + self.fBadTestbox = True; + else: + self._persistentVarUnset(self.ksVar_Skipped); + + ## @todo vbox.py still has bugs preventing us from invoking it seperately with each action. + if fRc is True and 'execute' not in self.asActions and 'all' not in self.asActions: + fRc = self._executeSubDriver([ 'verify', ]); + if fRc is True and 'execute' not in self.asActions and 'all' not in self.asActions: + fRc = self._executeSubDriver([ 'config', ], fPreloadASan = True); + return fRc; + + def actionExecute(self): + """ + Execute the sub testdriver. + """ + return self._executeSubDriver(self.asActions, fPreloadASan = True); + + def actionCleanupAfter(self): + """ + Forward this to the sub testdriver, then uninstall VBox. + """ + fRc = True; + if 'execute' not in self.asActions and 'all' not in self.asActions: + fRc = self._executeSubDriver([ 'cleanup-after', ], fMaySkip = False); + + if not self._killAllVBoxProcesses(): + fRc = False; + + if not self._uninstallVBox(self._persistentVarExists(self.ksVar_Skipped)): + fRc = False; + + if utils.getHostOs() == 'darwin': + self._darwinUnmountDmg(fIgnoreError = True); # paranoia + + if not TestDriverBase.actionCleanupAfter(self): + fRc = False; + + return fRc; + + + def actionAbort(self): + """ + Forward this to the sub testdriver first, then wipe all VBox like + processes, and finally do the pid file processing (again). + """ + fRc1 = self._executeSubDriver([ 'abort', ], fMaySkip = False, fPreloadASan = True); + fRc2 = self._killAllVBoxProcesses(); + fRc3 = TestDriverBase.actionAbort(self); + return fRc1 and fRc2 and fRc3; + + + # + # Persistent variables. + # + ## @todo integrate into the base driver. Persistent accross scratch wipes? + + def __persistentVarCalcName(self, sVar): + """Returns the (full) filename for the given persistent variable.""" + assert re.match(r'^[a-zA-Z0-9_-]*$', sVar) is not None; + return os.path.join(self.sScratchPath, 'persistent-%s.var' % (sVar,)); + + def _persistentVarSet(self, sVar, sValue = ''): + """ + Sets a persistent variable. + + Returns True on success, False + reporter.error on failure. + + May raise exception if the variable name is invalid or something + unexpected happens. + """ + sFull = self.__persistentVarCalcName(sVar); + try: + with open(sFull, 'w') as oFile: # pylint: disable=unspecified-encoding + if sValue: + oFile.write(sValue.encode('utf-8')); + except: + reporter.errorXcpt('Error creating "%s"' % (sFull,)); + return False; + return True; + + def _persistentVarUnset(self, sVar): + """ + Unsets a persistent variable. + + Returns True on success, False + reporter.error on failure. + + May raise exception if the variable name is invalid or something + unexpected happens. + """ + sFull = self.__persistentVarCalcName(sVar); + if os.path.exists(sFull): + try: + os.unlink(sFull); + except: + reporter.errorXcpt('Error unlinking "%s"' % (sFull,)); + return False; + return True; + + def _persistentVarExists(self, sVar): + """ + Checks if a persistent variable exists. + + Returns true/false. + + May raise exception if the variable name is invalid or something + unexpected happens. + """ + return os.path.exists(self.__persistentVarCalcName(sVar)); + + def _persistentVarGet(self, sVar): + """ + Gets the value of a persistent variable. + + Returns variable value on success. + Returns None if the variable doesn't exist or if an + error (reported) occured. + + May raise exception if the variable name is invalid or something + unexpected happens. + """ + sFull = self.__persistentVarCalcName(sVar); + if not os.path.exists(sFull): + return None; + try: + with open(sFull, 'r') as oFile: # pylint: disable=unspecified-encoding + sValue = oFile.read().decode('utf-8'); + except: + reporter.errorXcpt('Error creating "%s"' % (sFull,)); + return None; + return sValue; + + + # + # Helpers. + # + + def _killAllVBoxProcesses(self): + """ + Kills all virtual box related processes we find in the system. + """ + sHostOs = utils.getHostOs(); + asDebuggers = [ 'cdb', 'windbg', ] if sHostOs == 'windows' else [ 'gdb', 'gdb-i386-apple-darwin', 'lldb' ]; + + for iIteration in range(22): + # Gather processes to kill. + aoTodo = []; + aoDebuggers = []; + for oProcess in utils.processListAll(): + sBase = oProcess.getBaseImageNameNoExeSuff(); + if sBase is None: + continue; + sBase = sBase.lower(); + if sBase in [ 'vboxsvc', 'vboxsds', 'virtualbox', 'virtualboxvm', 'vboxheadless', 'vboxmanage', 'vboxsdl', + 'vboxwebsrv', 'vboxautostart', 'vboxballoonctrl', 'vboxbfe', 'vboxextpackhelperapp', 'vboxnetdhcp', + 'vboxnetnat', 'vboxnetadpctl', 'vboxtestogl', 'vboxtunctl', 'vboxvmmpreload', 'vboxxpcomipcd', ]: + aoTodo.append(oProcess); + if sBase.startswith('virtualbox-') and sBase.endswith('-multiarch.exe'): + aoTodo.append(oProcess); + if sBase in asDebuggers: + aoDebuggers.append(oProcess); + if iIteration in [0, 21]: + reporter.log('Warning: debugger running: %s (%s %s)' % (oProcess.iPid, sBase, oProcess.asArgs)); + if not aoTodo: + return True; + + # Are any of the debugger processes hooked up to a VBox process? + if sHostOs == 'windows': + # On demand debugging windows: windbg -p <decimal-pid> -e <decimal-event> -g + for oDebugger in aoDebuggers: + for oProcess in aoTodo: + # The whole command line is asArgs[0] here. Fix if that changes. + if oDebugger.asArgs and oDebugger.asArgs[0].find('-p %s ' % (oProcess.iPid,)) >= 0: + aoTodo.append(oDebugger); + break; + else: + for oDebugger in aoDebuggers: + for oProcess in aoTodo: + # Simplistic approach: Just check for argument equaling our pid. + if oDebugger.asArgs and ('%s' % oProcess.iPid) in oDebugger.asArgs: + aoTodo.append(oDebugger); + break; + + # Kill. + for oProcess in aoTodo: + reporter.log('Loop #%d - Killing %s (%s, uid=%s)' + % ( iIteration, oProcess.iPid, oProcess.sImage if oProcess.sName is None else oProcess.sName, + oProcess.iUid, )); + if not utils.processKill(oProcess.iPid) \ + and sHostOs != 'windows' \ + and utils.processExists(oProcess.iPid): + # Many of the vbox processes are initially set-uid-to-root and associated debuggers are running + # via sudo, so we might not be able to kill them unless we sudo and use /bin/kill. + try: utils.sudoProcessCall(['/bin/kill', '-9', '%s' % (oProcess.iPid,)]); + except: reporter.logXcpt(); + + # Check if they're all dead like they should be. + time.sleep(0.1); + for oProcess in aoTodo: + if utils.processExists(oProcess.iPid): + time.sleep(2); + break; + + return False; + + def _executeSync(self, asArgs, fMaySkip = False): + """ + Executes a child process synchronously. + + Returns True if the process executed successfully and returned 0. + Returns None if fMaySkip is true and the child exits with RTEXITCODE_SKIPPED. + Returns False for all other cases. + """ + reporter.log('Executing: %s' % (asArgs, )); + reporter.flushall(); + try: + iRc = utils.processCall(asArgs, shell = False, close_fds = False); + except: + reporter.errorXcpt(); + return False; + reporter.log('Exit code: %s (%s)' % (iRc, asArgs)); + if fMaySkip and iRc == rtexitcode.RTEXITCODE_SKIPPED: + return None; + return iRc == 0; + + def _sudoExecuteSync(self, asArgs): + """ + Executes a sudo child process synchronously. + Returns a tuple [True, 0] if the process executed successfully + and returned 0, otherwise [False, rc] is returned. + """ + reporter.log('Executing [sudo]: %s' % (asArgs, )); + reporter.flushall(); + iRc = 0; + try: + iRc = utils.sudoProcessCall(asArgs, shell = False, close_fds = False); + except: + reporter.errorXcpt(); + return (False, 0); + reporter.log('Exit code [sudo]: %s (%s)' % (iRc, asArgs)); + return (iRc == 0, iRc); + + def _findASanLibsForASanBuild(self): + """ + Returns a list of (address) santizier related libraries to preload + when launching the sub driver. + Returns empty list for non-asan builds or on platforms where this isn't needed. + """ + # Note! We include libasan.so.X in the VBoxAll tarball for asan builds, so we + # can use its presence both to detect an 'asan' build and to return it. + # Only the libasan.so.X library needs preloading at present. + if self.sHost in ('linux',): + sLibASan = self._findFile(r'libasan\.so\..*'); + if sLibASan: + return [sLibASan,]; + return []; + + def _executeSubDriver(self, asActions, fMaySkip = True, fPreloadASan = True): + """ + Execute the sub testdriver with the specified action. + """ + asArgs = list(self._asSubDriver) + asArgs.append('--no-wipe-clean'); + asArgs.extend(asActions); + + asASanLibs = []; + if fPreloadASan: + asASanLibs = self._findASanLibsForASanBuild(); + if asASanLibs: + os.environ['LD_PRELOAD'] = ':'.join(asASanLibs); + os.environ['LSAN_OPTIONS'] = 'detect_leaks=0'; # We don't want python leaks. vbox.py disables this. + + # Because of https://github.com/google/sanitizers/issues/856 we must try use setarch to disable + # address space randomization. + + reporter.log('LD_PRELOAD...') + if utils.getHostArch() == 'amd64': + sSetArch = utils.whichProgram('setarch'); + reporter.log('sSetArch=%s' % (sSetArch,)); + if sSetArch: + asArgs = [ sSetArch, 'x86_64', '-R', sys.executable ] + asArgs; + reporter.log('asArgs=%s' % (asArgs,)); + + rc = self._executeSync(asArgs, fMaySkip = fMaySkip); + + del os.environ['LSAN_OPTIONS']; + del os.environ['LD_PRELOAD']; + return rc; + + return self._executeSync(asArgs, fMaySkip = fMaySkip); + + def _maybeUnpackArchive(self, sMaybeArchive, fNonFatal = False): + """ + Attempts to unpack the given build file. + Updates _asBuildFiles. + Returns True/False. No exceptions. + """ + def unpackFilter(sMember): + # type: (string) -> bool + """ Skips debug info. """ + sLower = sMember.lower(); + if sLower.endswith('.pdb'): + return False; + return True; + + asMembers = utils.unpackFile(sMaybeArchive, self.sScratchPath, reporter.log, + reporter.log if fNonFatal else reporter.error, + fnFilter = unpackFilter); + if asMembers is None: + return False; + self._asBuildFiles.extend(asMembers); + return True; + + + def _installVBox(self): + """ + Download / copy the build files into the scratch area and install them. + """ + reporter.testStart('Installing VirtualBox'); + reporter.log('CWD=%s' % (os.getcwd(),)); # curious + + # + # Download the build files. + # + for i, sBuildUrl in enumerate(self._asBuildUrls): + if webutils.downloadFile(sBuildUrl, self._asBuildFiles[i], self.sBuildPath, reporter.log, reporter.log) is not True: + reporter.testDone(fSkipped = True); + return None; # Failed to get binaries, probably deleted. Skip the test run. + + # + # Unpack anything we know what is and append it to the build files + # list. This allows us to use VBoxAll*.tar.gz files. + # + for sFile in list(self._asBuildFiles): # Note! We copy the list as _maybeUnpackArchive updates it. + if self._maybeUnpackArchive(sFile, fNonFatal = True) is not True: + reporter.testDone(fSkipped = True); + return None; # Failed to unpack. Probably local error, like busy + # DLLs on windows, no reason for failing the build. + self._fUnpackedBuildFiles = True; + + # + # Go to system specific installation code. + # + sHost = utils.getHostOs() + if sHost == 'darwin': fRc = self._installVBoxOnDarwin(); + elif sHost == 'linux': fRc = self._installVBoxOnLinux(); + elif sHost == 'solaris': fRc = self._installVBoxOnSolaris(); + elif sHost == 'win': fRc = self._installVBoxOnWindows(); + else: + reporter.error('Unsupported host "%s".' % (sHost,)); + if fRc is False: + reporter.testFailure('Installation error.'); + elif fRc is not True: + reporter.log('Seems installation was skipped. Old version lurking behind? Not the fault of this build/test run!'); + + # + # Install the extension pack. + # + if fRc is True and self._fAutoInstallPuelExtPack: + fRc = self._installExtPack(); + if fRc is False: + reporter.testFailure('Extension pack installation error.'); + + # Some debugging... + try: + cMbFreeSpace = utils.getDiskUsage(self.sScratchPath); + reporter.log('Disk usage after VBox install: %d MB available at %s' % (cMbFreeSpace, self.sScratchPath,)); + except: + reporter.logXcpt('Unable to get disk free space. Ignored. Continuing.'); + + reporter.testDone(fRc is None); + return fRc; + + def _uninstallVBox(self, fIgnoreError = False): + """ + Uninstall VirtualBox. + """ + reporter.testStart('Uninstalling VirtualBox'); + + sHost = utils.getHostOs() + if sHost == 'darwin': fRc = self._uninstallVBoxOnDarwin(); + elif sHost == 'linux': fRc = self._uninstallVBoxOnLinux(); + elif sHost == 'solaris': fRc = self._uninstallVBoxOnSolaris(True); + elif sHost == 'win': fRc = self._uninstallVBoxOnWindows('uninstall'); + else: + reporter.error('Unsupported host "%s".' % (sHost,)); + if fRc is False and not fIgnoreError: + reporter.testFailure('Uninstallation failed.'); + + fRc2 = self._uninstallAllExtPacks(); + if not fRc2 and fRc: + fRc = fRc2; + + reporter.testDone(fSkipped = (fRc is None)); + return fRc; + + def _findFile(self, sRegExp, fMandatory = False): + """ + Returns the first build file that matches the given regular expression + (basename only). + + Returns None if no match was found, logging it as an error if + fMandatory is set. + """ + oRegExp = re.compile(sRegExp); + + reporter.log('_findFile: %s' % (sRegExp,)); + for sFile in self._asBuildFiles: + if oRegExp.match(os.path.basename(sFile)) and os.path.exists(sFile): + return sFile; + + # If we didn't unpack the build files, search all the files in the scratch area: + if not self._fUnpackedBuildFiles: + for sDir, _, asFiles in os.walk(self.sScratchPath): + for sFile in asFiles: + #reporter.log('_findFile: considering %s' % (sFile,)); + if oRegExp.match(sFile): + return os.path.join(sDir, sFile); + + if fMandatory: + reporter.error('Failed to find a file matching "%s" in %s.' % (sRegExp, self._asBuildFiles,)); + return None; + + def _waitForTestManagerConnectivity(self, cSecTimeout): + """ + Check and wait for network connectivity to the test manager. + + This is used with the windows installation and uninstallation since + these usually disrupts network connectivity when installing the filter + driver. If we proceed to quickly, we might finish the test at a time + when we cannot report to the test manager and thus end up with an + abandonded test error. + """ + cSecElapsed = 0; + secStart = utils.timestampSecond(); + while reporter.checkTestManagerConnection() is False: + cSecElapsed = utils.timestampSecond() - secStart; + if cSecElapsed >= cSecTimeout: + reporter.log('_waitForTestManagerConnectivity: Giving up after %u secs.' % (cSecTimeout,)); + return False; + time.sleep(2); + + if cSecElapsed > 0: + reporter.log('_waitForTestManagerConnectivity: Waited %s secs.' % (cSecTimeout,)); + return True; + + + # + # Darwin (Mac OS X). + # + + def _darwinDmgPath(self): + """ Returns the path to the DMG mount.""" + return os.path.join(self.sScratchPath, 'DmgMountPoint'); + + def _darwinUnmountDmg(self, fIgnoreError): + """ + Umount any DMG on at the default mount point. + """ + sMountPath = self._darwinDmgPath(); + if not os.path.exists(sMountPath): + return True; + + # Unmount. + fRc = self._executeSync(['hdiutil', 'detach', sMountPath ]); + if not fRc and not fIgnoreError: + # In case it's busy for some reason or another, just retry after a little delay. + for iTry in range(6): + time.sleep(5); + reporter.error('Retry #%s unmount DMT at %s' % (iTry + 1, sMountPath,)); + fRc = self._executeSync(['hdiutil', 'detach', sMountPath ]); + if fRc: + break; + if not fRc: + reporter.error('Failed to unmount DMG at %s' % (sMountPath,)); + + # Remove dir. + try: + os.rmdir(sMountPath); + except: + if not fIgnoreError: + reporter.errorXcpt('Failed to remove directory %s' % (sMountPath,)); + return fRc; + + def _darwinMountDmg(self, sDmg): + """ + Mount the DMG at the default mount point. + """ + self._darwinUnmountDmg(fIgnoreError = True) + + sMountPath = self._darwinDmgPath(); + if not os.path.exists(sMountPath): + try: + os.mkdir(sMountPath, 0o755); + except: + reporter.logXcpt(); + return False; + + return self._executeSync(['hdiutil', 'attach', '-readonly', '-mount', 'required', '-mountpoint', sMountPath, sDmg, ]); + + def _generateWithoutKextsChoicesXmlOnDarwin(self): + """ + Generates the choices XML when kernel drivers are disabled. + None is returned on failure. + """ + sPath = os.path.join(self.sScratchPath, 'DarwinChoices.xml'); + oFile = utils.openNoInherit(sPath, 'wt'); + oFile.write('<?xml version="1.0" encoding="UTF-8"?>\n' + '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n' + '<plist version="1.0">\n' + '<array>\n' + ' <dict>\n' + ' <key>attributeSetting</key>\n' + ' <integer>0</integer>\n' + ' <key>choiceAttribute</key>\n' + ' <string>selected</string>\n' + ' <key>choiceIdentifier</key>\n' + ' <string>choiceVBoxKEXTs</string>\n' + ' </dict>\n' + '</array>\n' + '</plist>\n'); + oFile.close(); + return sPath; + + def _installVBoxOnDarwin(self): + """ Installs VBox on Mac OS X.""" + + # TEMPORARY HACK - START + # Don't install the kernel drivers on the testboxes with BigSur and later + # Needs a more generic approach but that one needs more effort. + sHostName = socket.getfqdn(); + if sHostName.startswith('testboxmac10') \ + or sHostName.startswith('testboxmac11'): + self._fKernelDrivers = False; + # TEMPORARY HACK - END + + sDmg = self._findFile('^VirtualBox-.*\\.dmg$'); + if sDmg is None: + return False; + + # Mount the DMG. + fRc = self._darwinMountDmg(sDmg); + if fRc is not True: + return False; + + # Uninstall any previous vbox version first. + sUninstaller = os.path.join(self._darwinDmgPath(), 'VirtualBox_Uninstall.tool'); + fRc, _ = self._sudoExecuteSync([sUninstaller, '--unattended',]); + if fRc is True: + + # Install the package. + sPkg = os.path.join(self._darwinDmgPath(), 'VirtualBox.pkg'); + if self._fKernelDrivers: + fRc, _ = self._sudoExecuteSync(['installer', '-verbose', '-dumplog', '-pkg', sPkg, '-target', '/']); + else: + sChoicesXml = self._generateWithoutKextsChoicesXmlOnDarwin(); + if sChoicesXml is not None: + fRc, _ = self._sudoExecuteSync(['installer', '-verbose', '-dumplog', '-pkg', sPkg, \ + '-applyChoiceChangesXML', sChoicesXml, '-target', '/']); + else: + fRc = False; + + # Unmount the DMG and we're done. + if not self._darwinUnmountDmg(fIgnoreError = False): + fRc = False; + return fRc; + + def _uninstallVBoxOnDarwin(self): + """ Uninstalls VBox on Mac OS X.""" + + # Is VirtualBox installed? If not, don't try uninstall it. + sVBox = self._getVBoxInstallPath(fFailIfNotFound = False); + if sVBox is None: + return True; + + # Find the dmg. + sDmg = self._findFile('^VirtualBox-.*\\.dmg$'); + if sDmg is None: + return False; + if not os.path.exists(sDmg): + return True; + + # Mount the DMG. + fRc = self._darwinMountDmg(sDmg); + if fRc is not True: + return False; + + # Execute the uninstaller. + sUninstaller = os.path.join(self._darwinDmgPath(), 'VirtualBox_Uninstall.tool'); + fRc, _ = self._sudoExecuteSync([sUninstaller, '--unattended',]); + + # Unmount the DMG and we're done. + if not self._darwinUnmountDmg(fIgnoreError = False): + fRc = False; + return fRc; + + # + # GNU/Linux + # + + def _installVBoxOnLinux(self): + """ Installs VBox on Linux.""" + sRun = self._findFile('^VirtualBox-.*\\.run$'); + if sRun is None: + return False; + utils.chmodPlusX(sRun); + + # Install the new one. + fRc, _ = self._sudoExecuteSync([sRun,]); + return fRc; + + def _uninstallVBoxOnLinux(self): + """ Uninstalls VBox on Linux.""" + + # Is VirtualBox installed? If not, don't try uninstall it. + sVBox = self._getVBoxInstallPath(fFailIfNotFound = False); + if sVBox is None: + return True; + + # Find the .run file and use it. + sRun = self._findFile('^VirtualBox-.*\\.run$', fMandatory = False); + if sRun is not None: + utils.chmodPlusX(sRun); + fRc, _ = self._sudoExecuteSync([sRun, 'uninstall']); + return fRc; + + # Try the installed uninstaller. + for sUninstaller in [os.path.join(sVBox, 'uninstall.sh'), '/opt/VirtualBox/uninstall.sh', ]: + if os.path.isfile(sUninstaller): + reporter.log('Invoking "%s"...' % (sUninstaller,)); + fRc, _ = self._sudoExecuteSync([sUninstaller, 'uninstall']); + return fRc; + + reporter.log('Did not find any VirtualBox install to uninstall.'); + return True; + + + # + # Solaris + # + + def _generateAutoResponseOnSolaris(self): + """ + Generates an autoresponse file on solaris, returning the name. + None is return on failure. + """ + sPath = os.path.join(self.sScratchPath, 'SolarisAutoResponse'); + oFile = utils.openNoInherit(sPath, 'wt'); + oFile.write('basedir=default\n' + 'runlevel=nocheck\n' + 'conflict=quit\n' + 'setuid=nocheck\n' + 'action=nocheck\n' + 'partial=quit\n' + 'instance=unique\n' + 'idepend=quit\n' + 'rdepend=quit\n' + 'space=quit\n' + 'mail=\n'); + oFile.close(); + return sPath; + + def _installVBoxOnSolaris(self): + """ Installs VBox on Solaris.""" + sPkg = self._findFile('^VirtualBox-.*\\.pkg$', fMandatory = False); + if sPkg is None: + sTar = self._findFile('^VirtualBox-.*-SunOS-.*\\.tar.gz$', fMandatory = False); + if sTar is not None: + if self._maybeUnpackArchive(sTar) is not True: + return False; + sPkg = self._findFile('^VirtualBox-.*\\.pkg$', fMandatory = True); + sRsp = self._findFile('^autoresponse$', fMandatory = True); + if sPkg is None or sRsp is None: + return False; + + # Uninstall first (ignore result). + self._uninstallVBoxOnSolaris(False); + + # Install the new one. + fRc, _ = self._sudoExecuteSync(['pkgadd', '-d', sPkg, '-n', '-a', sRsp, 'SUNWvbox']); + return fRc; + + def _uninstallVBoxOnSolaris(self, fRestartSvcConfigD): + """ Uninstalls VBox on Solaris.""" + reporter.flushall(); + if utils.processCall(['pkginfo', '-q', 'SUNWvbox']) != 0: + return True; + sRsp = self._generateAutoResponseOnSolaris(); + fRc, _ = self._sudoExecuteSync(['pkgrm', '-n', '-a', sRsp, 'SUNWvbox']); + + # + # Restart the svc.configd as it has a tendency to clog up with time and + # become unresponsive. It will handle SIGHUP by exiting the sigwait() + # look in the main function and shut down the service nicely (backend_fini). + # The restarter will then start a new instance of it. + # + if fRestartSvcConfigD: + time.sleep(1); # Give it a chance to flush pkgrm stuff. + self._sudoExecuteSync(['pkill', '-HUP', 'svc.configd']); + time.sleep(5); # Spare a few cpu cycles it to shutdown and restart. + + return fRc; + + # + # Windows + # + + ## VBox windows services we can query the status of. + kasWindowsServices = [ 'vboxsup', 'vboxusbmon', 'vboxnetadp', 'vboxnetflt', 'vboxnetlwf' ]; + + def _installVBoxOnWindows(self): + """ Installs VBox on Windows.""" + sExe = self._findFile('^VirtualBox-.*-(MultiArch|Win).exe$'); + if sExe is None: + return False; + + # TEMPORARY HACK - START + # It seems that running the NDIS cleanup script upon uninstallation is not + # a good idea, so let's run it before installing VirtualBox. + #sHostName = socket.getfqdn(); + #if not sHostName.startswith('testboxwin3') \ + # and not sHostName.startswith('testboxharp2') \ + # and not sHostName.startswith('wei01-b6ka-3') \ + # and utils.getHostOsVersion() in ['8', '8.1', '9', '2008Server', '2008ServerR2', '2012Server']: + # reporter.log('Peforming extra NDIS cleanup...'); + # sMagicScript = os.path.abspath(os.path.join(g_ksValidationKitDir, 'testdriver', 'win-vbox-net-uninstall.ps1')); + # fRc2, _ = self._sudoExecuteSync(['powershell.exe', '-Command', 'set-executionpolicy unrestricted']); + # if not fRc2: + # reporter.log('set-executionpolicy failed.'); + # self._sudoExecuteSync(['powershell.exe', '-Command', 'get-executionpolicy']); + # fRc2, _ = self._sudoExecuteSync(['powershell.exe', '-File', sMagicScript]); + # if not fRc2: + # reporter.log('NDIS cleanup failed.'); + # TEMPORARY HACK - END + + # Uninstall any previous vbox version first. + fRc = self._uninstallVBoxOnWindows('install'); + if fRc is not True: + return None; # There shouldn't be anything to uninstall, and if there is, it's not our fault. + + # Install the MS Visual Studio Redistributable, if requested. (VBox 7.0+ needs this installed once.) + if self._fInstallMsCrt: + reporter.log('Installing MS Visual Studio Redistributable (untested code)...'); + ## @todo Test this. + ## @todo We could cache this on the testrsrc share. + sName = "vc_redist.x64.exe" + sUrl = "https://aka.ms/vs/17/release/" + sName # Permalink, according to MS. + sExe = os.path.join(self.sBuildPath, sName); + if webutils.downloadFile(sUrl, sExe, None, reporter.log, reporter.log): + asArgs = [ sExe, '/Q' ]; + fRc2, iRc = self._sudoExecuteSync(asArgs); + if fRc2 is False: + return reporter.error('Installing MS Visual Studio Redistributable failed, exit code: %s' % (iRc,)); + reporter.log('Installing MS Visual Studio Redistributable done'); + else: + return False; + + # We need the help text to detect supported options below. + reporter.log('Executing: %s' % ([sExe, '--silent', '--help'], )); + reporter.flushall(); + (iExitCode, sHelp, _) = utils.processOutputUnchecked([sExe, '--silent', '--help'], fIgnoreEncoding = True); + reporter.log('Exit code: %d, %u chars of help text' % (iExitCode, len(sHelp),)); + + # Gather installer arguments. + asArgs = [sExe, '-vvvv', '--silent', '--logging']; + asArgs.extend(['--msiparams', 'REBOOT=ReallySuppress']); + sVBoxInstallPath = os.environ.get('VBOX_INSTALL_PATH', None); + if sVBoxInstallPath is not None: + asArgs.extend(['INSTALLDIR="%s"' % (sVBoxInstallPath,)]); + + if sHelp.find("--msi-log-file") >= 0: + sLogFile = os.path.join(self.sScratchPath, 'VBoxInstallLog.txt'); # Specify location to prevent a random one. + asArgs.extend(['--msi-log-file', sLogFile]); + else: + sLogFile = os.path.join(tempfile.gettempdir(), 'VirtualBox', 'VBoxInstallLog.txt'); # Hardcoded TMP location. + + if self._fWinForcedInstallTimestampCA and sHelp.find("--force-install-timestamp-ca") >= 0: + asArgs.extend(['--force-install-timestamp-ca']); + + # Install it. + fRc2, iRc = self._sudoExecuteSync(asArgs); + if fRc2 is False: + if iRc == 3010: # ERROR_SUCCESS_REBOOT_REQUIRED + reporter.error('Installer required a reboot to complete installation (ERROR_SUCCESS_REBOOT_REQUIRED)'); + else: + reporter.error('Installer failed, exit code: %s' % (iRc,)); + fRc = False; + + # Add the installer log if present and wait for the network connection to be restore after the filter driver upset. + if os.path.isfile(sLogFile): + reporter.addLogFile(sLogFile, 'log/installer', "Verbose MSI installation log file"); + self._waitForTestManagerConnectivity(30); + + return fRc; + + def _isProcessPresent(self, sName): + """ Checks whether the named process is present or not. """ + for oProcess in utils.processListAll(): + sBase = oProcess.getBaseImageNameNoExeSuff(); + if sBase is not None and sBase.lower() == sName: + return True; + return False; + + def _killProcessesByName(self, sName, sDesc, fChildren = False): + """ Kills the named process, optionally including children. """ + cKilled = 0; + aoProcesses = utils.processListAll(); + for oProcess in aoProcesses: + sBase = oProcess.getBaseImageNameNoExeSuff(); + if sBase is not None and sBase.lower() == sName: + reporter.log('Killing %s process: %s (%s)' % (sDesc, oProcess.iPid, sBase)); + utils.processKill(oProcess.iPid); + cKilled += 1; + + if fChildren: + for oChild in aoProcesses: + if oChild.iParentPid == oProcess.iPid and oChild.iParentPid is not None: + reporter.log('Killing %s child process: %s (%s)' % (sDesc, oChild.iPid, sBase)); + utils.processKill(oChild.iPid); + cKilled += 1; + return cKilled; + + def _terminateProcessesByNameAndArgSubstr(self, sName, sArg, sDesc): + """ + Terminates the named process using taskkill.exe, if any of its args + contains the passed string. + """ + cKilled = 0; + aoProcesses = utils.processListAll(); + for oProcess in aoProcesses: + sBase = oProcess.getBaseImageNameNoExeSuff(); + if sBase is not None and sBase.lower() == sName and any(sArg in s for s in oProcess.asArgs): + + reporter.log('Killing %s process: %s (%s)' % (sDesc, oProcess.iPid, sBase)); + self._executeSync(['taskkill.exe', '/pid', '%u' % (oProcess.iPid,)]); + cKilled += 1; + return cKilled; + + def _uninstallVBoxOnWindows(self, sMode): + """ + Uninstalls VBox on Windows, all installations we find to be on the safe side... + """ + assert sMode in ['install', 'uninstall',]; + + import win32com.client; # pylint: disable=import-error + win32com.client.gencache.EnsureModule('{000C1092-0000-0000-C000-000000000046}', 1033, 1, 0); + oInstaller = win32com.client.Dispatch('WindowsInstaller.Installer', + resultCLSID = '{000C1090-0000-0000-C000-000000000046}') + + # Search installed products for VirtualBox. + asProdCodes = []; + for sProdCode in oInstaller.Products: + try: + sProdName = oInstaller.ProductInfo(sProdCode, "ProductName"); + except: + reporter.logXcpt(); + continue; + #reporter.log('Info: %s=%s' % (sProdCode, sProdName)); + if sProdName.startswith('Oracle VM VirtualBox') \ + or sProdName.startswith('Sun VirtualBox'): + asProdCodes.append([sProdCode, sProdName]); + + # Before we start uninstalling anything, just ruthlessly kill any cdb, + # msiexec, drvinst and some rundll process we might find hanging around. + if self._isProcessPresent('rundll32'): + cTimes = 0; + while cTimes < 3: + cTimes += 1; + cKilled = self._terminateProcessesByNameAndArgSubstr('rundll32', 'InstallSecurityPromptRunDllW', + 'MSI driver installation'); + if cKilled <= 0: + break; + time.sleep(10); # Give related drvinst process a chance to clean up after we killed the verification dialog. + + if self._isProcessPresent('drvinst'): + time.sleep(15); # In the hope that it goes away. + cTimes = 0; + while cTimes < 4: + cTimes += 1; + cKilled = self._killProcessesByName('drvinst', 'MSI driver installation', True); + if cKilled <= 0: + break; + time.sleep(10); # Give related MSI process a chance to clean up after we killed the driver installer. + + if self._isProcessPresent('msiexec'): + cTimes = 0; + while cTimes < 3: + reporter.log('found running msiexec process, waiting a bit...'); + time.sleep(20) # In the hope that it goes away. + if not self._isProcessPresent('msiexec'): + break; + cTimes += 1; + ## @todo this could also be the msiexec system service, try to detect this case! + if cTimes >= 6: + cKilled = self._killProcessesByName('msiexec', 'MSI driver installation'); + if cKilled > 0: + time.sleep(16); # fudge. + + # cdb.exe sometimes stays running (from utils.getProcessInfo), blocking + # the scratch directory. No idea why. + if self._isProcessPresent('cdb'): + cTimes = 0; + while cTimes < 3: + cKilled = self._killProcessesByName('cdb', 'cdb.exe from getProcessInfo'); + if cKilled <= 0: + break; + time.sleep(2); # fudge. + + # Do the uninstalling. + fRc = True; + sLogFile = os.path.join(self.sScratchPath, 'VBoxUninstallLog.txt'); + for sProdCode, sProdName in asProdCodes: + reporter.log('Uninstalling %s (%s)...' % (sProdName, sProdCode)); + fRc2, iRc = self._sudoExecuteSync(['msiexec', '/uninstall', sProdCode, '/quiet', '/passive', '/norestart', + '/L*v', '%s' % (sLogFile), ]); + if fRc2 is False: + if iRc == 3010: # ERROR_SUCCESS_REBOOT_REQUIRED + reporter.error('Uninstaller required a reboot to complete uninstallation'); + else: + reporter.error('Uninstaller failed, exit code: %s' % (iRc,)); + fRc = False; + + self._waitForTestManagerConnectivity(30); + + # Upload the log on failure. Do it early if the extra cleanups below causes trouble. + if fRc is False and os.path.isfile(sLogFile): + reporter.addLogFile(sLogFile, 'log/uninstaller', "Verbose MSI uninstallation log file"); + sLogFile = None; + + # Log driver service states (should ls \Driver\VBox* and \Device\VBox*). + fHadLeftovers = False; + asLeftovers = []; + for sService in reversed(self.kasWindowsServices): + cTries = 0; + while True: + fRc2, _ = self._sudoExecuteSync(['sc.exe', 'query', sService]); + if not fRc2: + break; + fHadLeftovers = True; + + cTries += 1; + if cTries > 3: + asLeftovers.append(sService,); + break; + + # Get the status output. + try: + sOutput = utils.sudoProcessOutputChecked(['sc.exe', 'query', sService]); + except: + reporter.logXcpt(); + else: + if re.search(r'STATE\s+:\s*1\s*STOPPED', sOutput) is None: + reporter.log('Trying to stop %s...' % (sService,)); + fRc2, _ = self._sudoExecuteSync(['sc.exe', 'stop', sService]); + time.sleep(1); # fudge + + reporter.log('Trying to delete %s...' % (sService,)); + self._sudoExecuteSync(['sc.exe', 'delete', sService]); + + time.sleep(1); # fudge + + if asLeftovers: + reporter.log('Warning! Leftover VBox drivers: %s' % (', '.join(asLeftovers),)); + fRc = False; + + if fHadLeftovers: + self._waitForTestManagerConnectivity(30); + + # Upload the log if we have any leftovers and didn't upload it already. + if sLogFile is not None and (fRc is False or fHadLeftovers) and os.path.isfile(sLogFile): + reporter.addLogFile(sLogFile, 'log/uninstaller', "Verbose MSI uninstallation log file"); + + return fRc; + + + # + # Extension pack. + # + + def _getVBoxInstallPath(self, fFailIfNotFound): + """ Returns the default VBox installation path. """ + sHost = utils.getHostOs(); + if sHost == 'win': + sProgFiles = os.environ.get('ProgramFiles', 'C:\\Program Files'); + asLocs = [ + os.path.join(sProgFiles, 'Oracle', 'VirtualBox'), + os.path.join(sProgFiles, 'OracleVM', 'VirtualBox'), + os.path.join(sProgFiles, 'Sun', 'VirtualBox'), + ]; + elif sHost in ('linux', 'solaris',): + asLocs = [ '/opt/VirtualBox', '/opt/VirtualBox-3.2', '/opt/VirtualBox-3.1', '/opt/VirtualBox-3.0']; + elif sHost == 'darwin': + asLocs = [ '/Applications/VirtualBox.app/Contents/MacOS' ]; + else: + asLocs = [ '/opt/VirtualBox' ]; + if 'VBOX_INSTALL_PATH' in os.environ: + asLocs.insert(0, os.environ.get('VBOX_INSTALL_PATH', None)); + + for sLoc in asLocs: + if os.path.isdir(sLoc): + return sLoc; + if fFailIfNotFound: + reporter.error('Failed to locate VirtualBox installation: %s' % (asLocs,)); + else: + reporter.log2('Failed to locate VirtualBox installation: %s' % (asLocs,)); + return None; + + def _installExtPack(self): + """ Installs the extension pack. """ + sVBox = self._getVBoxInstallPath(fFailIfNotFound = True); + if sVBox is None: + return False; + sExtPackDir = os.path.join(sVBox, 'ExtensionPacks'); + + if self._uninstallAllExtPacks() is not True: + return False; + + sExtPack = self._findFile('Oracle_VM_VirtualBox_Extension_Pack.vbox-extpack'); + if sExtPack is None: + sExtPack = self._findFile('Oracle_VM_VirtualBox_Extension_Pack.*.vbox-extpack'); + if sExtPack is None: + return True; + + sDstDir = os.path.join(sExtPackDir, 'Oracle_VM_VirtualBox_Extension_Pack'); + reporter.log('Installing extension pack "%s" to "%s"...' % (sExtPack, sExtPackDir)); + fRc, _ = self._sudoExecuteSync([ self.getBinTool('vts_tar'), + '--extract', + '--verbose', + '--gzip', + '--file', sExtPack, + '--directory', sDstDir, + '--file-mode-and-mask', '0644', + '--file-mode-or-mask', '0644', + '--dir-mode-and-mask', '0755', + '--dir-mode-or-mask', '0755', + '--owner', '0', + '--group', '0', + ]); + return fRc; + + def _uninstallAllExtPacks(self): + """ Uninstalls all extension packs. """ + sVBox = self._getVBoxInstallPath(fFailIfNotFound = False); + if sVBox is None: + return True; + + sExtPackDir = os.path.join(sVBox, 'ExtensionPacks'); + if not os.path.exists(sExtPackDir): + return True; + + fRc, _ = self._sudoExecuteSync([self.getBinTool('vts_rm'), '-Rfv', '--', sExtPackDir]); + return fRc; + + + +if __name__ == '__main__': + sys.exit(VBoxInstallerTestDriver().main(sys.argv)); |