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