VirtualBox

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

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

Copyright year updates by scm.

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