VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testboxscript/testboxupgrade.py

Last change on this file was 106061, checked in by vboxsync, 3 months ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 12.0 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testboxupgrade.py 106061 2024-09-16 14:03:52Z vboxsync $
3
4"""
5TestBox Script - Upgrade from local file ZIP.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2024 Oracle and/or its affiliates.
11
12This file is part of VirtualBox base platform packages, as
13available from https://www.virtualbox.org.
14
15This program is free software; you can redistribute it and/or
16modify it under the terms of the GNU General Public License
17as published by the Free Software Foundation, in version 3 of the
18License.
19
20This program is distributed in the hope that it will be useful, but
21WITHOUT ANY WARRANTY; without even the implied warranty of
22MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23General Public License for more details.
24
25You should have received a copy of the GNU General Public License
26along with this program; if not, see <https://www.gnu.org/licenses>.
27
28The contents of this file may alternatively be used under the terms
29of the Common Development and Distribution License Version 1.0
30(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
31in the VirtualBox distribution, in which case the provisions of the
32CDDL are applicable instead of those of the GPL.
33
34You may elect to license modified versions of this file under the
35terms and conditions of either the GPL or the CDDL or both.
36
37SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
38"""
39__version__ = "$Revision: 106061 $"
40
41# Standard python imports.
42import os
43import shutil
44import sys
45import subprocess
46import threading
47import time
48import uuid;
49import zipfile
50
51# Validation Kit imports.
52from common import utils;
53import testboxcommons
54from testboxscript import TBS_EXITCODE_SYNTAX;
55
56# Figure where we are.
57try: __file__ # pylint: disable=used-before-assignment
58except: __file__ = sys.argv[0];
59g_ksTestScriptDir = os.path.dirname(os.path.abspath(__file__));
60g_ksValidationKitDir = os.path.dirname(g_ksTestScriptDir);
61
62
63def _doUpgradeThreadProc(oStdOut, asBuf):
64 """Thread procedure for the upgrade test drive."""
65 asBuf.append(oStdOut.read());
66 return True;
67
68
69def _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
90def _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
128def _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
178def _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
253def _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
295def 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.
337if __name__ == '__main__':
338 sys.exit(upgradeFromZip(sys.argv[1]));
339
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette