VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/testboxcontroller.py@ 61318

Last change on this file since 61318 was 61318, checked in by vboxsync, 9 years ago

fix for new testboxes

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 43.8 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testboxcontroller.py 61318 2016-05-31 07:40:16Z vboxsync $
3
4"""
5Test Manager Core - Web Server Abstraction Base Class.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2015 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.215389.xyz. This file is free software;
14you can redistribute it and/or modify it under the terms of the GNU
15General Public License (GPL) as published by the Free Software
16Foundation, in version 2 as it comes in the "COPYING" file of the
17VirtualBox OSE distribution. VirtualBox OSE is distributed in the
18hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
19
20The contents of this file may alternatively be used under the terms
21of the Common Development and Distribution License Version 1.0
22(CDDL) only, as it comes in the "COPYING.CDDL" file of the
23VirtualBox OSE distribution, in which case the provisions of the
24CDDL are applicable instead of those of the GPL.
25
26You may elect to license modified versions of this file under the
27terms and conditions of either the GPL or the CDDL or both.
28"""
29__version__ = "$Revision: 61318 $"
30
31
32# Standard python imports.
33import re;
34import os;
35import string; # pylint: disable=W0402
36import sys;
37import uuid;
38
39# Validation Kit imports.
40from common import constants;
41from testmanager import config;
42from testmanager.core import coreconsts;
43from testmanager.core.db import TMDatabaseConnection;
44from testmanager.core.base import TMExceptionBase;
45from testmanager.core.globalresource import GlobalResourceLogic;
46from testmanager.core.testboxstatus import TestBoxStatusData, TestBoxStatusLogic;
47from testmanager.core.testbox import TestBoxData, TestBoxLogic;
48from testmanager.core.testresults import TestResultLogic;
49from testmanager.core.testset import TestSetData, TestSetLogic;
50from testmanager.core.systemlog import SystemLogData, SystemLogLogic;
51from testmanager.core.schedulerbase import SchedulerBase;
52
53# Python 3 hacks:
54if sys.version_info[0] >= 3:
55 long = int; # pylint: disable=W0622,C0103
56
57
58class TestBoxControllerException(TMExceptionBase):
59 """
60 Exception class for TestBoxController.
61 """
62 pass;
63
64
65class TestBoxController(object): # pylint: disable=R0903
66 """
67 TestBox Controller class.
68 """
69
70 ## Applicable testbox commands to an idle TestBox.
71 kasIdleCmds = [TestBoxData.ksTestBoxCmd_Reboot,
72 TestBoxData.ksTestBoxCmd_Upgrade,
73 TestBoxData.ksTestBoxCmd_UpgradeAndReboot,
74 TestBoxData.ksTestBoxCmd_Special];
75 ## Applicable testbox commands to a busy TestBox.
76 kasBusyCmds = [TestBoxData.ksTestBoxCmd_Abort, TestBoxData.ksTestBoxCmd_Reboot];
77 ## Commands that can be ACK'ed.
78 kasAckableCmds = [constants.tbresp.CMD_EXEC, constants.tbresp.CMD_ABORT, constants.tbresp.CMD_REBOOT,
79 constants.tbresp.CMD_UPGRADE, constants.tbresp.CMD_UPGRADE_AND_REBOOT, constants.tbresp.CMD_SPECIAL];
80 ## Commands that can be NACK'ed or NOTSUP'ed.
81 kasNackableCmds = kasAckableCmds + [kasAckableCmds, constants.tbresp.CMD_IDLE, constants.tbresp.CMD_WAIT];
82
83 ## Mapping from TestBoxCmd_T to TestBoxState_T
84 kdCmdToState = \
85 { \
86 TestBoxData.ksTestBoxCmd_Abort: None,
87 TestBoxData.ksTestBoxCmd_Reboot: TestBoxStatusData.ksTestBoxState_Rebooting,
88 TestBoxData.ksTestBoxCmd_Upgrade: TestBoxStatusData.ksTestBoxState_Upgrading,
89 TestBoxData.ksTestBoxCmd_UpgradeAndReboot: TestBoxStatusData.ksTestBoxState_UpgradingAndRebooting,
90 TestBoxData.ksTestBoxCmd_Special: TestBoxStatusData.ksTestBoxState_DoingSpecialCmd,
91 };
92
93 ## Mapping from TestBoxCmd_T to TestBox responses commands.
94 kdCmdToTbRespCmd = \
95 {
96 TestBoxData.ksTestBoxCmd_Abort: constants.tbresp.CMD_ABORT,
97 TestBoxData.ksTestBoxCmd_Reboot: constants.tbresp.CMD_REBOOT,
98 TestBoxData.ksTestBoxCmd_Upgrade: constants.tbresp.CMD_UPGRADE,
99 TestBoxData.ksTestBoxCmd_UpgradeAndReboot: constants.tbresp.CMD_UPGRADE_AND_REBOOT,
100 TestBoxData.ksTestBoxCmd_Special: constants.tbresp.CMD_SPECIAL,
101 };
102
103 ## Mapping from TestBox responses to TestBoxCmd_T commands.
104 kdTbRespCmdToCmd = \
105 {
106 constants.tbresp.CMD_IDLE: None,
107 constants.tbresp.CMD_WAIT: None,
108 constants.tbresp.CMD_EXEC: None,
109 constants.tbresp.CMD_ABORT: TestBoxData.ksTestBoxCmd_Abort,
110 constants.tbresp.CMD_REBOOT: TestBoxData.ksTestBoxCmd_Reboot,
111 constants.tbresp.CMD_UPGRADE: TestBoxData.ksTestBoxCmd_Upgrade,
112 constants.tbresp.CMD_UPGRADE_AND_REBOOT: TestBoxData.ksTestBoxCmd_UpgradeAndReboot,
113 constants.tbresp.CMD_SPECIAL: TestBoxData.ksTestBoxCmd_Special,
114 };
115
116
117 ## The path to the upgrade zip, relative WebServerGlueBase.getBaseUrl().
118 ksUpgradeZip = 'htdocs/upgrade/VBoxTestBoxScript.zip';
119
120 ## Valid TestBox result values.
121 kasValidResults = list(constants.result.g_kasValidResults);
122 ## Mapping TestBox result values to TestStatus_T values.
123 kadTbResultToStatus = \
124 {
125 constants.result.PASSED: TestSetData.ksTestStatus_Success,
126 constants.result.SKIPPED: TestSetData.ksTestStatus_Skipped,
127 constants.result.ABORTED: TestSetData.ksTestStatus_Aborted,
128 constants.result.BAD_TESTBOX: TestSetData.ksTestStatus_BadTestBox,
129 constants.result.FAILED: TestSetData.ksTestStatus_Failure,
130 constants.result.TIMED_OUT: TestSetData.ksTestStatus_TimedOut,
131 constants.result.REBOOTED: TestSetData.ksTestStatus_Rebooted,
132 };
133
134
135 def __init__(self, oSrvGlue):
136 """
137 Won't raise exceptions.
138 """
139 self._oSrvGlue = oSrvGlue;
140 self._sAction = None; # _getStandardParams / dispatchRequest sets this later on.
141 self._idTestBox = None; # _getStandardParams / dispatchRequest sets this later on.
142 self._sTestBoxUuid = None; # _getStandardParams / dispatchRequest sets this later on.
143 self._sTestBoxAddr = None; # _getStandardParams / dispatchRequest sets this later on.
144 self._idTestSet = None; # _getStandardParams / dispatchRequest sets this later on.
145 self._dParams = None; # _getStandardParams / dispatchRequest sets this later on.
146 self._asCheckedParams = [];
147 self._dActions = \
148 { \
149 constants.tbreq.SIGNON : self._actionSignOn,
150 constants.tbreq.REQUEST_COMMAND_BUSY: self._actionRequestCommandBusy,
151 constants.tbreq.REQUEST_COMMAND_IDLE: self._actionRequestCommandIdle,
152 constants.tbreq.COMMAND_ACK : self._actionCommandAck,
153 constants.tbreq.COMMAND_NACK : self._actionCommandNack,
154 constants.tbreq.COMMAND_NOTSUP : self._actionCommandNotSup,
155 constants.tbreq.LOG_MAIN : self._actionLogMain,
156 constants.tbreq.UPLOAD : self._actionUpload,
157 constants.tbreq.XML_RESULTS : self._actionXmlResults,
158 constants.tbreq.EXEC_COMPLETED : self._actionExecCompleted,
159 };
160
161 def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None):
162 """
163 Gets a string parameter (stripped).
164
165 Raises exception if not found and no default is provided, or if the
166 value isn't found in asValidValues.
167 """
168 if sName not in self._dParams:
169 if sDefValue is None:
170 raise TestBoxControllerException('%s parameter %s is missing' % (self._sAction, sName));
171 return sDefValue;
172 sValue = self._dParams[sName];
173 if fStrip:
174 sValue = sValue.strip();
175
176 if sName not in self._asCheckedParams:
177 self._asCheckedParams.append(sName);
178
179 if asValidValues is not None and sValue not in asValidValues:
180 raise TestBoxControllerException('%s parameter %s value "%s" not in %s ' \
181 % (self._sAction, sName, sValue, asValidValues));
182 return sValue;
183
184 def _getBoolParam(self, sName, fDefValue = None):
185 """
186 Gets a boolean parameter.
187
188 Raises exception if not found and no default is provided, or if not a
189 valid boolean.
190 """
191 sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue));
192 return sValue == 'True' or sValue == 'true' or sValue == '1';
193
194 def _getIntParam(self, sName, iMin = None, iMax = None):
195 """
196 Gets a string parameter.
197 Raises exception if not found, not a valid integer, or if the value
198 isn't in the range defined by iMin and iMax.
199 """
200 sValue = self._getStringParam(sName);
201 try:
202 iValue = int(sValue, 0);
203 except:
204 raise TestBoxControllerException('%s parameter %s value "%s" cannot be convert to an integer' \
205 % (self._sAction, sName, sValue));
206
207 if (iMin is not None and iValue < iMin) \
208 or (iMax is not None and iValue > iMax):
209 raise TestBoxControllerException('%s parameter %s value %d is out of range [%s..%s]' \
210 % (self._sAction, sName, iValue, iMin, iMax));
211 return iValue;
212
213 def _getLongParam(self, sName, lMin = None, lMax = None, lDefValue = None):
214 """
215 Gets a string parameter.
216 Raises exception if not found, not a valid long integer, or if the value
217 isn't in the range defined by lMin and lMax.
218 """
219 sValue = self._getStringParam(sName, sDefValue = (str(lDefValue) if lDefValue is not None else None));
220 try:
221 lValue = long(sValue, 0);
222 except Exception as oXcpt:
223 raise TestBoxControllerException('%s parameter %s value "%s" cannot be convert to an integer (%s)' \
224 % (self._sAction, sName, sValue, oXcpt));
225
226 if (lMin is not None and lValue < lMin) \
227 or (lMax is not None and lValue > lMax):
228 raise TestBoxControllerException('%s parameter %s value %d is out of range [%s..%s]' \
229 % (self._sAction, sName, lValue, lMin, lMax));
230 return lValue;
231
232 def _checkForUnknownParameters(self):
233 """
234 Check if we've handled all parameters, raises exception if anything
235 unknown was found.
236 """
237
238 if len(self._asCheckedParams) != len(self._dParams):
239 sUnknownParams = '';
240 for sKey in self._dParams:
241 if sKey not in self._asCheckedParams:
242 sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey];
243 raise TestBoxControllerException('Unknown parameters: ' + sUnknownParams);
244
245 return True;
246
247 def _writeResponse(self, dParams):
248 """
249 Makes a reply to the testbox script.
250 Will raise exception on failure.
251 """
252 self._oSrvGlue.writeParams(dParams);
253 self._oSrvGlue.flush();
254 return True;
255
256 def _resultResponse(self, sResultValue):
257 """
258 Makes a simple reply to the testbox script.
259 Will raise exception on failure.
260 """
261 return self._writeResponse({constants.tbresp.ALL_PARAM_RESULT: sResultValue});
262
263
264 def _idleResponse(self):
265 """
266 Makes an IDLE reply to the testbox script.
267 Will raise exception on failure.
268 """
269 return self._writeResponse({ constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.CMD_IDLE });
270
271 def _cleanupOldTest(self, oDb, oStatusData):
272 """
273 Cleans up any old test set that may be left behind and changes the
274 state to 'idle'. See scenario #9:
275 file://../../docs/AutomaticTestingRevamp.html#cleaning-up-abandond-testcase
276
277 Note. oStatusData.enmState is set to idle, but tsUpdated is not changed.
278 """
279
280 # Cleanup any abandond test.
281 if oStatusData.idTestSet is not None:
282 SystemLogLogic(oDb).addEntry(SystemLogData.ksEvent_TestSetAbandond,
283 "idTestSet=%u idTestBox=%u enmState=%s %s"
284 % (oStatusData.idTestSet, oStatusData.idTestBox,
285 oStatusData.enmState, self._sAction),
286 fCommit = False);
287 TestSetLogic(oDb).completeAsAbandond(oStatusData.idTestSet, fCommit = False);
288 GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False);
289
290 # Change to idle status
291 if oStatusData.enmState != TestBoxStatusData.ksTestBoxState_Idle:
292 TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
293 oStatusData.tsUpdated = oDb.getCurrentTimestamp();
294 oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle;
295
296 # Commit.
297 oDb.commit();
298
299 return True;
300
301 def _connectToDbAndValidateTb(self, asValidStates = None):
302 """
303 Connects to the database and validates the testbox.
304
305 Returns (TMDatabaseConnection, TestBoxStatusData, TestBoxData) on success.
306 Returns (None, None, None) on failure after sending the box an appropriate response.
307 May raise exception on DB error.
308 """
309 oDb = TMDatabaseConnection(self._oSrvGlue.dprint);
310 oLogic = TestBoxStatusLogic(oDb);
311 (oStatusData, oTestBoxData) = oLogic.tryFetchStatusAndConfig(self._idTestBox, self._sTestBoxUuid, self._sTestBoxAddr);
312 if oStatusData is None:
313 self._resultResponse(constants.tbresp.STATUS_DEAD);
314 elif asValidStates is not None and oStatusData.enmState not in asValidStates:
315 self._resultResponse(constants.tbresp.STATUS_NACK);
316 elif self._idTestSet is not None and self._idTestSet != oStatusData.idTestSet:
317 self._resultResponse(constants.tbresp.STATUS_NACK);
318 else:
319 return (oDb, oStatusData, oTestBoxData);
320 return (None, None, None);
321
322 def writeToMainLog(self, oTestSet, sText, fIgnoreSizeCheck = False):
323 """ Writes the text to the main log file. """
324
325 # Calc the file name and open the file.
326 sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-main.log');
327 if not os.path.exists(os.path.dirname(sFile)):
328 os.makedirs(os.path.dirname(sFile), 0o755);
329 oFile = open(sFile, 'ab');
330
331 # Check the size.
332 fSizeOk = True;
333 if not fIgnoreSizeCheck:
334 oStat = os.fstat(oFile.fileno());
335 fSizeOk = oStat.st_size / (1024 * 1024) < config.g_kcMbMaxMainLog;
336
337 # Write the text.
338 if fSizeOk:
339 if sys.version_info[0] >= 3:
340 oFile.write(bytes(sText, 'utf-8'));
341 else:
342 oFile.write(sText);
343
344 # Done
345 oFile.close();
346 return fSizeOk;
347
348 def _actionSignOn(self): # pylint: disable=R0914
349 """ Implement sign-on """
350
351 #
352 # Validate parameters (raises exception on failure).
353 #
354 sOs = self._getStringParam(constants.tbreq.SIGNON_PARAM_OS, coreconsts.g_kasOses);
355 sOsVersion = self._getStringParam(constants.tbreq.SIGNON_PARAM_OS_VERSION);
356 sCpuVendor = self._getStringParam(constants.tbreq.SIGNON_PARAM_CPU_VENDOR);
357 sCpuArch = self._getStringParam(constants.tbreq.SIGNON_PARAM_CPU_ARCH, coreconsts.g_kasCpuArches);
358 sCpuName = self._getStringParam(constants.tbreq.SIGNON_PARAM_CPU_NAME, fStrip = True, sDefValue = ''); # new
359 lCpuRevision = self._getLongParam( constants.tbreq.SIGNON_PARAM_CPU_REVISION, lMin = 0, lDefValue = 0); # new
360 cCpus = self._getIntParam( constants.tbreq.SIGNON_PARAM_CPU_COUNT, 1, 16384);
361 fCpuHwVirt = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_HW_VIRT);
362 fCpuNestedPaging = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_NESTED_PAGING);
363 fCpu64BitGuest = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_64_BIT_GUEST, fDefValue = True);
364 fChipsetIoMmu = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_IOMMU);
365 cMbMemory = self._getLongParam( constants.tbreq.SIGNON_PARAM_MEM_SIZE, 8, 1073741823); # 8MB..1PB
366 cMbScratch = self._getLongParam( constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE, 0, 1073741823); # 0..1PB
367 sReport = self._getStringParam(constants.tbreq.SIGNON_PARAM_REPORT, fStrip = True, sDefValue = ''); # new
368 iTestBoxScriptRev = self._getIntParam( constants.tbreq.SIGNON_PARAM_SCRIPT_REV, 1, 100000000);
369 iPythonHexVersion = self._getIntParam( constants.tbreq.SIGNON_PARAM_PYTHON_VERSION, 0x020300f0, 0x030f00f0);
370 self._checkForUnknownParameters();
371
372 # Null conversions for new parameters.
373 if len(sReport) == 0:
374 sReport = None;
375 if len(sCpuName) == 0:
376 sCpuName = None;
377 if lCpuRevision <= 0:
378 lCpuRevision = None;
379
380 #
381 # Connect to the database and validate the testbox.
382 #
383 oDb = TMDatabaseConnection(self._oSrvGlue.dprint);
384 oTestBoxLogic = TestBoxLogic(oDb);
385 oTestBox = oTestBoxLogic.tryFetchTestBoxByUuid(self._sTestBoxUuid);
386 if oTestBox is None:
387 oSystemLogLogic = SystemLogLogic(oDb);
388 oSystemLogLogic.addEntry(SystemLogData.ksEvent_TestBoxUnknown,
389 'addr=%s uuid=%s os=%s %d cpus' \
390 % (self._sTestBoxAddr, self._sTestBoxUuid, sOs, cCpus),
391 24, fCommit = True);
392 return self._resultResponse(constants.tbresp.STATUS_NACK);
393
394 #
395 # Update the row in TestBoxes if something changed.
396 #
397 if oTestBox.cMbScratch != 0:
398 cPctScratchDiff = (cMbScratch - oTestBox.cMbScratch) * 100 / oTestBox.cMbScratch;
399 else:
400 cPctScratchDiff = 100;
401
402 # pylint: disable=R0916
403 if self._sTestBoxAddr != oTestBox.ip \
404 or sOs != oTestBox.sOs \
405 or sOsVersion != oTestBox.sOsVersion \
406 or sCpuVendor != oTestBox.sCpuVendor \
407 or sCpuArch != oTestBox.sCpuArch \
408 or sCpuName != oTestBox.sCpuName \
409 or lCpuRevision != oTestBox.lCpuRevision \
410 or cCpus != oTestBox.cCpus \
411 or fCpuHwVirt != oTestBox.fCpuHwVirt \
412 or fCpuNestedPaging != oTestBox.fCpuNestedPaging \
413 or fCpu64BitGuest != oTestBox.fCpu64BitGuest \
414 or fChipsetIoMmu != oTestBox.fChipsetIoMmu \
415 or cMbMemory != oTestBox.cMbMemory \
416 or abs(cPctScratchDiff) >= min(4 + cMbScratch / 10240, 12) \
417 or sReport != oTestBox.sReport \
418 or iTestBoxScriptRev != oTestBox.iTestBoxScriptRev \
419 or iPythonHexVersion != oTestBox.iPythonHexVersion:
420 oTestBoxLogic.updateOnSignOn(oTestBox.idTestBox,
421 oTestBox.idGenTestBox,
422 sTestBoxAddr = self._sTestBoxAddr,
423 sOs = sOs,
424 sOsVersion = sOsVersion,
425 sCpuVendor = sCpuVendor,
426 sCpuArch = sCpuArch,
427 sCpuName = sCpuName,
428 lCpuRevision = lCpuRevision,
429 cCpus = cCpus,
430 fCpuHwVirt = fCpuHwVirt,
431 fCpuNestedPaging = fCpuNestedPaging,
432 fCpu64BitGuest = fCpu64BitGuest,
433 fChipsetIoMmu = fChipsetIoMmu,
434 cMbMemory = cMbMemory,
435 cMbScratch = cMbScratch,
436 sReport = sReport,
437 iTestBoxScriptRev = iTestBoxScriptRev,
438 iPythonHexVersion = iPythonHexVersion);
439
440 #
441 # Update the testbox status, making sure there is a status.
442 #
443 oStatusLogic = TestBoxStatusLogic(oDb);
444 oStatusData = oStatusLogic.tryFetchStatus(oTestBox.idTestBox);
445 if oStatusData is not None:
446 self._cleanupOldTest(oDb, oStatusData);
447 else:
448 oStatusLogic.insertIdleStatus(oTestBox.idTestBox, oTestBox.idGenTestBox, fCommit = True);
449
450 #
451 # ACK the request.
452 #
453 dResponse = \
454 {
455 constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.STATUS_ACK,
456 constants.tbresp.SIGNON_PARAM_ID: oTestBox.idTestBox,
457 constants.tbresp.SIGNON_PARAM_NAME: oTestBox.sName,
458 }
459 return self._writeResponse(dResponse);
460
461 def _doGangCleanup(self, oDb, oStatusData):
462 """
463 _doRequestCommand worker for handling a box in gang-cleanup.
464 This will check if all testboxes has completed their run, pretending to
465 be busy until that happens. Once all are completed, resources will be
466 freed and the testbox returns to idle state (we update oStatusData).
467 """
468 oStatusLogic = TestBoxStatusLogic(oDb)
469 oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
470 if oStatusLogic.isWholeGangDoneTesting(oTestSet.idTestSetGangLeader):
471 oDb.begin();
472
473 GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False);
474 TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
475
476 oStatusData.tsUpdated = oDb.getCurrentTimestamp();
477 oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle;
478
479 oDb.commit();
480 return None;
481
482 def _doGangGatheringTimedOut(self, oDb, oStatusData):
483 """
484 _doRequestCommand worker for handling a box in gang-gathering-timed-out state.
485 This will do clean-ups similar to _cleanupOldTest and update the state likewise.
486 """
487 oDb.begin();
488
489 TestSetLogic(oDb).completeAsGangGatheringTimeout(oStatusData.idTestSet, fCommit = False);
490 GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False);
491 TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
492
493 oStatusData.tsUpdated = oDb.getCurrentTimestamp();
494 oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle;
495
496 oDb.commit();
497 return None;
498
499 def _doGangGathering(self, oDb, oStatusData):
500 """
501 _doRequestCommand worker for handling a box in gang-gathering state.
502 This only checks for timeout. It will update the oStatusData if a
503 timeout is detected, so that the box will be idle upon return.
504 """
505 oStatusLogic = TestBoxStatusLogic(oDb);
506 if oStatusLogic.timeSinceLastChangeInSecs(oStatusData) > config.g_kcSecGangGathering \
507 and SchedulerBase.tryCancelGangGathering(oDb, oStatusData): # <-- Updates oStatusData.
508 self._doGangGatheringTimedOut(oDb, oStatusData);
509 return None;
510
511 def _doRequestCommand(self, fIdle):
512 """
513 Common code for handling command request.
514 """
515
516 (oDb, oStatusData, oTestBoxData) = self._connectToDbAndValidateTb();
517 if oDb is None:
518 return False;
519
520 #
521 # Status clean up.
522 #
523 # Only when BUSY will the TestBox Script request and execute commands
524 # concurrently. So, it must be idle when sending REQUEST_COMMAND_IDLE.
525 #
526 if fIdle:
527 if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGathering:
528 self._doGangGathering(oDb, oStatusData);
529 elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut:
530 self._doGangGatheringTimedOut(oDb, oStatusData);
531 elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangTesting:
532 dResponse = SchedulerBase.composeExecResponse(oDb, oTestBoxData.idTestBox, self._oSrvGlue.getBaseUrl());
533 if dResponse is not None:
534 return dResponse;
535 elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangCleanup:
536 self._doGangCleanup(oDb, oStatusData);
537 elif oStatusData.enmState != TestBoxStatusData.ksTestBoxState_Idle: # (includes ksTestBoxState_GangGatheringTimedOut)
538 self._cleanupOldTest(oDb, oStatusData);
539
540 #
541 # Check for pending command.
542 #
543 if oTestBoxData.enmPendingCmd != TestBoxData.ksTestBoxCmd_None:
544 asValidCmds = TestBoxController.kasIdleCmds if fIdle else TestBoxController.kasBusyCmds;
545 if oTestBoxData.enmPendingCmd in asValidCmds:
546 dResponse = { constants.tbresp.ALL_PARAM_RESULT: TestBoxController.kdCmdToTbRespCmd[oTestBoxData.enmPendingCmd] };
547 if oTestBoxData.enmPendingCmd in [TestBoxData.ksTestBoxCmd_Upgrade, TestBoxData.ksTestBoxCmd_UpgradeAndReboot]:
548 dResponse[constants.tbresp.UPGRADE_PARAM_URL] = self._oSrvGlue.getBaseUrl() + TestBoxController.ksUpgradeZip;
549 return self._writeResponse(dResponse);
550
551 if oTestBoxData.enmPendingCmd == TestBoxData.ksTestBoxCmd_Abort and fIdle:
552 TestBoxLogic(oDb).setCommand(self._idTestBox, sOldCommand = oTestBoxData.enmPendingCmd,
553 sNewCommand = TestBoxData.ksTestBoxCmd_None, fCommit = True);
554
555 #
556 # If doing gang stuff, return 'CMD_WAIT'.
557 #
558 ## @todo r=bird: Why is GangTesting included here? Figure out when testing gang testing.
559 if oStatusData.enmState in [TestBoxStatusData.ksTestBoxState_GangGathering,
560 TestBoxStatusData.ksTestBoxState_GangTesting,
561 TestBoxStatusData.ksTestBoxState_GangCleanup]:
562 return self._resultResponse(constants.tbresp.CMD_WAIT);
563
564 #
565 # If idling and enabled try schedule a new task.
566 #
567 if fIdle \
568 and oTestBoxData.fEnabled \
569 and oStatusData.enmState == TestBoxStatusData.ksTestBoxState_Idle: # (paranoia)
570 dResponse = SchedulerBase.scheduleNewTask(oDb, oTestBoxData, self._oSrvGlue.getBaseUrl());
571 if dResponse is not None:
572 return self._writeResponse(dResponse);
573
574 #
575 # Touch the status row every couple of mins so we can tell that the box is alive.
576 #
577 oStatusLogic = TestBoxStatusLogic(oDb);
578 if oStatusData.enmState != TestBoxStatusData.ksTestBoxState_GangGathering \
579 and oStatusLogic.timeSinceLastChangeInSecs(oStatusData) >= TestBoxStatusLogic.kcSecIdleTouchStatus:
580 oStatusLogic.touchStatus(oTestBoxData.idTestBox, fCommit = True);
581
582 return self._idleResponse();
583
584 def _actionRequestCommandBusy(self):
585 """ Implement request for command. """
586 self._checkForUnknownParameters();
587 return self._doRequestCommand(False);
588
589 def _actionRequestCommandIdle(self):
590 """ Implement request for command. """
591 self._checkForUnknownParameters();
592 return self._doRequestCommand(True);
593
594 def _doCommandAckNck(self, sCmd):
595 """ Implements ACK, NACK and NACK(ENOTSUP). """
596
597 (oDb, _, _) = self._connectToDbAndValidateTb();
598 if oDb is None:
599 return False;
600
601 #
602 # If the command maps to a TestBoxCmd_T value, it means we have to
603 # check and update TestBoxes. If it's an ACK, the testbox status will
604 # need updating as well.
605 #
606 sPendingCmd = TestBoxController.kdTbRespCmdToCmd[sCmd];
607 if sPendingCmd is not None:
608 oTestBoxLogic = TestBoxLogic(oDb)
609 oTestBoxLogic.setCommand(self._idTestBox, sOldCommand = sPendingCmd,
610 sNewCommand = TestBoxData.ksTestBoxCmd_None, fCommit = False);
611
612 if self._sAction == constants.tbreq.COMMAND_ACK \
613 and TestBoxController.kdCmdToState[sPendingCmd] is not None:
614 oStatusLogic = TestBoxStatusLogic(oDb);
615 oStatusLogic.updateState(self._idTestBox, TestBoxController.kdCmdToState[sPendingCmd], fCommit = False);
616
617 # Commit the two updates.
618 oDb.commit();
619
620 #
621 # Log NACKs.
622 #
623 if self._sAction != constants.tbreq.COMMAND_ACK:
624 oSysLogLogic = SystemLogLogic(oDb);
625 oSysLogLogic.addEntry(SystemLogData.ksEvent_CmdNacked,
626 'idTestBox=%s sCmd=%s' % (self._idTestBox, sPendingCmd),
627 24, fCommit = True);
628
629 return self._resultResponse(constants.tbresp.STATUS_ACK);
630
631 def _actionCommandAck(self):
632 """ Implement command ACK'ing """
633 sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasAckableCmds);
634 self._checkForUnknownParameters();
635 return self._doCommandAckNck(sCmd);
636
637 def _actionCommandNack(self):
638 """ Implement command NACK'ing """
639 sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasNackableCmds);
640 self._checkForUnknownParameters();
641 return self._doCommandAckNck(sCmd);
642
643 def _actionCommandNotSup(self):
644 """ Implement command NACK(ENOTSUP)'ing """
645 sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasNackableCmds);
646 self._checkForUnknownParameters();
647 return self._doCommandAckNck(sCmd);
648
649 def _actionLogMain(self):
650 """ Implement submitting log entries to the main log file. """
651 #
652 # Parameter validation.
653 #
654 sBody = self._getStringParam(constants.tbreq.LOG_PARAM_BODY, fStrip = False);
655 if len(sBody) == 0:
656 return self._resultResponse(constants.tbresp.STATUS_NACK);
657 self._checkForUnknownParameters();
658
659 (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
660 TestBoxStatusData.ksTestBoxState_GangTesting]);
661 if oStatusData is None:
662 return False;
663
664 #
665 # Write the text to the log file.
666 #
667 oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
668 self.writeToMainLog(oTestSet, sBody);
669 ## @todo Overflow is a hanging offence, need to note it and fail whatever is going on...
670
671 # Done.
672 return self._resultResponse(constants.tbresp.STATUS_ACK);
673
674 def _actionUpload(self):
675 """ Implement uploading of files. """
676 #
677 # Parameter validation.
678 #
679 sName = self._getStringParam(constants.tbreq.UPLOAD_PARAM_NAME);
680 sMime = self._getStringParam(constants.tbreq.UPLOAD_PARAM_MIME);
681 sKind = self._getStringParam(constants.tbreq.UPLOAD_PARAM_KIND);
682 sDesc = self._getStringParam(constants.tbreq.UPLOAD_PARAM_DESC);
683 self._checkForUnknownParameters();
684
685 (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
686 TestBoxStatusData.ksTestBoxState_GangTesting]);
687 if oStatusData is None:
688 return False;
689
690 if len(sName) > 128 or len(sName) < 3:
691 raise TestBoxControllerException('Invalid file name "%s"' % (sName,));
692 if re.match(r'^[a-zA-Z0-9_\-(){}#@+,.=]*$', sName) is None:
693 raise TestBoxControllerException('Invalid file name "%s"' % (sName,));
694
695 if sMime not in [ 'text/plain', #'text/html', 'text/xml',
696 'application/octet-stream',
697 'image/png', #'image/gif', 'image/jpeg',
698 #'video/webm', 'video/mpeg', 'video/mpeg4-generic',
699 ]:
700 raise TestBoxControllerException('Invalid MIME type "%s"' % (sMime,));
701
702 if sKind not in [ 'log/release/vm',
703 'log/debug/vm',
704 'log/release/svc',
705 'log/debug/svc',
706 'log/release/client',
707 'log/debug/client',
708 'log/installer',
709 'log/uninstaller',
710 'crash/report/vm',
711 'crash/dump/vm',
712 'crash/report/svc',
713 'crash/dump/svc',
714 'crash/report/client',
715 'crash/dump/client',
716 'misc/other',
717 'screenshot/failure',
718 'screenshot/success',
719 #'screencapture/failure',
720 ]:
721 raise TestBoxControllerException('Invalid kind "%s"' % (sKind,));
722
723 if len(sDesc) > 256:
724 raise TestBoxControllerException('Invalid description "%s"' % (sDesc,));
725 if not set(sDesc).issubset(set(string.printable)):
726 raise TestBoxControllerException('Invalid description "%s"' % (sDesc,));
727
728 if ('application/octet-stream', {}) != self._oSrvGlue.getContentType():
729 raise TestBoxControllerException('Unexpected content type: %s; %s' % self._oSrvGlue.getContentType());
730
731 cbFile = self._oSrvGlue.getContentLength();
732 if cbFile <= 0:
733 raise TestBoxControllerException('File "%s" is empty or negative in size (%s)' % (sName, cbFile));
734 if (cbFile + 1048575) / 1048576 > config.g_kcMbMaxUploadSingle:
735 raise TestBoxControllerException('File "%s" is too big %u bytes (max %u MiB)'
736 % (sName, cbFile, config.g_kcMbMaxUploadSingle,));
737
738 #
739 # Write the text to the log file.
740 #
741 oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
742 oDstFile = TestSetLogic(oDb).createFile(oTestSet, sName = sName, sMime = sMime, sKind = sKind, sDesc = sDesc,
743 cbFile = cbFile, fCommit = True);
744
745 offFile = 0;
746 oSrcFile = self._oSrvGlue.getBodyIoStream();
747 while offFile < cbFile:
748 cbToRead = cbFile - offFile;
749 if cbToRead > 256*1024:
750 cbToRead = 256*1024;
751 offFile += cbToRead;
752
753 abBuf = oSrcFile.read(cbToRead);
754 oDstFile.write(abBuf); # pylint: disable=E1103
755 del abBuf;
756
757 oDstFile.close(); # pylint: disable=E1103
758
759 # Done.
760 return self._resultResponse(constants.tbresp.STATUS_ACK);
761
762 def _actionXmlResults(self):
763 """ Implement submitting "XML" like test result stream. """
764 #
765 # Parameter validation.
766 #
767 sXml = self._getStringParam(constants.tbreq.XML_RESULT_PARAM_BODY, fStrip = False);
768 self._checkForUnknownParameters();
769 if len(sXml) == 0: # Used for link check by vboxinstaller.py on Windows.
770 return self._resultResponse(constants.tbresp.STATUS_ACK);
771
772 (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
773 TestBoxStatusData.ksTestBoxState_GangTesting]);
774 if oStatusData is None:
775 return False;
776
777 #
778 # Process the XML.
779 #
780 (sError, fUnforgivable) = TestResultLogic(oDb).processXmlStream(sXml, self._idTestSet);
781 if sError is not None:
782 oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
783 self.writeToMainLog(oTestSet, '\n!!XML error: %s\n%s\n\n' % (sError, sXml,));
784 if fUnforgivable:
785 return self._resultResponse(constants.tbresp.STATUS_NACK);
786 return self._resultResponse(constants.tbresp.STATUS_ACK);
787
788
789 def _actionExecCompleted(self):
790 """
791 Implement EXEC completion.
792
793 Because the action is request by the worker thread of the testbox
794 script we cannot pass pending commands back to it like originally
795 planned. So, we just complete the test set and update the status.
796 """
797 #
798 # Parameter validation.
799 #
800 sStatus = self._getStringParam(constants.tbreq.EXEC_COMPLETED_PARAM_RESULT, TestBoxController.kasValidResults);
801 self._checkForUnknownParameters();
802
803 (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
804 TestBoxStatusData.ksTestBoxState_GangTesting]);
805 if oStatusData is None:
806 return False;
807
808 #
809 # Complete the status.
810 #
811 oDb.rollback();
812 oDb.begin();
813 oTestSetLogic = TestSetLogic(oDb);
814 idTestSetGangLeader = oTestSetLogic.complete(oStatusData.idTestSet, self.kadTbResultToStatus[sStatus], fCommit = False);
815
816 oStatusLogic = TestBoxStatusLogic(oDb);
817 if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_Testing:
818 assert idTestSetGangLeader is None;
819 GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox);
820 oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
821 else:
822 assert idTestSetGangLeader is not None;
823 oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_GangCleanup, oStatusData.idTestSet,
824 fCommit = False);
825 if oStatusLogic.isWholeGangDoneTesting(idTestSetGangLeader):
826 GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox);
827 oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
828
829 oDb.commit();
830 return self._resultResponse(constants.tbresp.STATUS_ACK);
831
832
833
834 def _getStandardParams(self, dParams):
835 """
836 Gets the standard parameters and validates them.
837
838 The parameters are returned as a tuple: sAction, idTestBox, sTestBoxUuid.
839 Note! the sTextBoxId can be None if it's a SIGNON request.
840
841 Raises TestBoxControllerException on invalid input.
842 """
843 #
844 # Get the action parameter and validate it.
845 #
846 if constants.tbreq.ALL_PARAM_ACTION not in dParams:
847 raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
848 % (constants.tbreq.ALL_PARAM_ACTION, dParams,));
849 sAction = dParams[constants.tbreq.ALL_PARAM_ACTION];
850
851 if sAction not in self._dActions:
852 raise TestBoxControllerException('Unknown action "%s" in request (params: %s; action: %s)' \
853 % (sAction, dParams, self._dActions));
854
855 #
856 # TestBox UUID.
857 #
858 if constants.tbreq.ALL_PARAM_TESTBOX_UUID not in dParams:
859 raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
860 % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, dParams,));
861 sTestBoxUuid = dParams[constants.tbreq.ALL_PARAM_TESTBOX_UUID];
862 try:
863 sTestBoxUuid = str(uuid.UUID(sTestBoxUuid));
864 except Exception as oXcpt:
865 raise TestBoxControllerException('Invalid %s parameter value "%s": %s ' \
866 % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, sTestBoxUuid, oXcpt));
867 if sTestBoxUuid == '00000000-0000-0000-0000-000000000000':
868 raise TestBoxControllerException('Invalid %s parameter value "%s": NULL UUID not allowed.' \
869 % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, sTestBoxUuid));
870
871 #
872 # TestBox ID.
873 #
874 if constants.tbreq.ALL_PARAM_TESTBOX_ID in dParams:
875 sTestBoxId = dParams[constants.tbreq.ALL_PARAM_TESTBOX_ID];
876 try:
877 idTestBox = int(sTestBoxId);
878 if idTestBox <= 0 or idTestBox >= 0x7fffffff:
879 raise Exception;
880 except:
881 raise TestBoxControllerException('Bad value for "%s": "%s"' \
882 % (constants.tbreq.ALL_PARAM_TESTBOX_ID, sTestBoxId));
883 elif sAction == constants.tbreq.SIGNON:
884 idTestBox = None;
885 else:
886 raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
887 % (constants.tbreq.ALL_PARAM_TESTBOX_ID, dParams,));
888
889 #
890 # Test Set ID.
891 #
892 if constants.tbreq.RESULT_PARAM_TEST_SET_ID in dParams:
893 sTestSetId = dParams[constants.tbreq.RESULT_PARAM_TEST_SET_ID];
894 try:
895 idTestSet = int(sTestSetId);
896 if idTestSet <= 0 or idTestSet >= 0x7fffffff:
897 raise Exception;
898 except:
899 raise TestBoxControllerException('Bad value for "%s": "%s"' \
900 % (constants.tbreq.RESULT_PARAM_TEST_SET_ID, sTestSetId));
901 elif sAction not in [ constants.tbreq.XML_RESULTS, ]: ## More later.
902 idTestSet = None;
903 else:
904 raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
905 % (constants.tbreq.RESULT_PARAM_TEST_SET_ID, dParams,));
906
907 #
908 # The testbox address.
909 #
910 sTestBoxAddr = self._oSrvGlue.getClientAddr();
911 if sTestBoxAddr is None or sTestBoxAddr.strip() == '':
912 raise TestBoxControllerException('Invalid client address "%s"' % (sTestBoxAddr,));
913
914 #
915 # Update the list of checked parameters.
916 #
917 self._asCheckedParams.extend([constants.tbreq.ALL_PARAM_TESTBOX_UUID, constants.tbreq.ALL_PARAM_ACTION]);
918 if idTestBox is not None:
919 self._asCheckedParams.append(constants.tbreq.ALL_PARAM_TESTBOX_ID);
920 if idTestSet is not None:
921 self._asCheckedParams.append(constants.tbreq.RESULT_PARAM_TEST_SET_ID);
922
923 return (sAction, idTestBox, sTestBoxUuid, sTestBoxAddr, idTestSet);
924
925 def dispatchRequest(self):
926 """
927 Dispatches the incoming request.
928
929 Will raise TestBoxControllerException on failure.
930 """
931
932 #
933 # Must be a POST request.
934 #
935 try:
936 sMethod = self._oSrvGlue.getMethod();
937 except Exception as oXcpt:
938 raise TestBoxControllerException('Error retriving request method: %s' % (oXcpt,));
939 if sMethod != 'POST':
940 raise TestBoxControllerException('Error expected POST request not "%s"' % (sMethod,));
941
942 #
943 # Get the parameters and checks for duplicates.
944 #
945 try:
946 dParams = self._oSrvGlue.getParameters();
947 except Exception as oXcpt:
948 raise TestBoxControllerException('Error retriving parameters: %s' % (oXcpt,));
949 for sKey in dParams.keys():
950 if len(dParams[sKey]) > 1:
951 raise TestBoxControllerException('Parameter "%s" is given multiple times: %s' % (sKey, dParams[sKey]));
952 dParams[sKey] = dParams[sKey][0];
953 self._dParams = dParams;
954
955 #
956 # Get+validate the standard action parameters and dispatch the request.
957 #
958 (self._sAction, self._idTestBox, self._sTestBoxUuid, self._sTestBoxAddr, self._idTestSet) = \
959 self._getStandardParams(dParams);
960 return self._dActions[self._sAction]();
961
Note: See TracBrowser for help on using the repository browser.

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