summaryrefslogtreecommitdiffstats
path: root/src/VBox/ValidationKit/testboxscript/testboxupgrade.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/VBox/ValidationKit/testboxscript/testboxupgrade.py')
-rwxr-xr-xsrc/VBox/ValidationKit/testboxscript/testboxupgrade.py339
1 files changed, 339 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testboxscript/testboxupgrade.py b/src/VBox/ValidationKit/testboxscript/testboxupgrade.py
new file mode 100755
index 00000000..765bff3c
--- /dev/null
+++ b/src/VBox/ValidationKit/testboxscript/testboxupgrade.py
@@ -0,0 +1,339 @@
+# -*- coding: utf-8 -*-
+# $Id: testboxupgrade.py $
+
+"""
+TestBox Script - Upgrade from local file ZIP.
+"""
+
+__copyright__ = \
+"""
+Copyright (C) 2012-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 shutil
+import sys
+import subprocess
+import threading
+import time
+import uuid;
+import zipfile
+
+# Validation Kit imports.
+from common import utils;
+import testboxcommons
+from testboxscript import TBS_EXITCODE_SYNTAX;
+
+# Figure where we are.
+try: __file__
+except: __file__ = sys.argv[0];
+g_ksTestScriptDir = os.path.dirname(os.path.abspath(__file__));
+g_ksValidationKitDir = os.path.dirname(g_ksTestScriptDir);
+
+
+def _doUpgradeThreadProc(oStdOut, asBuf):
+ """Thread procedure for the upgrade test drive."""
+ asBuf.append(oStdOut.read());
+ return True;
+
+
+def _doUpgradeCheckZip(oZip):
+ """
+ Check that the essential files are there.
+ Returns list of members on success, None on failure.
+ """
+ asMembers = oZip.namelist();
+ if ('testboxscript/testboxscript/testboxscript.py' not in asMembers) \
+ or ('testboxscript/testboxscript/testboxscript_real.py' not in asMembers):
+ testboxcommons.log('Missing one or both testboxscripts (members: %s)' % (asMembers,));
+ return None;
+
+ for sMember in asMembers:
+ if not sMember.startswith('testboxscript/'):
+ testboxcommons.log('zip file contains member outside testboxscript/: "%s"' % (sMember,));
+ return None;
+ if sMember.find('/../') > 0 or sMember.endswith('/..'):
+ testboxcommons.log('zip file contains member with escape sequence: "%s"' % (sMember,));
+ return None;
+
+ return asMembers;
+
+def _doUpgradeUnzipAndCheck(oZip, sUpgradeDir, asMembers):
+ """
+ Unzips the files into sUpdateDir, does chmod(755) on all files and
+ checks that there are no symlinks or special files.
+ Returns True/False.
+ """
+ #
+ # Extract the files.
+ #
+ if os.path.exists(sUpgradeDir):
+ shutil.rmtree(sUpgradeDir);
+ for sMember in asMembers:
+ if sMember.endswith('/'):
+ os.makedirs(os.path.join(sUpgradeDir, sMember.replace('/', os.path.sep)), 0o775);
+ else:
+ oZip.extract(sMember, sUpgradeDir);
+
+ #
+ # Make all files executable and make sure only owner can write to them.
+ # While at it, also check that there are only files and directory, no
+ # symbolic links or special stuff.
+ #
+ for sMember in asMembers:
+ sFull = os.path.join(sUpgradeDir, sMember);
+ if sMember.endswith('/'):
+ if not os.path.isdir(sFull):
+ testboxcommons.log('Not directory: "%s"' % sFull);
+ return False;
+ else:
+ if not os.path.isfile(sFull):
+ testboxcommons.log('Not regular file: "%s"' % sFull);
+ return False;
+ try:
+ os.chmod(sFull, 0o755);
+ except Exception as oXcpt:
+ testboxcommons.log('warning chmod error on %s: %s' % (sFull, oXcpt));
+ return True;
+
+def _doUpgradeTestRun(sUpgradeDir):
+ """
+ Do a testrun of the new script, to make sure it doesn't fail with
+ to run in any way because of old python, missing import or generally
+ busted upgrade.
+ Returns True/False.
+ """
+ asArgs = [os.path.join(sUpgradeDir, 'testboxscript', 'testboxscript', 'testboxscript.py'), '--version' ];
+ testboxcommons.log('Testing the new testbox script (%s)...' % (asArgs[0],));
+ if sys.executable:
+ asArgs.insert(0, sys.executable);
+ oChild = subprocess.Popen(asArgs, shell = False, # pylint: disable=consider-using-with
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT);
+
+ asBuf = []
+ oThread = threading.Thread(target=_doUpgradeThreadProc, args=(oChild.stdout, asBuf));
+ oThread.daemon = True;
+ oThread.start();
+ oThread.join(30);
+
+ # Give child up to 5 seconds to terminate after producing output.
+ if sys.version_info[0] >= 3 and sys.version_info[1] >= 3:
+ oChild.wait(5); # pylint: disable=too-many-function-args
+ else:
+ for _ in range(50):
+ iStatus = oChild.poll();
+ if iStatus is None:
+ break;
+ time.sleep(0.1);
+ iStatus = oChild.poll();
+ if iStatus is None:
+ testboxcommons.log('Checking the new testboxscript timed out.');
+ oChild.terminate();
+ oThread.join(5);
+ return False;
+ if iStatus is not TBS_EXITCODE_SYNTAX:
+ testboxcommons.log('The new testboxscript returned %d instead of %d during check.' \
+ % (iStatus, TBS_EXITCODE_SYNTAX));
+ return False;
+
+ sOutput = b''.join(asBuf).decode('utf-8');
+ sOutput = sOutput.strip();
+ try:
+ iNewVersion = int(sOutput);
+ except:
+ testboxcommons.log('The new testboxscript returned an unparseable version string: "%s"!' % (sOutput,));
+ return False;
+ testboxcommons.log('New script version: %s' % (iNewVersion,));
+ return True;
+
+def _doUpgradeApply(sUpgradeDir, asMembers):
+ """
+ # Apply the directories and files from the upgrade.
+ returns True/False/Exception.
+ """
+
+ #
+ # Create directories first since that's least intrusive.
+ #
+ for sMember in asMembers:
+ if sMember[-1] == '/':
+ sMember = sMember[len('testboxscript/'):];
+ if sMember != '':
+ sFull = os.path.join(g_ksValidationKitDir, sMember);
+ if not os.path.isdir(sFull):
+ os.makedirs(sFull, 0o755);
+
+ #
+ # Move the files into place.
+ #
+ fRc = True;
+ asOldFiles = [];
+ for sMember in asMembers:
+ if sMember[-1] != '/':
+ sSrc = os.path.join(sUpgradeDir, sMember);
+ sDst = os.path.join(g_ksValidationKitDir, sMember[len('testboxscript/'):]);
+
+ # Move the old file out of the way first.
+ sDstRm = None;
+ if os.path.exists(sDst):
+ testboxcommons.log2('Info: Installing "%s"' % (sDst,));
+ sDstRm = '%s-delete-me-%s' % (sDst, uuid.uuid4(),);
+ try:
+ os.rename(sDst, sDstRm);
+ except Exception as oXcpt:
+ testboxcommons.log('Error: failed to rename (old) "%s" to "%s": %s' % (sDst, sDstRm, oXcpt));
+ try:
+ shutil.copy(sDst, sDstRm);
+ except Exception as oXcpt:
+ testboxcommons.log('Error: failed to copy (old) "%s" to "%s": %s' % (sDst, sDstRm, oXcpt));
+ break;
+ try:
+ os.unlink(sDst);
+ except Exception as oXcpt:
+ testboxcommons.log('Error: failed to unlink (old) "%s": %s' % (sDst, oXcpt));
+ break;
+
+ # Move/copy the new one into place.
+ testboxcommons.log2('Info: Installing "%s"' % (sDst,));
+ try:
+ os.rename(sSrc, sDst);
+ except Exception as oXcpt:
+ testboxcommons.log('Warning: failed to rename (new) "%s" to "%s": %s' % (sSrc, sDst, oXcpt));
+ try:
+ shutil.copy(sSrc, sDst);
+ except:
+ testboxcommons.log('Error: failed to copy (new) "%s" to "%s": %s' % (sSrc, sDst, oXcpt));
+ fRc = False;
+ break;
+
+ #
+ # Roll back on failure.
+ #
+ if fRc is not True:
+ testboxcommons.log('Attempting to roll back old files...');
+ for sDstRm in asOldFiles:
+ sDst = sDstRm[:sDstRm.rfind('-delete-me')];
+ testboxcommons.log2('Info: Rolling back "%s" (%s)' % (sDst, os.path.basename(sDstRm)));
+ try:
+ shutil.move(sDstRm, sDst);
+ except:
+ testboxcommons.log('Error: failed to rollback "%s" onto "%s": %s' % (sDstRm, sDst, oXcpt));
+ return False;
+ return True;
+
+def _doUpgradeRemoveOldStuff(sUpgradeDir, asMembers):
+ """
+ Clean up all obsolete files and directories.
+ Returns True (shouldn't fail or raise any exceptions).
+ """
+
+ try:
+ shutil.rmtree(sUpgradeDir, ignore_errors = True);
+ except:
+ pass;
+
+ asKnownFiles = [];
+ asKnownDirs = [];
+ for sMember in asMembers:
+ sMember = sMember[len('testboxscript/'):];
+ if sMember == '':
+ continue;
+ if sMember[-1] == '/':
+ asKnownDirs.append(os.path.normpath(os.path.join(g_ksValidationKitDir, sMember[:-1])));
+ else:
+ asKnownFiles.append(os.path.normpath(os.path.join(g_ksValidationKitDir, sMember)));
+
+ for sDirPath, asDirs, asFiles in os.walk(g_ksValidationKitDir, topdown=False):
+ for sDir in asDirs:
+ sFull = os.path.normpath(os.path.join(sDirPath, sDir));
+ if sFull not in asKnownDirs:
+ testboxcommons.log2('Info: Removing obsolete directory "%s"' % (sFull,));
+ try:
+ os.rmdir(sFull);
+ except Exception as oXcpt:
+ testboxcommons.log('Warning: failed to rmdir obsolete dir "%s": %s' % (sFull, oXcpt));
+
+ for sFile in asFiles:
+ sFull = os.path.normpath(os.path.join(sDirPath, sFile));
+ if sFull not in asKnownFiles:
+ testboxcommons.log2('Info: Removing obsolete file "%s"' % (sFull,));
+ try:
+ os.unlink(sFull);
+ except Exception as oXcpt:
+ testboxcommons.log('Warning: failed to unlink obsolete file "%s": %s' % (sFull, oXcpt));
+ return True;
+
+def upgradeFromZip(sZipFile):
+ """
+ Upgrade the testboxscript install using the specified zip file.
+ Returns True/False.
+ """
+
+ # A little precaution.
+ if utils.isRunningFromCheckout():
+ testboxcommons.log('Use "svn up" to "upgrade" your source tree!');
+ return False;
+
+ #
+ # Prepare.
+ #
+ # Note! Don't bother cleaning up files and dirs in the error paths,
+ # they'll be restricted to the one zip and the one upgrade dir.
+ # We'll remove them next time we upgrade.
+ #
+ oZip = zipfile.ZipFile(sZipFile, 'r'); # No 'with' support in 2.6 class: pylint: disable=consider-using-with
+ asMembers = _doUpgradeCheckZip(oZip);
+ if asMembers is None:
+ return False;
+
+ sUpgradeDir = os.path.join(g_ksTestScriptDir, 'upgrade');
+ testboxcommons.log('Unzipping "%s" to "%s"...' % (sZipFile, sUpgradeDir));
+ if _doUpgradeUnzipAndCheck(oZip, sUpgradeDir, asMembers) is not True:
+ return False;
+ oZip.close();
+
+ if _doUpgradeTestRun(sUpgradeDir) is not True:
+ return False;
+
+ #
+ # Execute.
+ #
+ if _doUpgradeApply(sUpgradeDir, asMembers) is not True:
+ return False;
+ _doUpgradeRemoveOldStuff(sUpgradeDir, asMembers);
+ return True;
+
+
+# For testing purposes.
+if __name__ == '__main__':
+ sys.exit(upgradeFromZip(sys.argv[1]));
+