1 | # -*- coding: utf-8 -*-
|
---|
2 | # $Id: testboxupgrade.py 93115 2022-01-01 11:31:46Z vboxsync $
|
---|
3 |
|
---|
4 | """
|
---|
5 | TestBox Script - Upgrade from local file ZIP.
|
---|
6 | """
|
---|
7 |
|
---|
8 | __copyright__ = \
|
---|
9 | """
|
---|
10 | Copyright (C) 2012-2022 Oracle Corporation
|
---|
11 |
|
---|
12 | This file is part of VirtualBox Open Source Edition (OSE), as
|
---|
13 | available from http://www.virtualbox.org. This file is free software;
|
---|
14 | you can redistribute it and/or modify it under the terms of the GNU
|
---|
15 | General Public License (GPL) as published by the Free Software
|
---|
16 | Foundation, in version 2 as it comes in the "COPYING" file of the
|
---|
17 | VirtualBox OSE distribution. VirtualBox OSE is distributed in the
|
---|
18 | hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
|
---|
19 |
|
---|
20 | The contents of this file may alternatively be used under the terms
|
---|
21 | of the Common Development and Distribution License Version 1.0
|
---|
22 | (CDDL) only, as it comes in the "COPYING.CDDL" file of the
|
---|
23 | VirtualBox OSE distribution, in which case the provisions of the
|
---|
24 | CDDL are applicable instead of those of the GPL.
|
---|
25 |
|
---|
26 | You may elect to license modified versions of this file under the
|
---|
27 | terms and conditions of either the GPL or the CDDL or both.
|
---|
28 | """
|
---|
29 | __version__ = "$Revision: 93115 $"
|
---|
30 |
|
---|
31 | # Standard python imports.
|
---|
32 | import os
|
---|
33 | import shutil
|
---|
34 | import sys
|
---|
35 | import subprocess
|
---|
36 | import threading
|
---|
37 | import time
|
---|
38 | import uuid;
|
---|
39 | import zipfile
|
---|
40 |
|
---|
41 | # Validation Kit imports.
|
---|
42 | import testboxcommons
|
---|
43 | from testboxscript import TBS_EXITCODE_SYNTAX;
|
---|
44 | from common import utils;
|
---|
45 |
|
---|
46 | # Figure where we are.
|
---|
47 | try: __file__
|
---|
48 | except: __file__ = sys.argv[0];
|
---|
49 | g_ksTestScriptDir = os.path.dirname(os.path.abspath(__file__));
|
---|
50 | g_ksValidationKitDir = os.path.dirname(g_ksTestScriptDir);
|
---|
51 |
|
---|
52 |
|
---|
53 | def _doUpgradeThreadProc(oStdOut, asBuf):
|
---|
54 | """Thread procedure for the upgrade test drive."""
|
---|
55 | asBuf.append(oStdOut.read());
|
---|
56 | return True;
|
---|
57 |
|
---|
58 |
|
---|
59 | def _doUpgradeCheckZip(oZip):
|
---|
60 | """
|
---|
61 | Check that the essential files are there.
|
---|
62 | Returns list of members on success, None on failure.
|
---|
63 | """
|
---|
64 | asMembers = oZip.namelist();
|
---|
65 | if ('testboxscript/testboxscript/testboxscript.py' not in asMembers) \
|
---|
66 | or ('testboxscript/testboxscript/testboxscript_real.py' not in asMembers):
|
---|
67 | testboxcommons.log('Missing one or both testboxscripts (members: %s)' % (asMembers,));
|
---|
68 | return None;
|
---|
69 |
|
---|
70 | for sMember in asMembers:
|
---|
71 | if not sMember.startswith('testboxscript/'):
|
---|
72 | testboxcommons.log('zip file contains member outside testboxscript/: "%s"' % (sMember,));
|
---|
73 | return None;
|
---|
74 | if sMember.find('/../') > 0 or sMember.endswith('/..'):
|
---|
75 | testboxcommons.log('zip file contains member with escape sequence: "%s"' % (sMember,));
|
---|
76 | return None;
|
---|
77 |
|
---|
78 | return asMembers;
|
---|
79 |
|
---|
80 | def _doUpgradeUnzipAndCheck(oZip, sUpgradeDir, asMembers):
|
---|
81 | """
|
---|
82 | Unzips the files into sUpdateDir, does chmod(755) on all files and
|
---|
83 | checks that there are no symlinks or special files.
|
---|
84 | Returns True/False.
|
---|
85 | """
|
---|
86 | #
|
---|
87 | # Extract the files.
|
---|
88 | #
|
---|
89 | if os.path.exists(sUpgradeDir):
|
---|
90 | shutil.rmtree(sUpgradeDir);
|
---|
91 | for sMember in asMembers:
|
---|
92 | if sMember.endswith('/'):
|
---|
93 | os.makedirs(os.path.join(sUpgradeDir, sMember.replace('/', os.path.sep)), 0o775);
|
---|
94 | else:
|
---|
95 | oZip.extract(sMember, sUpgradeDir);
|
---|
96 |
|
---|
97 | #
|
---|
98 | # Make all files executable and make sure only owner can write to them.
|
---|
99 | # While at it, also check that there are only files and directory, no
|
---|
100 | # symbolic links or special stuff.
|
---|
101 | #
|
---|
102 | for sMember in asMembers:
|
---|
103 | sFull = os.path.join(sUpgradeDir, sMember);
|
---|
104 | if sMember.endswith('/'):
|
---|
105 | if not os.path.isdir(sFull):
|
---|
106 | testboxcommons.log('Not directory: "%s"' % sFull);
|
---|
107 | return False;
|
---|
108 | else:
|
---|
109 | if not os.path.isfile(sFull):
|
---|
110 | testboxcommons.log('Not regular file: "%s"' % sFull);
|
---|
111 | return False;
|
---|
112 | try:
|
---|
113 | os.chmod(sFull, 0o755);
|
---|
114 | except Exception as oXcpt:
|
---|
115 | testboxcommons.log('warning chmod error on %s: %s' % (sFull, oXcpt));
|
---|
116 | return True;
|
---|
117 |
|
---|
118 | def _doUpgradeTestRun(sUpgradeDir):
|
---|
119 | """
|
---|
120 | Do a testrun of the new script, to make sure it doesn't fail with
|
---|
121 | to run in any way because of old python, missing import or generally
|
---|
122 | busted upgrade.
|
---|
123 | Returns True/False.
|
---|
124 | """
|
---|
125 | asArgs = [os.path.join(sUpgradeDir, 'testboxscript', 'testboxscript', 'testboxscript.py'), '--version' ];
|
---|
126 | testboxcommons.log('Testing the new testbox script (%s)...' % (asArgs[0],));
|
---|
127 | if sys.executable:
|
---|
128 | asArgs.insert(0, sys.executable);
|
---|
129 | oChild = subprocess.Popen(asArgs, shell = False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT);
|
---|
130 |
|
---|
131 | asBuf = []
|
---|
132 | oThread = threading.Thread(target=_doUpgradeThreadProc, args=(oChild.stdout, asBuf));
|
---|
133 | oThread.daemon = True;
|
---|
134 | oThread.start();
|
---|
135 | oThread.join(30);
|
---|
136 |
|
---|
137 | # Give child up to 5 seconds to terminate after producing output.
|
---|
138 | if sys.version_info[0] >= 3 and sys.version_info[1] >= 3:
|
---|
139 | oChild.wait(5); # pylint: disable=too-many-function-args
|
---|
140 | else:
|
---|
141 | for _ in range(50):
|
---|
142 | iStatus = oChild.poll();
|
---|
143 | if iStatus is None:
|
---|
144 | break;
|
---|
145 | time.sleep(0.1);
|
---|
146 | iStatus = oChild.poll();
|
---|
147 | if iStatus is None:
|
---|
148 | testboxcommons.log('Checking the new testboxscript timed out.');
|
---|
149 | oChild.terminate();
|
---|
150 | oThread.join(5);
|
---|
151 | return False;
|
---|
152 | if iStatus is not TBS_EXITCODE_SYNTAX:
|
---|
153 | testboxcommons.log('The new testboxscript returned %d instead of %d during check.' \
|
---|
154 | % (iStatus, TBS_EXITCODE_SYNTAX));
|
---|
155 | return False;
|
---|
156 |
|
---|
157 | sOutput = b''.join(asBuf).decode('utf-8');
|
---|
158 | sOutput = sOutput.strip();
|
---|
159 | try:
|
---|
160 | iNewVersion = int(sOutput);
|
---|
161 | except:
|
---|
162 | testboxcommons.log('The new testboxscript returned an unparseable version string: "%s"!' % (sOutput,));
|
---|
163 | return False;
|
---|
164 | testboxcommons.log('New script version: %s' % (iNewVersion,));
|
---|
165 | return True;
|
---|
166 |
|
---|
167 | def _doUpgradeApply(sUpgradeDir, asMembers):
|
---|
168 | """
|
---|
169 | # Apply the directories and files from the upgrade.
|
---|
170 | returns True/False/Exception.
|
---|
171 | """
|
---|
172 |
|
---|
173 | #
|
---|
174 | # Create directories first since that's least intrusive.
|
---|
175 | #
|
---|
176 | for sMember in asMembers:
|
---|
177 | if sMember[-1] == '/':
|
---|
178 | sMember = sMember[len('testboxscript/'):];
|
---|
179 | if sMember != '':
|
---|
180 | sFull = os.path.join(g_ksValidationKitDir, sMember);
|
---|
181 | if not os.path.isdir(sFull):
|
---|
182 | os.makedirs(sFull, 0o755);
|
---|
183 |
|
---|
184 | #
|
---|
185 | # Move the files into place.
|
---|
186 | #
|
---|
187 | fRc = True;
|
---|
188 | asOldFiles = [];
|
---|
189 | for sMember in asMembers:
|
---|
190 | if sMember[-1] != '/':
|
---|
191 | sSrc = os.path.join(sUpgradeDir, sMember);
|
---|
192 | sDst = os.path.join(g_ksValidationKitDir, sMember[len('testboxscript/'):]);
|
---|
193 |
|
---|
194 | # Move the old file out of the way first.
|
---|
195 | sDstRm = None;
|
---|
196 | if os.path.exists(sDst):
|
---|
197 | testboxcommons.log2('Info: Installing "%s"' % (sDst,));
|
---|
198 | sDstRm = '%s-delete-me-%s' % (sDst, uuid.uuid4(),);
|
---|
199 | try:
|
---|
200 | os.rename(sDst, sDstRm);
|
---|
201 | except Exception as oXcpt:
|
---|
202 | testboxcommons.log('Error: failed to rename (old) "%s" to "%s": %s' % (sDst, sDstRm, oXcpt));
|
---|
203 | try:
|
---|
204 | shutil.copy(sDst, sDstRm);
|
---|
205 | except Exception as oXcpt:
|
---|
206 | testboxcommons.log('Error: failed to copy (old) "%s" to "%s": %s' % (sDst, sDstRm, oXcpt));
|
---|
207 | break;
|
---|
208 | try:
|
---|
209 | os.unlink(sDst);
|
---|
210 | except Exception as oXcpt:
|
---|
211 | testboxcommons.log('Error: failed to unlink (old) "%s": %s' % (sDst, oXcpt));
|
---|
212 | break;
|
---|
213 |
|
---|
214 | # Move/copy the new one into place.
|
---|
215 | testboxcommons.log2('Info: Installing "%s"' % (sDst,));
|
---|
216 | try:
|
---|
217 | os.rename(sSrc, sDst);
|
---|
218 | except Exception as oXcpt:
|
---|
219 | testboxcommons.log('Warning: failed to rename (new) "%s" to "%s": %s' % (sSrc, sDst, oXcpt));
|
---|
220 | try:
|
---|
221 | shutil.copy(sSrc, sDst);
|
---|
222 | except:
|
---|
223 | testboxcommons.log('Error: failed to copy (new) "%s" to "%s": %s' % (sSrc, sDst, oXcpt));
|
---|
224 | fRc = False;
|
---|
225 | break;
|
---|
226 |
|
---|
227 | #
|
---|
228 | # Roll back on failure.
|
---|
229 | #
|
---|
230 | if fRc is not True:
|
---|
231 | testboxcommons.log('Attempting to roll back old files...');
|
---|
232 | for sDstRm in asOldFiles:
|
---|
233 | sDst = sDstRm[:sDstRm.rfind('-delete-me')];
|
---|
234 | testboxcommons.log2('Info: Rolling back "%s" (%s)' % (sDst, os.path.basename(sDstRm)));
|
---|
235 | try:
|
---|
236 | shutil.move(sDstRm, sDst);
|
---|
237 | except:
|
---|
238 | testboxcommons.log('Error: failed to rollback "%s" onto "%s": %s' % (sDstRm, sDst, oXcpt));
|
---|
239 | return False;
|
---|
240 | return True;
|
---|
241 |
|
---|
242 | def _doUpgradeRemoveOldStuff(sUpgradeDir, asMembers):
|
---|
243 | """
|
---|
244 | Clean up all obsolete files and directories.
|
---|
245 | Returns True (shouldn't fail or raise any exceptions).
|
---|
246 | """
|
---|
247 |
|
---|
248 | try:
|
---|
249 | shutil.rmtree(sUpgradeDir, ignore_errors = True);
|
---|
250 | except:
|
---|
251 | pass;
|
---|
252 |
|
---|
253 | asKnownFiles = [];
|
---|
254 | asKnownDirs = [];
|
---|
255 | for sMember in asMembers:
|
---|
256 | sMember = sMember[len('testboxscript/'):];
|
---|
257 | if sMember == '':
|
---|
258 | continue;
|
---|
259 | if sMember[-1] == '/':
|
---|
260 | asKnownDirs.append(os.path.normpath(os.path.join(g_ksValidationKitDir, sMember[:-1])));
|
---|
261 | else:
|
---|
262 | asKnownFiles.append(os.path.normpath(os.path.join(g_ksValidationKitDir, sMember)));
|
---|
263 |
|
---|
264 | for sDirPath, asDirs, asFiles in os.walk(g_ksValidationKitDir, topdown=False):
|
---|
265 | for sDir in asDirs:
|
---|
266 | sFull = os.path.normpath(os.path.join(sDirPath, sDir));
|
---|
267 | if sFull not in asKnownDirs:
|
---|
268 | testboxcommons.log2('Info: Removing obsolete directory "%s"' % (sFull,));
|
---|
269 | try:
|
---|
270 | os.rmdir(sFull);
|
---|
271 | except Exception as oXcpt:
|
---|
272 | testboxcommons.log('Warning: failed to rmdir obsolete dir "%s": %s' % (sFull, oXcpt));
|
---|
273 |
|
---|
274 | for sFile in asFiles:
|
---|
275 | sFull = os.path.normpath(os.path.join(sDirPath, sFile));
|
---|
276 | if sFull not in asKnownFiles:
|
---|
277 | testboxcommons.log2('Info: Removing obsolete file "%s"' % (sFull,));
|
---|
278 | try:
|
---|
279 | os.unlink(sFull);
|
---|
280 | except Exception as oXcpt:
|
---|
281 | testboxcommons.log('Warning: failed to unlink obsolete file "%s": %s' % (sFull, oXcpt));
|
---|
282 | return True;
|
---|
283 |
|
---|
284 | def upgradeFromZip(sZipFile):
|
---|
285 | """
|
---|
286 | Upgrade the testboxscript install using the specified zip file.
|
---|
287 | Returns True/False.
|
---|
288 | """
|
---|
289 |
|
---|
290 | # A little precaution.
|
---|
291 | if utils.isRunningFromCheckout():
|
---|
292 | testboxcommons.log('Use "svn up" to "upgrade" your source tree!');
|
---|
293 | return False;
|
---|
294 |
|
---|
295 | #
|
---|
296 | # Prepare.
|
---|
297 | #
|
---|
298 | # Note! Don't bother cleaning up files and dirs in the error paths,
|
---|
299 | # they'll be restricted to the one zip and the one upgrade dir.
|
---|
300 | # We'll remove them next time we upgrade.
|
---|
301 | #
|
---|
302 | oZip = zipfile.ZipFile(sZipFile, 'r');
|
---|
303 | asMembers = _doUpgradeCheckZip(oZip);
|
---|
304 | if asMembers is None:
|
---|
305 | return False;
|
---|
306 |
|
---|
307 | sUpgradeDir = os.path.join(g_ksTestScriptDir, 'upgrade');
|
---|
308 | testboxcommons.log('Unzipping "%s" to "%s"...' % (sZipFile, sUpgradeDir));
|
---|
309 | if _doUpgradeUnzipAndCheck(oZip, sUpgradeDir, asMembers) is not True:
|
---|
310 | return False;
|
---|
311 | oZip.close();
|
---|
312 |
|
---|
313 | if _doUpgradeTestRun(sUpgradeDir) is not True:
|
---|
314 | return False;
|
---|
315 |
|
---|
316 | #
|
---|
317 | # Execute.
|
---|
318 | #
|
---|
319 | if _doUpgradeApply(sUpgradeDir, asMembers) is not True:
|
---|
320 | return False;
|
---|
321 | _doUpgradeRemoveOldStuff(sUpgradeDir, asMembers);
|
---|
322 | return True;
|
---|
323 |
|
---|
324 |
|
---|
325 | # For testing purposes.
|
---|
326 | if __name__ == '__main__':
|
---|
327 | sys.exit(upgradeFromZip(sys.argv[1]));
|
---|
328 |
|
---|