VirtualBox

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

Last change on this file was 106061, checked in by vboxsync, 8 weeks ago

Copyright year updates by scm.

  • Property svn:eol-style set to LF
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
File size: 48.7 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# $Id: testboxscript_real.py 106061 2024-09-16 14:03:52Z vboxsync $
4
5"""
6TestBox Script - main().
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2012-2024 Oracle and/or its affiliates.
12
13This file is part of VirtualBox base platform packages, as
14available from https://www.virtualbox.org.
15
16This program is free software; you can redistribute it and/or
17modify it under the terms of the GNU General Public License
18as published by the Free Software Foundation, in version 3 of the
19License.
20
21This program is distributed in the hope that it will be useful, but
22WITHOUT ANY WARRANTY; without even the implied warranty of
23MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
24General Public License for more details.
25
26You should have received a copy of the GNU General Public License
27along with this program; if not, see <https://www.gnu.org/licenses>.
28
29The contents of this file may alternatively be used under the terms
30of the Common Development and Distribution License Version 1.0
31(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
32in the VirtualBox distribution, in which case the provisions of the
33CDDL are applicable instead of those of the GPL.
34
35You may elect to license modified versions of this file under the
36terms and conditions of either the GPL or the CDDL or both.
37
38SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
39"""
40__version__ = "$Revision: 106061 $"
41
42
43# Standard python imports.
44import math
45import os
46from optparse import OptionParser # pylint: disable=deprecated-module
47import platform
48import random
49import shutil
50import sys
51import tempfile
52import time
53import uuid
54
55# Only the main script needs to modify the path.
56try: __file__ # pylint: disable=used-before-assignment
57except: __file__ = sys.argv[0];
58g_ksTestScriptDir = os.path.dirname(os.path.abspath(__file__));
59g_ksValidationKitDir = os.path.dirname(g_ksTestScriptDir);
60sys.path.extend([g_ksTestScriptDir, g_ksValidationKitDir]);
61
62# Validation Kit imports.
63from common import constants;
64from common import utils;
65import testboxcommons;
66from testboxcommons import TestBoxException;
67from testboxcommand import TestBoxCommand;
68from testboxconnection import TestBoxConnection;
69from testboxscript import TBS_EXITCODE_SYNTAX, TBS_EXITCODE_FAILURE;
70
71# Python 3 hacks:
72if sys.version_info[0] >= 3:
73 long = int; # pylint: disable=redefined-builtin,invalid-name
74
75
76class TestBoxScriptException(Exception):
77 """ For raising exceptions during TestBoxScript.__init__. """
78 pass; # pylint: disable=unnecessary-pass
79
80
81class TestBoxScript(object):
82 """
83 Implementation of the test box script.
84 Communicate with test manager and perform offered actions.
85 """
86
87 ## @name Class Constants.
88 # @{
89
90 # Scratch space round value (MB).
91 kcMbScratchSpaceRounding = 64
92 # Memory size round value (MB).
93 kcMbMemoryRounding = 4
94 # A NULL UUID in string form.
95 ksNullUuid = '00000000-0000-0000-0000-000000000000';
96 # The minimum dispatch loop delay.
97 kcSecMinDelay = 12;
98 # The maximum dispatch loop delay (inclusive).
99 kcSecMaxDelay = 24;
100 # The minimum sign-on delay.
101 kcSecMinSignOnDelay = 30;
102 # The maximum sign-on delay (inclusive).
103 kcSecMaxSignOnDelay = 60;
104
105 # Keys for config params
106 VALUE = 'value'
107 FN = 'fn' # pylint: disable=invalid-name
108
109 ## @}
110
111
112 def __init__(self, oOptions):
113 """
114 Initialize internals
115 """
116 self._oOptions = oOptions;
117 self._sTestBoxHelper = None;
118
119 # Signed-on state
120 self._cSignOnAttempts = 0;
121 self._fSignedOn = False;
122 self._fNeedReSignOn = False;
123 self._fFirstSignOn = True;
124 self._idTestBox = None;
125 self._sTestBoxName = '';
126 self._sTestBoxUuid = self.ksNullUuid; # convenience, assigned below.
127
128 # Command processor.
129 self._oCommand = TestBoxCommand(self);
130
131 #
132 # Scratch dir setup. Use /var/tmp instead of /tmp because we may need
133 # many many GBs for some test scenarios and /tmp can be backed by swap
134 # or be a fast+small disk of some kind, while /var/tmp is normally
135 # larger, if slower. /var/tmp is generally not cleaned up on reboot,
136 # /tmp often is, this would break host panic / triple-fault detection.
137 #
138 if self._oOptions.sScratchRoot is None:
139 if utils.getHostOs() in ('win', 'os2', 'haiku', 'dos'):
140 # We need *lots* of space, so avoid /tmp as it may be a memory
141 # file system backed by the swap file, or worse.
142 self._oOptions.sScratchRoot = tempfile.gettempdir();
143 else:
144 self._oOptions.sScratchRoot = '/var/tmp';
145 sSubDir = 'testbox';
146 try:
147 sSubDir = '%s-%u' % (sSubDir, os.getuid()); # pylint: disable=no-member
148 except:
149 pass;
150 self._oOptions.sScratchRoot = os.path.join(self._oOptions.sScratchRoot, sSubDir);
151
152 self._sScratchSpill = os.path.join(self._oOptions.sScratchRoot, 'scratch');
153 self._sScratchScripts = os.path.join(self._oOptions.sScratchRoot, 'scripts');
154 self._sScratchState = os.path.join(self._oOptions.sScratchRoot, 'state'); # persistant storage.
155
156 for sDir in [self._oOptions.sScratchRoot, self._sScratchSpill, self._sScratchScripts, self._sScratchState]:
157 if not os.path.isdir(sDir):
158 os.makedirs(sDir, 0o700);
159
160 # We count consecutive reinitScratch failures and will reboot the
161 # testbox after a while in the hope that it will correct the issue.
162 self._cReinitScratchErrors = 0;
163
164 #
165 # Mount builds and test resources if requested.
166 #
167 self.mountShares();
168
169 #
170 # Sign-on parameters: Packed into list of records of format:
171 # { <Parameter ID>: { <Current value>, <Check function> } }
172 #
173 self._ddSignOnParams = \
174 {
175 constants.tbreq.ALL_PARAM_TESTBOX_UUID: { self.VALUE: self._getHostSystemUuid(), self.FN: None },
176 constants.tbreq.SIGNON_PARAM_OS: { self.VALUE: utils.getHostOs(), self.FN: None },
177 constants.tbreq.SIGNON_PARAM_OS_VERSION: { self.VALUE: utils.getHostOsVersion(), self.FN: None },
178 constants.tbreq.SIGNON_PARAM_CPU_ARCH: { self.VALUE: utils.getHostArch(), self.FN: None },
179 constants.tbreq.SIGNON_PARAM_CPU_VENDOR: { self.VALUE: self._getHostCpuVendor(), self.FN: None },
180 constants.tbreq.SIGNON_PARAM_CPU_NAME: { self.VALUE: self._getHostCpuName(), self.FN: None },
181 constants.tbreq.SIGNON_PARAM_CPU_REVISION: { self.VALUE: self._getHostCpuRevision(), self.FN: None },
182 constants.tbreq.SIGNON_PARAM_HAS_HW_VIRT: { self.VALUE: self._hasHostHwVirt(), self.FN: None },
183 constants.tbreq.SIGNON_PARAM_HAS_NESTED_PAGING:{ self.VALUE: self._hasHostNestedPaging(), self.FN: None },
184 constants.tbreq.SIGNON_PARAM_HAS_64_BIT_GUEST: { self.VALUE: self._can64BitGuest(), self.FN: None },
185 constants.tbreq.SIGNON_PARAM_HAS_IOMMU: { self.VALUE: self._hasHostIoMmu(), self.FN: None },
186 constants.tbreq.SIGNON_PARAM_HAS_NATIVE_API: { self.VALUE: self._hasHostNativeApi(), self.FN: None },
187 #constants.tbreq.SIGNON_PARAM_WITH_RAW_MODE: { self.VALUE: self._withRawModeSupport(), self.FN: None },
188 constants.tbreq.SIGNON_PARAM_SCRIPT_REV: { self.VALUE: self._getScriptRev(), self.FN: None },
189 constants.tbreq.SIGNON_PARAM_REPORT: { self.VALUE: self._getHostReport(), self.FN: None },
190 constants.tbreq.SIGNON_PARAM_PYTHON_VERSION: { self.VALUE: self._getPythonHexVersion(), self.FN: None },
191 constants.tbreq.SIGNON_PARAM_CPU_COUNT: { self.VALUE: None, self.FN: utils.getPresentCpuCount },
192 constants.tbreq.SIGNON_PARAM_MEM_SIZE: { self.VALUE: None, self.FN: self._getHostMemSize },
193 constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE: { self.VALUE: None, self.FN: self._getFreeScratchSpace },
194 }
195 for sItem in self._ddSignOnParams: # pylint: disable=consider-using-dict-items
196 if self._ddSignOnParams[sItem][self.FN] is not None:
197 self._ddSignOnParams[sItem][self.VALUE] = self._ddSignOnParams[sItem][self.FN]()
198
199 testboxcommons.log('Starting Test Box script (%s)' % (self._getScriptRev(),));
200 testboxcommons.log('Test Manager URL: %s' % self._oOptions.sTestManagerUrl,)
201 testboxcommons.log('Scratch root path: %s' % self._oOptions.sScratchRoot,)
202 for sItem in self._ddSignOnParams: # pylint: disable=consider-using-dict-items
203 testboxcommons.log('Sign-On value %18s: %s' % (sItem, self._ddSignOnParams[sItem][self.VALUE]));
204
205 #
206 # The System UUID is the primary identification of the machine, so
207 # refuse to cooperate if it's NULL.
208 #
209 self._sTestBoxUuid = self.getSignOnParam(constants.tbreq.ALL_PARAM_TESTBOX_UUID);
210 if self._sTestBoxUuid == self.ksNullUuid:
211 raise TestBoxScriptException('Couldn\'t determine the System UUID, please use --system-uuid to specify it.');
212
213 #
214 # Export environment variables, clearing any we don't know yet.
215 #
216 for sEnvVar in self._oOptions.asEnvVars:
217 iEqual = sEnvVar.find('=');
218 if iEqual == -1: # No '=', remove it.
219 if sEnvVar in os.environ:
220 del os.environ[sEnvVar];
221 elif iEqual > 0: # Set it.
222 os.environ[sEnvVar[:iEqual]] = sEnvVar[iEqual+1:];
223 else: # Starts with '=', bad user.
224 raise TestBoxScriptException('Invalid -E argument: "%s"' % (sEnvVar,));
225
226 os.environ['TESTBOX_PATH_BUILDS'] = self._oOptions.sBuildsPath;
227 os.environ['TESTBOX_PATH_RESOURCES'] = self._oOptions.sTestRsrcPath;
228 os.environ['TESTBOX_PATH_SCRATCH'] = self._sScratchSpill;
229 os.environ['TESTBOX_PATH_SCRIPTS'] = self._sScratchScripts;
230 os.environ['TESTBOX_PATH_UPLOAD'] = self._sScratchSpill; ## @todo drop the UPLOAD dir?
231 os.environ['TESTBOX_HAS_HW_VIRT'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_HAS_HW_VIRT);
232 os.environ['TESTBOX_HAS_NESTED_PAGING'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_HAS_NESTED_PAGING);
233 os.environ['TESTBOX_HAS_IOMMU'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_HAS_IOMMU);
234 os.environ['TESTBOX_HAS_NATIVE_API'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_HAS_NATIVE_API);
235 os.environ['TESTBOX_SCRIPT_REV'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_SCRIPT_REV);
236 os.environ['TESTBOX_CPU_COUNT'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_CPU_COUNT);
237 os.environ['TESTBOX_MEM_SIZE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_MEM_SIZE);
238 os.environ['TESTBOX_SCRATCH_SIZE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE);
239 #TODO: os.environ['TESTBOX_WITH_RAW_MODE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_WITH_RAW_MODE);
240 os.environ['TESTBOX_WITH_RAW_MODE'] = str(self._withRawModeSupport());
241 os.environ['TESTBOX_MANAGER_URL'] = self._oOptions.sTestManagerUrl;
242 os.environ['TESTBOX_UUID'] = self._sTestBoxUuid;
243 os.environ['TESTBOX_REPORTER'] = 'remote';
244 os.environ['TESTBOX_NAME'] = '';
245 os.environ['TESTBOX_ID'] = '';
246 os.environ['TESTBOX_TEST_SET_ID'] = '';
247 os.environ['TESTBOX_TIMEOUT'] = '0';
248 os.environ['TESTBOX_TIMEOUT_ABS'] = '0';
249
250 if utils.getHostOs() == 'win':
251 os.environ['COMSPEC'] = os.path.join(os.environ['SystemRoot'], 'System32', 'cmd.exe');
252 # Currently omitting any kBuild tools.
253
254 def mountShares(self):
255 """
256 Mounts the shares.
257 Raises exception on failure.
258 """
259 self._mountShare(self._oOptions.sBuildsPath, self._oOptions.sBuildsServerType, self._oOptions.sBuildsServerName,
260 self._oOptions.sBuildsServerShare,
261 self._oOptions.sBuildsServerUser, self._oOptions.sBuildsServerPasswd,
262 self._oOptions.sBuildsServerMountOpt, 'builds');
263 self._mountShare(self._oOptions.sTestRsrcPath, self._oOptions.sTestRsrcServerType, self._oOptions.sTestRsrcServerName,
264 self._oOptions.sTestRsrcServerShare,
265 self._oOptions.sTestRsrcServerUser, self._oOptions.sTestRsrcServerPasswd,
266 self._oOptions.sTestRsrcServerMountOpt, 'testrsrc');
267 return True;
268
269 def _mountShare(self, sMountPoint, sType, sServer, sShare, sUser, sPassword, sMountOpt, sWhat):
270 """
271 Mounts the specified share if needed.
272 Raises exception on failure.
273 """
274 # Only mount if the type is specified.
275 if sType is None:
276 return True;
277
278 # Test if already mounted.
279 sTestFile = os.path.join(sMountPoint + os.path.sep, os.path.basename(sShare) + '-new.txt');
280 if os.path.isfile(sTestFile):
281 return True;
282
283 #
284 # Platform specific mount code.
285 #
286 sHostOs = utils.getHostOs()
287 if sHostOs in ('darwin', 'freebsd'):
288 if sMountOpt != '':
289 sMountOpt = ',' + sMountOpt
290 utils.sudoProcessCall(['/sbin/umount', sMountPoint]);
291 utils.sudoProcessCall(['/bin/mkdir', '-p', sMountPoint]);
292 utils.sudoProcessCall(['/usr/sbin/chown', str(os.getuid()), sMountPoint]); # pylint: disable=no-member
293 if sType == 'cifs':
294 # Note! no smb://server/share stuff here, 10.6.8 didn't like it.
295 utils.processOutputChecked(['/sbin/mount_smbfs',
296 '-o',
297 'automounted,nostreams,soft,noowners,noatime,rdonly' + sMountOpt,
298 '-f', '0555', '-d', '0555',
299 '//%s:%s@%s/%s' % (sUser, sPassword, sServer, sShare),
300 sMountPoint]);
301 else:
302 raise TestBoxScriptException('Unsupported server type %s.' % (sType,));
303
304 elif sHostOs == 'linux':
305 if sMountOpt != '':
306 sMountOpt = ',' + sMountOpt
307 utils.sudoProcessCall(['/bin/umount', sMountPoint]);
308 utils.sudoProcessCall(['/bin/mkdir', '-p', sMountPoint]);
309 if sType == 'cifs':
310 utils.sudoProcessOutputChecked(['/bin/mount', '-t', 'cifs',
311 '-o',
312 'user=' + sUser
313 + ',password=' + sPassword
314 + ',sec=ntlmv2'
315 + ',uid=' + str(os.getuid()) # pylint: disable=no-member
316 + ',gid=' + str(os.getgid()) # pylint: disable=no-member
317 + ',nounix,file_mode=0555,dir_mode=0555,soft,ro'
318 + sMountOpt,
319 '//%s/%s' % (sServer, sShare),
320 sMountPoint]);
321 elif sType == 'nfs':
322 utils.sudoProcessOutputChecked(['/bin/mount', '-t', 'nfs',
323 '-o', 'soft,ro' + sMountOpt,
324 '%s:%s' % (sServer, sShare if sShare.find('/') >= 0 else ('/export/' + sShare)),
325 sMountPoint]);
326
327 else:
328 raise TestBoxScriptException('Unsupported server type %s.' % (sType,));
329
330 elif sHostOs == 'solaris':
331 if sMountOpt != '':
332 sMountOpt = ',' + sMountOpt
333 utils.sudoProcessCall(['/sbin/umount', sMountPoint]);
334 utils.sudoProcessCall(['/bin/mkdir', '-p', sMountPoint]);
335 if sType == 'cifs':
336 ## @todo This stuff doesn't work on wei01-x4600b.de.oracle.com running 11.1. FIXME!
337 oPasswdFile = tempfile.TemporaryFile(); # pylint: disable=consider-using-with
338 oPasswdFile.write(sPassword + '\n');
339 oPasswdFile.flush();
340 utils.sudoProcessOutputChecked(['/sbin/mount', '-F', 'smbfs',
341 '-o',
342 'user=' + sUser
343 + ',uid=' + str(os.getuid()) # pylint: disable=no-member
344 + ',gid=' + str(os.getgid()) # pylint: disable=no-member
345 + ',fileperms=0555,dirperms=0555,noxattr,ro'
346 + sMountOpt,
347 '//%s/%s' % (sServer, sShare),
348 sMountPoint],
349 stdin = oPasswdFile);
350 oPasswdFile.close();
351 elif sType == 'nfs':
352 utils.sudoProcessOutputChecked(['/sbin/mount', '-F', 'nfs',
353 '-o', 'noxattr,ro' + sMountOpt,
354 '%s:%s' % (sServer, sShare if sShare.find('/') >= 0 else ('/export/' + sShare)),
355 sMountPoint]);
356
357 else:
358 raise TestBoxScriptException('Unsupported server type %s.' % (sType,));
359
360
361 elif sHostOs == 'win':
362 if sType != 'cifs':
363 raise TestBoxScriptException('Only CIFS mounts are supported on Windows.');
364 utils.processCall(['net', 'use', sMountPoint, '/d']);
365 utils.processOutputChecked(['net', 'use', sMountPoint,
366 '\\\\' + sServer + '\\' + sShare,
367 sPassword,
368 '/USER:' + sUser,]);
369 else:
370 raise TestBoxScriptException('Unsupported host %s' % (sHostOs,));
371
372 #
373 # Re-test.
374 #
375 if not os.path.isfile(sTestFile):
376 raise TestBoxException('Failed to mount %s (%s[%s]) at %s: %s not found'
377 % (sWhat, sServer, sShare, sMountPoint, sTestFile));
378
379 return True;
380
381 ## @name Signon property releated methods.
382 # @{
383
384 def _getHelperOutput(self, sCmd):
385 """
386 Invokes TestBoxHelper to obtain information hard to access from python.
387 """
388 if self._sTestBoxHelper is None:
389 if not utils.isRunningFromCheckout():
390 # See VBoxTestBoxScript.zip for layout.
391 self._sTestBoxHelper = os.path.join(g_ksValidationKitDir, utils.getHostOs(), utils.getHostArch(), \
392 'TestBoxHelper');
393 else: # Only for in-tree testing, so don't bother be too accurate right now.
394 sType = os.environ.get('KBUILD_TYPE', 'debug');
395 self._sTestBoxHelper = os.path.join(g_ksValidationKitDir, os.pardir, os.pardir, os.pardir, 'out', \
396 utils.getHostOsDotArch(), sType, 'testboxscript', \
397 utils.getHostOs(), utils.getHostArch(), \
398 'TestBoxHelper');
399 if utils.getHostOs() in ['win', 'os2']:
400 self._sTestBoxHelper += '.exe';
401
402 return utils.processOutputChecked([self._sTestBoxHelper, sCmd]).strip();
403
404 def _getHelperOutputTristate(self, sCmd, fDunnoValue):
405 """
406 Invokes TestBoxHelper to obtain information hard to access from python.
407 """
408 sValue = self._getHelperOutput(sCmd);
409 sValue = sValue.lower();
410 if sValue == 'true':
411 return True;
412 if sValue == 'false':
413 return False;
414 if sValue not in ('dunno', 'none',):
415 raise TestBoxException('Unexpected response "%s" to helper command "%s"' % (sValue, sCmd));
416 return fDunnoValue;
417
418
419 @staticmethod
420 def _isUuidGood(sUuid):
421 """
422 Checks if the UUID looks good.
423
424 There are systems with really bad UUIDs, for instance
425 "03000200-0400-0500-0006-000700080009".
426 """
427 if sUuid == TestBoxScript.ksNullUuid:
428 return False;
429 sUuid = sUuid.lower();
430 for sDigit in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']:
431 if sUuid.count(sDigit) > 16:
432 return False;
433 return True;
434
435 def _getHostSystemUuid(self):
436 """
437 Get the system UUID string from the System, return null-uuid if
438 unable to get retrieve it.
439 """
440 if self._oOptions.sSystemUuid is not None:
441 return self._oOptions.sSystemUuid;
442
443 sUuid = self.ksNullUuid;
444
445 #
446 # Try get at the firmware UUID.
447 #
448 if utils.getHostOs() == 'linux':
449 # NOTE: This requires to have kernel option enabled:
450 # Firmware Drivers -> Export DMI identification via sysfs to userspace
451 if os.path.exists('/sys/devices/virtual/dmi/id/product_uuid'):
452 try:
453 sVar = utils.sudoProcessOutputChecked(['cat', '/sys/devices/virtual/dmi/id/product_uuid']);
454 sUuid = str(uuid.UUID(sVar.strip()));
455 except:
456 pass;
457 ## @todo consider dmidecoder? What about EFI systems?
458
459 elif utils.getHostOs() == 'win':
460 # Windows: WMI
461 try:
462 import win32com.client; # pylint: disable=import-error
463 oWmi = win32com.client.Dispatch('WbemScripting.SWbemLocator');
464 oWebm = oWmi.ConnectServer('.', 'root\\cimv2');
465 for oItem in oWebm.ExecQuery('SELECT * FROM Win32_ComputerSystemProduct'):
466 if oItem.UUID is not None:
467 sUuid = str(uuid.UUID(oItem.UUID));
468 except:
469 pass;
470
471 elif utils.getHostOs() == 'darwin':
472 try:
473 sVar = utils.processOutputChecked(['/bin/sh', '-c',
474 '/usr/sbin/ioreg -k IOPlatformUUID' \
475 + '| /usr/bin/grep IOPlatformUUID' \
476 + '| /usr/bin/head -1']);
477 sVar = sVar.strip()[-(len(self.ksNullUuid) + 1):-1];
478 sUuid = str(uuid.UUID(sVar));
479 except:
480 pass;
481
482 elif utils.getHostOs() == 'solaris':
483 # Solaris: The smbios util.
484 try:
485 sVar = utils.processOutputChecked(['/bin/sh', '-c',
486 '/usr/sbin/smbios ' \
487 + '| /usr/xpg4/bin/sed -ne \'s/^.*UUID: *//p\'' \
488 + '| /usr/bin/head -1']);
489 sUuid = str(uuid.UUID(sVar.strip()));
490 except:
491 pass;
492
493 if self._isUuidGood(sUuid):
494 return sUuid;
495
496 #
497 # Try add the MAC address.
498 # uuid.getnode may provide it, or it may return a random number...
499 #
500 lMacAddr = uuid.getnode();
501 sNode = '%012x' % (lMacAddr,)
502 if lMacAddr == uuid.getnode() and lMacAddr != 0 and len(sNode) == 12:
503 return sUuid[:-12] + sNode;
504
505 return sUuid;
506
507 def _getHostCpuVendor(self):
508 """
509 Get the CPUID vendor string on intel HW.
510 """
511 return self._getHelperOutput('cpuvendor');
512
513 def _getHostCpuName(self):
514 """
515 Get the CPU name/description string.
516 """
517 return self._getHelperOutput('cpuname');
518
519 def _getHostCpuRevision(self):
520 """
521 Get the CPU revision (family/model/stepping) value.
522 """
523 return self._getHelperOutput('cpurevision');
524
525 def _hasHostHwVirt(self):
526 """
527 Check if the host supports AMD-V or VT-x
528 """
529 if self._oOptions.fHasHwVirt is None:
530 self._oOptions.fHasHwVirt = self._getHelperOutput('cpuhwvirt');
531 return self._oOptions.fHasHwVirt;
532
533 def _hasHostNestedPaging(self):
534 """
535 Check if the host supports nested paging.
536 """
537 if not self._hasHostHwVirt():
538 return False;
539 if self._oOptions.fHasNestedPaging is None:
540 self._oOptions.fHasNestedPaging = self._getHelperOutputTristate('nestedpaging', False);
541 return self._oOptions.fHasNestedPaging;
542
543 def _can64BitGuest(self):
544 """
545 Check if the we (VBox) can run 64-bit guests.
546 """
547 if not self._hasHostHwVirt():
548 return False;
549 if self._oOptions.fCan64BitGuest is None:
550 self._oOptions.fCan64BitGuest = self._getHelperOutputTristate('longmode', True);
551 return self._oOptions.fCan64BitGuest;
552
553 def _hasHostIoMmu(self):
554 """
555 Check if the host has an I/O MMU of the VT-d kind.
556 """
557 if not self._hasHostHwVirt():
558 return False;
559 if self._oOptions.fHasIoMmu is None:
560 ## @todo Any way to figure this one out on any host OS?
561 self._oOptions.fHasIoMmu = False;
562 return self._oOptions.fHasIoMmu;
563
564 def _hasHostNativeApi(self):
565 """
566 Check if the host supports the native API/NEM mode.
567 """
568 if self._oOptions.fHasNativeApi is None:
569 self._oOptions.fHasNativeApi = self._getHelperOutput('nativeapi');
570 return self._oOptions.fHasNativeApi;
571
572 def _withRawModeSupport(self):
573 """
574 Check if the testbox is configured with raw-mode support or not.
575 """
576 if self._oOptions.fWithRawMode is None:
577 self._oOptions.fWithRawMode = True;
578 return self._oOptions.fWithRawMode;
579
580 def _getHostReport(self):
581 """
582 Generate a report about the host hardware and software.
583 """
584 return self._getHelperOutput('report');
585
586
587 def _getHostMemSize(self):
588 """
589 Gets the amount of physical memory on the host (and accessible to the
590 OS, i.e. don't report stuff over 4GB if Windows doesn't wanna use it).
591 Unit: MiB.
592 """
593 cMbMemory = long(self._getHelperOutput('memsize').strip()) / (1024 * 1024);
594
595 # Round it.
596 cMbMemory = long(math.floor(cMbMemory / self.kcMbMemoryRounding)) * self.kcMbMemoryRounding;
597 return cMbMemory;
598
599 def _getFreeScratchSpace(self):
600 """
601 Get free space on the volume where scratch directory is located and
602 return it in bytes rounded down to nearest 64MB
603 (currently works on Linux only)
604 Unit: MiB.
605 """
606 if platform.system() == 'Windows':
607 import ctypes
608 cTypeMbFreeSpace = ctypes.c_ulonglong(0)
609 ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(self._oOptions.sScratchRoot), None, None,
610 ctypes.pointer(cTypeMbFreeSpace))
611 cMbFreeSpace = cTypeMbFreeSpace.value
612 else:
613 stats = os.statvfs(self._oOptions.sScratchRoot); # pylint: disable=no-member
614 cMbFreeSpace = stats.f_frsize * stats.f_bfree
615
616 # Convert to MB
617 cMbFreeSpace = long(cMbFreeSpace) /(1024 * 1024)
618
619 # Round free space size
620 cMbFreeSpace = long(math.floor(cMbFreeSpace / self.kcMbScratchSpaceRounding)) * self.kcMbScratchSpaceRounding;
621 return cMbFreeSpace;
622
623 def _getScriptRev(self):
624 """
625 The script (subversion) revision number.
626 """
627 sRev = '@VBOX_SVN_REV@';
628 sRev = sRev.strip(); # just in case...
629 try:
630 _ = int(sRev);
631 except:
632 return __version__[11:-1].strip();
633 return sRev;
634
635 def _getPythonHexVersion(self):
636 """
637 The python hex version number.
638 """
639 uHexVersion = getattr(sys, 'hexversion', None);
640 if uHexVersion is None:
641 uHexVersion = (sys.version_info[0] << 24) | (sys.version_info[1] << 16) | (sys.version_info[2] << 8);
642 if sys.version_info[3] == 'final':
643 uHexVersion |= 0xf0;
644 return uHexVersion;
645
646 # @}
647
648 def openTestManagerConnection(self):
649 """
650 Opens up a connection to the test manager.
651
652 Raises exception on failure.
653 """
654 return TestBoxConnection(self._oOptions.sTestManagerUrl, self._idTestBox, self._sTestBoxUuid);
655
656 def getSignOnParam(self, sName):
657 """
658 Returns a sign-on parameter value as string.
659 Raises exception if the name is incorrect.
660 """
661 return str(self._ddSignOnParams[sName][self.VALUE]);
662
663 def getPathState(self):
664 """
665 Get the path to the state dir in the scratch area.
666 """
667 return self._sScratchState;
668
669 def getPathScripts(self):
670 """
671 Get the path to the scripts dir (TESTBOX_PATH_SCRIPTS) in the scratch area.
672 """
673 return self._sScratchScripts;
674
675 def getPathSpill(self):
676 """
677 Get the path to the spill dir (TESTBOX_PATH_SCRATCH) in the scratch area.
678 """
679 return self._sScratchSpill;
680
681 def getPathBuilds(self):
682 """
683 Get the path to the builds.
684 """
685 return self._oOptions.sBuildsPath;
686
687 def getTestBoxId(self):
688 """
689 Get the TestBox ID for state saving purposes.
690 """
691 return self._idTestBox;
692
693 def getTestBoxName(self):
694 """
695 Get the TestBox name for state saving purposes.
696 """
697 return self._sTestBoxName;
698
699 def _reinitScratch(self, fnLog, fUseTheForce):
700 """
701 Wipes the scratch directories and re-initializes them.
702
703 No exceptions raise, returns success indicator instead.
704 """
705 if fUseTheForce is None:
706 fUseTheForce = self._fFirstSignOn;
707
708 class ErrorCallback(object): # pylint: disable=too-few-public-methods
709 """
710 Callbacks + state for the cleanup.
711 """
712 def __init__(self):
713 self.fRc = True;
714 def onErrorCallback(self, sFnName, sPath, aXcptInfo):
715 """ Logs error during shutil.rmtree operation. """
716 fnLog('Error removing "%s": fn=%s %s' % (sPath, sFnName, aXcptInfo[1]));
717 self.fRc = False;
718 oRc = ErrorCallback();
719
720 #
721 # Cleanup.
722 #
723 for sName in os.listdir(self._oOptions.sScratchRoot):
724 sFullName = os.path.join(self._oOptions.sScratchRoot, sName);
725 try:
726 if os.path.isdir(sFullName):
727 shutil.rmtree(sFullName, False, oRc.onErrorCallback); # pylint: disable=deprecated-argument
728 else:
729 os.remove(sFullName);
730 if os.path.exists(sFullName):
731 raise Exception('Still exists after deletion, weird.');
732 except Exception as oXcpt:
733 if fUseTheForce is True \
734 and utils.getHostOs() not in ['win', 'os2'] \
735 and len(sFullName) >= 8 \
736 and sFullName[0] == '/' \
737 and sFullName[1] != '/' \
738 and sFullName.find('/../') < 0:
739 fnLog('Problems deleting "%s" (%s) using the force...' % (sFullName, oXcpt));
740 try:
741 if os.path.isdir(sFullName):
742 iRc = utils.sudoProcessCall(['/bin/rm', '-Rf', sFullName])
743 else:
744 iRc = utils.sudoProcessCall(['/bin/rm', '-f', sFullName])
745 if iRc != 0:
746 raise Exception('exit code %s' % iRc);
747 if os.path.exists(sFullName):
748 raise Exception('Still exists after forced deletion, weird^2.');
749 except:
750 fnLog('Error sudo deleting "%s": %s' % (sFullName, oXcpt));
751 oRc.fRc = False;
752 else:
753 fnLog('Error deleting "%s": %s' % (sFullName, oXcpt));
754 oRc.fRc = False;
755
756 # Display files left behind.
757 def dirEnumCallback(sName, oStat):
758 """ callback for dirEnumerateTree """
759 fnLog(u'%s %s' % (utils.formatFileStat(oStat) if oStat is not None else '????????????', sName));
760 utils.dirEnumerateTree(self._oOptions.sScratchRoot, dirEnumCallback);
761
762 #
763 # Re-create the directories.
764 #
765 for sDir in [self._oOptions.sScratchRoot, self._sScratchSpill, self._sScratchScripts, self._sScratchState]:
766 if not os.path.isdir(sDir):
767 try:
768 os.makedirs(sDir, 0o700);
769 except Exception as oXcpt:
770 fnLog('Error creating "%s": %s' % (sDir, oXcpt));
771 oRc.fRc = False;
772
773 if oRc.fRc is True:
774 self._cReinitScratchErrors = 0;
775 else:
776 self._cReinitScratchErrors += 1;
777 return oRc.fRc;
778
779 def reinitScratch(self, fnLog = testboxcommons.log, fUseTheForce = None, cRetries = 0, cMsDelay = 5000):
780 """
781 Wipes the scratch directories and re-initializes them.
782
783 Will retry according to the cRetries and cMsDelay parameters. Windows
784 forces us to apply this hack as it ships with services asynchronously
785 scanning files after they execute, thus racing us cleaning up after a
786 test. On testboxwin3 we had frequent trouble with aelupsvc.dll keeping
787 vts_rm.exe kind of open, somehow preventing us from removing the
788 directory containing it, despite not issuing any errors deleting the
789 file itself. The service is called "Application Experience", which
790 feels like a weird joke here.
791
792 No exceptions raise, returns success indicator instead.
793 """
794 fRc = self._reinitScratch(fnLog, fUseTheForce)
795 while fRc is False and cRetries > 0:
796 time.sleep(cMsDelay / 1000.0);
797 fnLog('reinitScratch: Retrying...');
798 fRc = self._reinitScratch(fnLog, fUseTheForce)
799 cRetries -= 1;
800 return fRc;
801
802
803 def _doSignOn(self):
804 """
805 Worker for _maybeSignOn that does the actual signing on.
806 """
807 assert not self._oCommand.isRunning();
808
809 # Reset the siged-on state.
810 testboxcommons.log('Signing-on...')
811 self._fSignedOn = False
812 self._idTestBox = None
813 self._cSignOnAttempts += 1;
814
815 # Assemble SIGN-ON request parameters and send the request.
816 dParams = {};
817 for sParam in self._ddSignOnParams: # pylint: disable=consider-using-dict-items
818 dParams[sParam] = self._ddSignOnParams[sParam][self.VALUE];
819 oResponse = TestBoxConnection.sendSignOn(self._oOptions.sTestManagerUrl, dParams);
820
821 # Check response.
822 try:
823 sResult = oResponse.getStringChecked(constants.tbresp.ALL_PARAM_RESULT);
824 if sResult != constants.tbresp.STATUS_ACK:
825 raise TestBoxException('Result is %s' % (sResult,));
826 oResponse.checkParameterCount(3);
827 idTestBox = oResponse.getIntChecked(constants.tbresp.SIGNON_PARAM_ID, 1, 0x7ffffffe);
828 sTestBoxName = oResponse.getStringChecked(constants.tbresp.SIGNON_PARAM_NAME);
829 except TestBoxException as err:
830 testboxcommons.log('Failed to sign-on: %s' % (str(err),))
831 testboxcommons.log('Server response: %s' % (oResponse.toString(),));
832 return False;
833
834 # Successfully signed on, update the state.
835 self._fSignedOn = True;
836 self._fNeedReSignOn = False;
837 self._cSignOnAttempts = 0;
838 self._idTestBox = idTestBox;
839 self._sTestBoxName = sTestBoxName;
840
841 # Update the environment.
842 os.environ['TESTBOX_ID'] = str(self._idTestBox);
843 os.environ['TESTBOX_NAME'] = sTestBoxName;
844 os.environ['TESTBOX_CPU_COUNT'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_CPU_COUNT);
845 os.environ['TESTBOX_MEM_SIZE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_MEM_SIZE);
846 os.environ['TESTBOX_SCRATCH_SIZE'] = self.getSignOnParam(constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE);
847
848 testboxcommons.log('Successfully signed-on with Test Box ID #%s and given the name "%s"' \
849 % (self._idTestBox, self._sTestBoxName));
850
851 # Set up the scratch area.
852 self.reinitScratch(fUseTheForce = self._fFirstSignOn, cRetries = 2);
853
854 self._fFirstSignOn = False;
855 return True;
856
857 def _maybeSignOn(self):
858 """
859 Check if Test Box parameters were changed
860 and do sign-in in case of positive result
861 """
862
863 # Skip sign-on check if background command is currently in
864 # running state (avoid infinite signing on).
865 if self._oCommand.isRunning():
866 return None;
867
868 # Refresh sign-on parameters, changes triggers sign-on.
869 fNeedSignOn = not self._fSignedOn or self._fNeedReSignOn;
870 for sItem in self._ddSignOnParams: # pylint: disable=consider-using-dict-items
871 if self._ddSignOnParams[sItem][self.FN] is None:
872 continue
873
874 sOldValue = self._ddSignOnParams[sItem][self.VALUE]
875 self._ddSignOnParams[sItem][self.VALUE] = self._ddSignOnParams[sItem][self.FN]()
876 if sOldValue != self._ddSignOnParams[sItem][self.VALUE]:
877 fNeedSignOn = True
878 testboxcommons.log('Detected %s parameter change: %s -> %s'
879 % (sItem, sOldValue, self._ddSignOnParams[sItem][self.VALUE],))
880
881 if fNeedSignOn:
882 self._doSignOn();
883 return None;
884
885 def dispatch(self):
886 """
887 Receive orders from Test Manager and execute them
888 """
889
890 (self._idTestBox, self._sTestBoxName, self._fSignedOn) = self._oCommand.resumeIncompleteCommand();
891 self._fNeedReSignOn = self._fSignedOn;
892 if self._fSignedOn:
893 os.environ['TESTBOX_ID'] = str(self._idTestBox);
894 os.environ['TESTBOX_NAME'] = self._sTestBoxName;
895
896 while True:
897 # Make sure we're signed on before trying to do anything.
898 self._maybeSignOn();
899 while not self._fSignedOn:
900 iFactor = 1 if self._cSignOnAttempts < 100 else 4;
901 time.sleep(random.randint(self.kcSecMinSignOnDelay * iFactor, self.kcSecMaxSignOnDelay * iFactor));
902 self._maybeSignOn();
903
904 # Retrieve and handle command from the TM.
905 (oResponse, oConnection) = TestBoxConnection.requestCommandWithConnection(self._oOptions.sTestManagerUrl,
906 self._idTestBox,
907 self._sTestBoxUuid,
908 self._oCommand.isRunning());
909 if oResponse is not None:
910 self._oCommand.handleCommand(oResponse, oConnection);
911 if oConnection is not None:
912 if oConnection.isConnected():
913 self._oCommand.flushLogOnConnection(oConnection);
914 oConnection.close();
915
916 # Automatically reboot if scratch init fails.
917 #if self._cReinitScratchErrors > 8 and self.reinitScratch(cRetries = 3) is False:
918 # testboxcommons.log('Scratch does not initialize cleanly after %d attempts, rebooting...'
919 # % ( self._cReinitScratchErrors, ));
920 # self._oCommand.doReboot();
921
922 # delay a wee bit before looping.
923 ## @todo We shouldn't bother the server too frequently. We should try combine the test reporting done elsewhere
924 # with the command retrieval done here. I believe tinderclient.pl is capable of doing that.
925 iFactor = 1;
926 if self._cReinitScratchErrors > 0:
927 iFactor = 4;
928 time.sleep(random.randint(self.kcSecMinDelay * iFactor, self.kcSecMaxDelay * iFactor));
929
930 # Not reached.
931
932
933 @staticmethod
934 def main():
935 """
936 Main function a la C/C++. Returns exit code.
937 """
938
939 #
940 # Parse arguments.
941 #
942 sDefShareType = 'nfs' if utils.getHostOs() == 'solaris' else 'cifs';
943 if utils.getHostOs() in ('win', 'os2'):
944 sDefTestRsrc = 'T:';
945 sDefBuilds = 'U:';
946 elif utils.getHostOs() == 'darwin':
947 sDefTestRsrc = '/Volumes/testrsrc';
948 sDefBuilds = '/Volumes/builds';
949 else:
950 sDefTestRsrc = '/mnt/testrsrc';
951 sDefBuilds = '/mnt/builds';
952
953 class MyOptionParser(OptionParser):
954 """ We need to override the exit code on --help, error and so on. """
955 def __init__(self, *args, **kwargs):
956 OptionParser.__init__(self, *args, **kwargs);
957 def exit(self, status = 0, msg = None):
958 OptionParser.exit(self, TBS_EXITCODE_SYNTAX, msg);
959
960 parser = MyOptionParser(version=__version__[11:-1].strip());
961 for sMixed, sDefault, sDesc in [('Builds', sDefBuilds, 'builds'), ('TestRsrc', sDefTestRsrc, 'test resources') ]:
962 sLower = sMixed.lower();
963 sPrefix = 's' + sMixed;
964 parser.add_option('--' + sLower + '-path',
965 dest=sPrefix + 'Path', metavar='<abs-path>', default=sDefault,
966 help='Where ' + sDesc + ' can be found');
967 parser.add_option('--' + sLower + '-server-type',
968 dest=sPrefix + 'ServerType', metavar='<nfs|cifs>', default=sDefShareType,
969 help='The type of server, cifs (default) or nfs. If empty, we won\'t try mount anything.');
970 parser.add_option('--' + sLower + '-server-name',
971 dest=sPrefix + 'ServerName', metavar='<server>',
972 default='vboxstor.de.oracle.com' if sLower == 'builds' else 'teststor.de.oracle.com',
973 help='The name of the server with the builds.');
974 parser.add_option('--' + sLower + '-server-share',
975 dest=sPrefix + 'ServerShare', metavar='<share>', default=sLower,
976 help='The name of the builds share.');
977 parser.add_option('--' + sLower + '-server-user',
978 dest=sPrefix + 'ServerUser', metavar='<user>', default='guestr',
979 help='The user name to use when accessing the ' + sDesc + ' share.');
980 parser.add_option('--' + sLower + '-server-passwd', '--' + sLower + '-server-password',
981 dest=sPrefix + 'ServerPasswd', metavar='<password>', default='guestr',
982 help='The password to use when accessing the ' + sDesc + ' share.');
983 parser.add_option('--' + sLower + '-server-mountopt',
984 dest=sPrefix + 'ServerMountOpt', metavar='<mountopt>', default='',
985 help='The mount options to use when accessing the ' + sDesc + ' share.');
986
987 parser.add_option("--test-manager", metavar="<url>",
988 dest="sTestManagerUrl",
989 help="Test Manager URL",
990 default="http://tindertux.de.oracle.com/testmanager")
991 parser.add_option("--scratch-root", metavar="<abs-path>",
992 dest="sScratchRoot",
993 help="Path to the scratch directory",
994 default=None)
995 parser.add_option("--system-uuid", metavar="<uuid>",
996 dest="sSystemUuid",
997 help="The system UUID of the testbox, used for uniquely identifiying the machine",
998 default=None)
999 parser.add_option("--hwvirt",
1000 dest="fHasHwVirt", action="store_true", default=None,
1001 help="Hardware virtualization available in the CPU");
1002 parser.add_option("--no-hwvirt",
1003 dest="fHasHwVirt", action="store_false", default=None,
1004 help="Hardware virtualization not available in the CPU");
1005 parser.add_option("--nested-paging",
1006 dest="fHasNestedPaging", action="store_true", default=None,
1007 help="Nested paging is available");
1008 parser.add_option("--no-nested-paging",
1009 dest="fHasNestedPaging", action="store_false", default=None,
1010 help="Nested paging is not available");
1011 parser.add_option("--64-bit-guest",
1012 dest="fCan64BitGuest", action="store_true", default=None,
1013 help="Host can execute 64-bit guests");
1014 parser.add_option("--no-64-bit-guest",
1015 dest="fCan64BitGuest", action="store_false", default=None,
1016 help="Host cannot execute 64-bit guests");
1017 parser.add_option("--native-api",
1018 dest="fHasNativeApi", action="store_true", default=None,
1019 help="Native API virtualization is available");
1020 parser.add_option("--no-native-api",
1021 dest="fHasNativeApi", action="store_false", default=None,
1022 help="Native API virtualization is not available");
1023 parser.add_option("--io-mmu",
1024 dest="fHasIoMmu", action="store_true", default=None,
1025 help="I/O MMU available");
1026 parser.add_option("--no-io-mmu",
1027 dest="fHasIoMmu", action="store_false", default=None,
1028 help="No I/O MMU available");
1029 parser.add_option("--raw-mode",
1030 dest="fWithRawMode", action="store_true", default=None,
1031 help="Use raw-mode on this host.");
1032 parser.add_option("--no-raw-mode",
1033 dest="fWithRawMode", action="store_false", default=None,
1034 help="Disables raw-mode tests on this host.");
1035 parser.add_option("--pidfile",
1036 dest="sPidFile", default=None,
1037 help="For the parent script, ignored.");
1038 parser.add_option("-E", "--putenv", metavar = "<variable>=<value>", action = "append",
1039 dest = "asEnvVars", default = [],
1040 help = "Sets an environment variable. Can be repeated.");
1041 def sbp_callback(option, opt_str, value, parser):
1042 _, _, _ = opt_str, value, option
1043 parser.values.sTestManagerUrl = 'http://10.162.100.8/testmanager/'
1044 parser.values.sBuildsServerName = 'vbox-st02.ru.oracle.com'
1045 parser.values.sTestRsrcServerName = 'vbox-st02.ru.oracle.com'
1046 parser.values.sTestRsrcServerShare = 'scratch/data/testrsrc'
1047 parser.add_option("--spb", "--load-sbp-defaults", action="callback", callback=sbp_callback,
1048 help="Load defaults for the sbp setup.")
1049
1050 (oOptions, args) = parser.parse_args()
1051 # Check command line
1052 if args != []:
1053 parser.print_help();
1054 return TBS_EXITCODE_SYNTAX;
1055
1056 if oOptions.sSystemUuid is not None:
1057 uuid.UUID(oOptions.sSystemUuid);
1058 if not oOptions.sTestManagerUrl.startswith('http://') \
1059 and not oOptions.sTestManagerUrl.startswith('https://'):
1060 print('Syntax error: Invalid test manager URL "%s"' % (oOptions.sTestManagerUrl,));
1061 return TBS_EXITCODE_SYNTAX;
1062
1063 for sPrefix in ['sBuilds', 'sTestRsrc']:
1064 sType = getattr(oOptions, sPrefix + 'ServerType');
1065 if sType is None or not sType.strip():
1066 setattr(oOptions, sPrefix + 'ServerType', None);
1067 elif sType not in ['cifs', 'nfs']:
1068 print('Syntax error: Invalid server type "%s"' % (sType,));
1069 return TBS_EXITCODE_SYNTAX;
1070
1071
1072 #
1073 # Instantiate the testbox script and start dispatching work.
1074 #
1075 try:
1076 oTestBoxScript = TestBoxScript(oOptions);
1077 except TestBoxScriptException as oXcpt:
1078 print('Error: %s' % (oXcpt,));
1079 return TBS_EXITCODE_SYNTAX;
1080 oTestBoxScript.dispatch();
1081
1082 # Not supposed to get here...
1083 return TBS_EXITCODE_FAILURE;
1084
1085
1086
1087if __name__ == '__main__':
1088 sys.exit(TestBoxScript.main());
1089
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