VirtualBox

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

Last change on this file since 82968 was 82968, checked in by vboxsync, 5 years ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 43.5 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testboxcontroller.py 82968 2020-02-04 10:35:17Z vboxsync $
3
4"""
5Test Manager Core - Web Server Abstraction Base Class.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2020 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.virtualbox.org. 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: 82968 $"
30
31
32# Standard python imports.
33import re;
34import os;
35import string; # pylint: disable=deprecated-module
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, TestResultFileData;
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=redefined-builtin,invalid-name
56
57
58class TestBoxControllerException(TMExceptionBase):
59 """
60 Exception class for TestBoxController.
61 """
62 pass; # pylint: disable=unnecessary-pass
63
64
65class TestBoxController(object): # pylint: disable=too-few-public-methods
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 in ('True', 'true', '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-abandoned-testcase
276
277 Note. oStatusData.enmState is set to idle, but tsUpdated is not changed.
278 """
279
280 # Cleanup any abandoned test.
281 if oStatusData.idTestSet is not None:
282 SystemLogLogic(oDb).addEntry(SystemLogData.ksEvent_TestSetAbandoned,
283 "idTestSet=%u idTestBox=%u enmState=%s %s"
284 % (oStatusData.idTestSet, oStatusData.idTestBox,
285 oStatusData.enmState, self._sAction),
286 fCommit = False);
287 TestSetLogic(oDb).completeAsAbandoned(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=too-many-locals
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 fRawMode = self._getBoolParam( constants.tbreq.SIGNON_PARAM_WITH_RAW_MODE, fDefValue = None);
366 cMbMemory = self._getLongParam( constants.tbreq.SIGNON_PARAM_MEM_SIZE, 8, 1073741823); # 8MB..1PB
367 cMbScratch = self._getLongParam( constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE, 0, 1073741823); # 0..1PB
368 sReport = self._getStringParam(constants.tbreq.SIGNON_PARAM_REPORT, fStrip = True, sDefValue = ''); # new
369 iTestBoxScriptRev = self._getIntParam( constants.tbreq.SIGNON_PARAM_SCRIPT_REV, 1, 100000000);
370 iPythonHexVersion = self._getIntParam( constants.tbreq.SIGNON_PARAM_PYTHON_VERSION, 0x020300f0, 0x030f00f0);
371 self._checkForUnknownParameters();
372
373 # Null conversions for new parameters.
374 if not sReport:
375 sReport = None;
376 if not sCpuName:
377 sCpuName = None;
378 if lCpuRevision <= 0:
379 lCpuRevision = None;
380
381 #
382 # Connect to the database and validate the testbox.
383 #
384 oDb = TMDatabaseConnection(self._oSrvGlue.dprint);
385 oTestBoxLogic = TestBoxLogic(oDb);
386 oTestBox = oTestBoxLogic.tryFetchTestBoxByUuid(self._sTestBoxUuid);
387 if oTestBox is None:
388 oSystemLogLogic = SystemLogLogic(oDb);
389 oSystemLogLogic.addEntry(SystemLogData.ksEvent_TestBoxUnknown,
390 'addr=%s uuid=%s os=%s %d cpus' \
391 % (self._sTestBoxAddr, self._sTestBoxUuid, sOs, cCpus),
392 24, fCommit = True);
393 return self._resultResponse(constants.tbresp.STATUS_NACK);
394
395 #
396 # Update the row in TestBoxes if something changed.
397 #
398 if oTestBox.cMbScratch is not None and oTestBox.cMbScratch != 0:
399 cPctScratchDiff = (cMbScratch - oTestBox.cMbScratch) * 100 / oTestBox.cMbScratch;
400 else:
401 cPctScratchDiff = 100;
402
403 # pylint: disable=too-many-boolean-expressions
404 if self._sTestBoxAddr != oTestBox.ip \
405 or sOs != oTestBox.sOs \
406 or sOsVersion != oTestBox.sOsVersion \
407 or sCpuVendor != oTestBox.sCpuVendor \
408 or sCpuArch != oTestBox.sCpuArch \
409 or sCpuName != oTestBox.sCpuName \
410 or lCpuRevision != oTestBox.lCpuRevision \
411 or cCpus != oTestBox.cCpus \
412 or fCpuHwVirt != oTestBox.fCpuHwVirt \
413 or fCpuNestedPaging != oTestBox.fCpuNestedPaging \
414 or fCpu64BitGuest != oTestBox.fCpu64BitGuest \
415 or fChipsetIoMmu != oTestBox.fChipsetIoMmu \
416 or fRawMode != oTestBox.fRawMode \
417 or cMbMemory != oTestBox.cMbMemory \
418 or abs(cPctScratchDiff) >= min(4 + cMbScratch / 10240, 12) \
419 or sReport != oTestBox.sReport \
420 or iTestBoxScriptRev != oTestBox.iTestBoxScriptRev \
421 or iPythonHexVersion != oTestBox.iPythonHexVersion:
422 oTestBoxLogic.updateOnSignOn(oTestBox.idTestBox,
423 oTestBox.idGenTestBox,
424 sTestBoxAddr = self._sTestBoxAddr,
425 sOs = sOs,
426 sOsVersion = sOsVersion,
427 sCpuVendor = sCpuVendor,
428 sCpuArch = sCpuArch,
429 sCpuName = sCpuName,
430 lCpuRevision = lCpuRevision,
431 cCpus = cCpus,
432 fCpuHwVirt = fCpuHwVirt,
433 fCpuNestedPaging = fCpuNestedPaging,
434 fCpu64BitGuest = fCpu64BitGuest,
435 fChipsetIoMmu = fChipsetIoMmu,
436 fRawMode = fRawMode,
437 cMbMemory = cMbMemory,
438 cMbScratch = cMbScratch,
439 sReport = sReport,
440 iTestBoxScriptRev = iTestBoxScriptRev,
441 iPythonHexVersion = iPythonHexVersion);
442
443 #
444 # Update the testbox status, making sure there is a status.
445 #
446 oStatusLogic = TestBoxStatusLogic(oDb);
447 oStatusData = oStatusLogic.tryFetchStatus(oTestBox.idTestBox);
448 if oStatusData is not None:
449 self._cleanupOldTest(oDb, oStatusData);
450 else:
451 oStatusLogic.insertIdleStatus(oTestBox.idTestBox, oTestBox.idGenTestBox, fCommit = True);
452
453 #
454 # ACK the request.
455 #
456 dResponse = \
457 {
458 constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.STATUS_ACK,
459 constants.tbresp.SIGNON_PARAM_ID: oTestBox.idTestBox,
460 constants.tbresp.SIGNON_PARAM_NAME: oTestBox.sName,
461 }
462 return self._writeResponse(dResponse);
463
464 def _doGangCleanup(self, oDb, oStatusData):
465 """
466 _doRequestCommand worker for handling a box in gang-cleanup.
467 This will check if all testboxes has completed their run, pretending to
468 be busy until that happens. Once all are completed, resources will be
469 freed and the testbox returns to idle state (we update oStatusData).
470 """
471 oStatusLogic = TestBoxStatusLogic(oDb)
472 oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
473 if oStatusLogic.isWholeGangDoneTesting(oTestSet.idTestSetGangLeader):
474 oDb.begin();
475
476 GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False);
477 TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
478
479 oStatusData.tsUpdated = oDb.getCurrentTimestamp();
480 oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle;
481
482 oDb.commit();
483 return None;
484
485 def _doGangGatheringTimedOut(self, oDb, oStatusData):
486 """
487 _doRequestCommand worker for handling a box in gang-gathering-timed-out state.
488 This will do clean-ups similar to _cleanupOldTest and update the state likewise.
489 """
490 oDb.begin();
491
492 TestSetLogic(oDb).completeAsGangGatheringTimeout(oStatusData.idTestSet, fCommit = False);
493 GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False);
494 TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
495
496 oStatusData.tsUpdated = oDb.getCurrentTimestamp();
497 oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle;
498
499 oDb.commit();
500 return None;
501
502 def _doGangGathering(self, oDb, oStatusData):
503 """
504 _doRequestCommand worker for handling a box in gang-gathering state.
505 This only checks for timeout. It will update the oStatusData if a
506 timeout is detected, so that the box will be idle upon return.
507 """
508 oStatusLogic = TestBoxStatusLogic(oDb);
509 if oStatusLogic.timeSinceLastChangeInSecs(oStatusData) > config.g_kcSecGangGathering \
510 and SchedulerBase.tryCancelGangGathering(oDb, oStatusData): # <-- Updates oStatusData.
511 self._doGangGatheringTimedOut(oDb, oStatusData);
512 return None;
513
514 def _doRequestCommand(self, fIdle):
515 """
516 Common code for handling command request.
517 """
518
519 (oDb, oStatusData, oTestBoxData) = self._connectToDbAndValidateTb();
520 if oDb is None:
521 return False;
522
523 #
524 # Status clean up.
525 #
526 # Only when BUSY will the TestBox Script request and execute commands
527 # concurrently. So, it must be idle when sending REQUEST_COMMAND_IDLE.
528 #
529 if fIdle:
530 if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGathering:
531 self._doGangGathering(oDb, oStatusData);
532 elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut:
533 self._doGangGatheringTimedOut(oDb, oStatusData);
534 elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangTesting:
535 dResponse = SchedulerBase.composeExecResponse(oDb, oTestBoxData.idTestBox, self._oSrvGlue.getBaseUrl());
536 if dResponse is not None:
537 return dResponse;
538 elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangCleanup:
539 self._doGangCleanup(oDb, oStatusData);
540 elif oStatusData.enmState != TestBoxStatusData.ksTestBoxState_Idle: # (includes ksTestBoxState_GangGatheringTimedOut)
541 self._cleanupOldTest(oDb, oStatusData);
542
543 #
544 # Check for pending command.
545 #
546 if oTestBoxData.enmPendingCmd != TestBoxData.ksTestBoxCmd_None:
547 asValidCmds = TestBoxController.kasIdleCmds if fIdle else TestBoxController.kasBusyCmds;
548 if oTestBoxData.enmPendingCmd in asValidCmds:
549 dResponse = { constants.tbresp.ALL_PARAM_RESULT: TestBoxController.kdCmdToTbRespCmd[oTestBoxData.enmPendingCmd] };
550 if oTestBoxData.enmPendingCmd in [TestBoxData.ksTestBoxCmd_Upgrade, TestBoxData.ksTestBoxCmd_UpgradeAndReboot]:
551 dResponse[constants.tbresp.UPGRADE_PARAM_URL] = self._oSrvGlue.getBaseUrl() + TestBoxController.ksUpgradeZip;
552 return self._writeResponse(dResponse);
553
554 if oTestBoxData.enmPendingCmd == TestBoxData.ksTestBoxCmd_Abort and fIdle:
555 TestBoxLogic(oDb).setCommand(self._idTestBox, sOldCommand = oTestBoxData.enmPendingCmd,
556 sNewCommand = TestBoxData.ksTestBoxCmd_None, fCommit = True);
557
558 #
559 # If doing gang stuff, return 'CMD_WAIT'.
560 #
561 ## @todo r=bird: Why is GangTesting included here? Figure out when testing gang testing.
562 if oStatusData.enmState in [TestBoxStatusData.ksTestBoxState_GangGathering,
563 TestBoxStatusData.ksTestBoxState_GangTesting,
564 TestBoxStatusData.ksTestBoxState_GangCleanup]:
565 return self._resultResponse(constants.tbresp.CMD_WAIT);
566
567 #
568 # If idling and enabled try schedule a new task.
569 #
570 if fIdle \
571 and oTestBoxData.fEnabled \
572 and not TestSetLogic(oDb).isTestBoxExecutingToRapidly(oTestBoxData.idTestBox) \
573 and oStatusData.enmState == TestBoxStatusData.ksTestBoxState_Idle: # (paranoia)
574 dResponse = SchedulerBase.scheduleNewTask(oDb, oTestBoxData, oStatusData.iWorkItem, self._oSrvGlue.getBaseUrl());
575 if dResponse is not None:
576 return self._writeResponse(dResponse);
577
578 #
579 # Touch the status row every couple of mins so we can tell that the box is alive.
580 #
581 oStatusLogic = TestBoxStatusLogic(oDb);
582 if oStatusData.enmState != TestBoxStatusData.ksTestBoxState_GangGathering \
583 and oStatusLogic.timeSinceLastChangeInSecs(oStatusData) >= TestBoxStatusLogic.kcSecIdleTouchStatus:
584 oStatusLogic.touchStatus(oTestBoxData.idTestBox, fCommit = True);
585
586 return self._idleResponse();
587
588 def _actionRequestCommandBusy(self):
589 """ Implement request for command. """
590 self._checkForUnknownParameters();
591 return self._doRequestCommand(False);
592
593 def _actionRequestCommandIdle(self):
594 """ Implement request for command. """
595 self._checkForUnknownParameters();
596 return self._doRequestCommand(True);
597
598 def _doCommandAckNck(self, sCmd):
599 """ Implements ACK, NACK and NACK(ENOTSUP). """
600
601 (oDb, _, _) = self._connectToDbAndValidateTb();
602 if oDb is None:
603 return False;
604
605 #
606 # If the command maps to a TestBoxCmd_T value, it means we have to
607 # check and update TestBoxes. If it's an ACK, the testbox status will
608 # need updating as well.
609 #
610 sPendingCmd = TestBoxController.kdTbRespCmdToCmd[sCmd];
611 if sPendingCmd is not None:
612 oTestBoxLogic = TestBoxLogic(oDb)
613 oTestBoxLogic.setCommand(self._idTestBox, sOldCommand = sPendingCmd,
614 sNewCommand = TestBoxData.ksTestBoxCmd_None, fCommit = False);
615
616 if self._sAction == constants.tbreq.COMMAND_ACK \
617 and TestBoxController.kdCmdToState[sPendingCmd] is not None:
618 oStatusLogic = TestBoxStatusLogic(oDb);
619 oStatusLogic.updateState(self._idTestBox, TestBoxController.kdCmdToState[sPendingCmd], fCommit = False);
620
621 # Commit the two updates.
622 oDb.commit();
623
624 #
625 # Log NACKs.
626 #
627 if self._sAction != constants.tbreq.COMMAND_ACK:
628 oSysLogLogic = SystemLogLogic(oDb);
629 oSysLogLogic.addEntry(SystemLogData.ksEvent_CmdNacked,
630 'idTestBox=%s sCmd=%s' % (self._idTestBox, sPendingCmd),
631 24, fCommit = True);
632
633 return self._resultResponse(constants.tbresp.STATUS_ACK);
634
635 def _actionCommandAck(self):
636 """ Implement command ACK'ing """
637 sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasAckableCmds);
638 self._checkForUnknownParameters();
639 return self._doCommandAckNck(sCmd);
640
641 def _actionCommandNack(self):
642 """ Implement command NACK'ing """
643 sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasNackableCmds);
644 self._checkForUnknownParameters();
645 return self._doCommandAckNck(sCmd);
646
647 def _actionCommandNotSup(self):
648 """ Implement command NACK(ENOTSUP)'ing """
649 sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasNackableCmds);
650 self._checkForUnknownParameters();
651 return self._doCommandAckNck(sCmd);
652
653 def _actionLogMain(self):
654 """ Implement submitting log entries to the main log file. """
655 #
656 # Parameter validation.
657 #
658 sBody = self._getStringParam(constants.tbreq.LOG_PARAM_BODY, fStrip = False);
659 if not sBody:
660 return self._resultResponse(constants.tbresp.STATUS_NACK);
661 self._checkForUnknownParameters();
662
663 (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
664 TestBoxStatusData.ksTestBoxState_GangTesting]);
665 if oStatusData is None:
666 return False;
667
668 #
669 # Write the text to the log file.
670 #
671 oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
672 self.writeToMainLog(oTestSet, sBody);
673 ## @todo Overflow is a hanging offence, need to note it and fail whatever is going on...
674
675 # Done.
676 return self._resultResponse(constants.tbresp.STATUS_ACK);
677
678 def _actionUpload(self):
679 """ Implement uploading of files. """
680 #
681 # Parameter validation.
682 #
683 sName = self._getStringParam(constants.tbreq.UPLOAD_PARAM_NAME);
684 sMime = self._getStringParam(constants.tbreq.UPLOAD_PARAM_MIME);
685 sKind = self._getStringParam(constants.tbreq.UPLOAD_PARAM_KIND);
686 sDesc = self._getStringParam(constants.tbreq.UPLOAD_PARAM_DESC);
687 self._checkForUnknownParameters();
688
689 (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
690 TestBoxStatusData.ksTestBoxState_GangTesting]);
691 if oStatusData is None:
692 return False;
693
694 if len(sName) > 128 or len(sName) < 3:
695 raise TestBoxControllerException('Invalid file name "%s"' % (sName,));
696 if re.match(r'^[a-zA-Z0-9_\-(){}#@+,.=]*$', sName) is None:
697 raise TestBoxControllerException('Invalid file name "%s"' % (sName,));
698
699 if sMime not in [ 'text/plain', #'text/html', 'text/xml',
700 'application/octet-stream',
701 'image/png', #'image/gif', 'image/jpeg',
702 #'video/webm', 'video/mpeg', 'video/mpeg4-generic',
703 ]:
704 raise TestBoxControllerException('Invalid MIME type "%s"' % (sMime,));
705
706 if sKind not in TestResultFileData.kasKinds:
707 raise TestBoxControllerException('Invalid kind "%s"' % (sKind,));
708
709 if len(sDesc) > 256:
710 raise TestBoxControllerException('Invalid description "%s"' % (sDesc,));
711 if not set(sDesc).issubset(set(string.printable)):
712 raise TestBoxControllerException('Invalid description "%s"' % (sDesc,));
713
714 if ('application/octet-stream', {}) != self._oSrvGlue.getContentType():
715 raise TestBoxControllerException('Unexpected content type: %s; %s' % self._oSrvGlue.getContentType());
716
717 cbFile = self._oSrvGlue.getContentLength();
718 if cbFile <= 0:
719 raise TestBoxControllerException('File "%s" is empty or negative in size (%s)' % (sName, cbFile));
720 if (cbFile + 1048575) / 1048576 > config.g_kcMbMaxUploadSingle:
721 raise TestBoxControllerException('File "%s" is too big %u bytes (max %u MiB)'
722 % (sName, cbFile, config.g_kcMbMaxUploadSingle,));
723
724 #
725 # Write the text to the log file.
726 #
727 oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
728 oDstFile = TestSetLogic(oDb).createFile(oTestSet, sName = sName, sMime = sMime, sKind = sKind, sDesc = sDesc,
729 cbFile = cbFile, fCommit = True);
730
731 offFile = 0;
732 oSrcFile = self._oSrvGlue.getBodyIoStream();
733 while offFile < cbFile:
734 cbToRead = cbFile - offFile;
735 if cbToRead > 256*1024:
736 cbToRead = 256*1024;
737 offFile += cbToRead;
738
739 abBuf = oSrcFile.read(cbToRead);
740 oDstFile.write(abBuf); # pylint: disable=maybe-no-member
741 del abBuf;
742
743 oDstFile.close(); # pylint: disable=maybe-no-member
744
745 # Done.
746 return self._resultResponse(constants.tbresp.STATUS_ACK);
747
748 def _actionXmlResults(self):
749 """ Implement submitting "XML" like test result stream. """
750 #
751 # Parameter validation.
752 #
753 sXml = self._getStringParam(constants.tbreq.XML_RESULT_PARAM_BODY, fStrip = False);
754 self._checkForUnknownParameters();
755 if not sXml: # Used for link check by vboxinstaller.py on Windows.
756 return self._resultResponse(constants.tbresp.STATUS_ACK);
757
758 (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
759 TestBoxStatusData.ksTestBoxState_GangTesting]);
760 if oStatusData is None:
761 return False;
762
763 #
764 # Process the XML.
765 #
766 (sError, fUnforgivable) = TestResultLogic(oDb).processXmlStream(sXml, self._idTestSet);
767 if sError is not None:
768 oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet);
769 self.writeToMainLog(oTestSet, '\n!!XML error: %s\n%s\n\n' % (sError, sXml,));
770 if fUnforgivable:
771 return self._resultResponse(constants.tbresp.STATUS_NACK);
772 return self._resultResponse(constants.tbresp.STATUS_ACK);
773
774
775 def _actionExecCompleted(self):
776 """
777 Implement EXEC completion.
778
779 Because the action is request by the worker thread of the testbox
780 script we cannot pass pending commands back to it like originally
781 planned. So, we just complete the test set and update the status.
782 """
783 #
784 # Parameter validation.
785 #
786 sStatus = self._getStringParam(constants.tbreq.EXEC_COMPLETED_PARAM_RESULT, TestBoxController.kasValidResults);
787 self._checkForUnknownParameters();
788
789 (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing,
790 TestBoxStatusData.ksTestBoxState_GangTesting]);
791 if oStatusData is None:
792 return False;
793
794 #
795 # Complete the status.
796 #
797 oDb.rollback();
798 oDb.begin();
799 oTestSetLogic = TestSetLogic(oDb);
800 idTestSetGangLeader = oTestSetLogic.complete(oStatusData.idTestSet, self.kadTbResultToStatus[sStatus], fCommit = False);
801
802 oStatusLogic = TestBoxStatusLogic(oDb);
803 if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_Testing:
804 assert idTestSetGangLeader is None;
805 GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox);
806 oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
807 else:
808 assert idTestSetGangLeader is not None;
809 oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_GangCleanup, oStatusData.idTestSet,
810 fCommit = False);
811 if oStatusLogic.isWholeGangDoneTesting(idTestSetGangLeader):
812 GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox);
813 oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False);
814
815 oDb.commit();
816 return self._resultResponse(constants.tbresp.STATUS_ACK);
817
818
819
820 def _getStandardParams(self, dParams):
821 """
822 Gets the standard parameters and validates them.
823
824 The parameters are returned as a tuple: sAction, idTestBox, sTestBoxUuid.
825 Note! the sTextBoxId can be None if it's a SIGNON request.
826
827 Raises TestBoxControllerException on invalid input.
828 """
829 #
830 # Get the action parameter and validate it.
831 #
832 if constants.tbreq.ALL_PARAM_ACTION not in dParams:
833 raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
834 % (constants.tbreq.ALL_PARAM_ACTION, dParams,));
835 sAction = dParams[constants.tbreq.ALL_PARAM_ACTION];
836
837 if sAction not in self._dActions:
838 raise TestBoxControllerException('Unknown action "%s" in request (params: %s; action: %s)' \
839 % (sAction, dParams, self._dActions));
840
841 #
842 # TestBox UUID.
843 #
844 if constants.tbreq.ALL_PARAM_TESTBOX_UUID not in dParams:
845 raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
846 % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, dParams,));
847 sTestBoxUuid = dParams[constants.tbreq.ALL_PARAM_TESTBOX_UUID];
848 try:
849 sTestBoxUuid = str(uuid.UUID(sTestBoxUuid));
850 except Exception as oXcpt:
851 raise TestBoxControllerException('Invalid %s parameter value "%s": %s ' \
852 % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, sTestBoxUuid, oXcpt));
853 if sTestBoxUuid == '00000000-0000-0000-0000-000000000000':
854 raise TestBoxControllerException('Invalid %s parameter value "%s": NULL UUID not allowed.' \
855 % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, sTestBoxUuid));
856
857 #
858 # TestBox ID.
859 #
860 if constants.tbreq.ALL_PARAM_TESTBOX_ID in dParams:
861 sTestBoxId = dParams[constants.tbreq.ALL_PARAM_TESTBOX_ID];
862 try:
863 idTestBox = int(sTestBoxId);
864 if idTestBox <= 0 or idTestBox >= 0x7fffffff:
865 raise Exception;
866 except:
867 raise TestBoxControllerException('Bad value for "%s": "%s"' \
868 % (constants.tbreq.ALL_PARAM_TESTBOX_ID, sTestBoxId));
869 elif sAction == constants.tbreq.SIGNON:
870 idTestBox = None;
871 else:
872 raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
873 % (constants.tbreq.ALL_PARAM_TESTBOX_ID, dParams,));
874
875 #
876 # Test Set ID.
877 #
878 if constants.tbreq.RESULT_PARAM_TEST_SET_ID in dParams:
879 sTestSetId = dParams[constants.tbreq.RESULT_PARAM_TEST_SET_ID];
880 try:
881 idTestSet = int(sTestSetId);
882 if idTestSet <= 0 or idTestSet >= 0x7fffffff:
883 raise Exception;
884 except:
885 raise TestBoxControllerException('Bad value for "%s": "%s"' \
886 % (constants.tbreq.RESULT_PARAM_TEST_SET_ID, sTestSetId));
887 elif sAction not in [ constants.tbreq.XML_RESULTS, ]: ## More later.
888 idTestSet = None;
889 else:
890 raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \
891 % (constants.tbreq.RESULT_PARAM_TEST_SET_ID, dParams,));
892
893 #
894 # The testbox address.
895 #
896 sTestBoxAddr = self._oSrvGlue.getClientAddr();
897 if sTestBoxAddr is None or sTestBoxAddr.strip() == '':
898 raise TestBoxControllerException('Invalid client address "%s"' % (sTestBoxAddr,));
899
900 #
901 # Update the list of checked parameters.
902 #
903 self._asCheckedParams.extend([constants.tbreq.ALL_PARAM_TESTBOX_UUID, constants.tbreq.ALL_PARAM_ACTION]);
904 if idTestBox is not None:
905 self._asCheckedParams.append(constants.tbreq.ALL_PARAM_TESTBOX_ID);
906 if idTestSet is not None:
907 self._asCheckedParams.append(constants.tbreq.RESULT_PARAM_TEST_SET_ID);
908
909 return (sAction, idTestBox, sTestBoxUuid, sTestBoxAddr, idTestSet);
910
911 def dispatchRequest(self):
912 """
913 Dispatches the incoming request.
914
915 Will raise TestBoxControllerException on failure.
916 """
917
918 #
919 # Must be a POST request.
920 #
921 try:
922 sMethod = self._oSrvGlue.getMethod();
923 except Exception as oXcpt:
924 raise TestBoxControllerException('Error retriving request method: %s' % (oXcpt,));
925 if sMethod != 'POST':
926 raise TestBoxControllerException('Error expected POST request not "%s"' % (sMethod,));
927
928 #
929 # Get the parameters and checks for duplicates.
930 #
931 try:
932 dParams = self._oSrvGlue.getParameters();
933 except Exception as oXcpt:
934 raise TestBoxControllerException('Error retriving parameters: %s' % (oXcpt,));
935 for sKey in dParams.keys():
936 if len(dParams[sKey]) > 1:
937 raise TestBoxControllerException('Parameter "%s" is given multiple times: %s' % (sKey, dParams[sKey]));
938 dParams[sKey] = dParams[sKey][0];
939 self._dParams = dParams;
940
941 #
942 # Get+validate the standard action parameters and dispatch the request.
943 #
944 (self._sAction, self._idTestBox, self._sTestBoxUuid, self._sTestBoxAddr, self._idTestSet) = \
945 self._getStandardParams(dParams);
946 return self._dActions[self._sAction]();
947
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