VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/cgi/status.py@ 98537

Last change on this file since 98537 was 98537, checked in by vboxsync, 2 years ago

testmanager/status.py: The enmStatus in the select result is either None or a valid TestStatus_T value, so a simple check for None suffices in testbox_data_processing. In dict_update it isn't needed to check if enmStatus is None anymore. Since dictionaries are passed by reference, dict_update doesn't need to return the target dictionary. Misc style cleanups. bugref:10364

  • Property svn:eol-style set to LF
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
File size: 18.9 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# $Id: status.py 98537 2023-02-10 18:01:38Z vboxsync $
4
5"""
6CGI - Administrator Web-UI.
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2012-2023 Oracle and/or its affiliates.
12
13This file is part of VirtualBox base platform packages, as
14available from https://www.virtualbox.org.
15
16This program is free software; you can redistribute it and/or
17modify it under the terms of the GNU General Public License
18as published by the Free Software Foundation, in version 3 of the
19License.
20
21This program is distributed in the hope that it will be useful, but
22WITHOUT ANY WARRANTY; without even the implied warranty of
23MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
24General Public License for more details.
25
26You should have received a copy of the GNU General Public License
27along with this program; if not, see <https://www.gnu.org/licenses>.
28
29The contents of this file may alternatively be used under the terms
30of the Common Development and Distribution License Version 1.0
31(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
32in the VirtualBox distribution, in which case the provisions of the
33CDDL are applicable instead of those of the GPL.
34
35You may elect to license modified versions of this file under the
36terms and conditions of either the GPL or the CDDL or both.
37
38SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
39"""
40__version__ = "$Revision: 98537 $"
41
42
43# Standard python imports.
44import os
45import sys
46
47# Only the main script needs to modify the path.
48g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
49sys.path.append(g_ksValidationKitDir);
50
51# Validation Kit imports.
52from testmanager import config;
53from testmanager.core.webservergluecgi import WebServerGlueCgi;
54
55from common import constants;
56from testmanager.core.base import TMExceptionBase;
57from testmanager.core.db import TMDatabaseConnection;
58
59
60
61def timeDeltaToHours(oTimeDelta):
62 return oTimeDelta.days * 24 + oTimeDelta.seconds // 3600
63
64
65def testbox_data_processing(oDb):
66 dTestBoxes = {}
67 while True:
68 # Fetch the next row and unpack it.
69 aoRow = oDb.fetchOne();
70 if aoRow is None:
71 break;
72 sTextBoxName = aoRow[0];
73 enmStatus = aoRow[1];
74 oTimeDeltaSinceStarted = aoRow[2];
75 sTestBoxOs = aoRow[3]
76 sSchedGroupNames = aoRow[4];
77
78 # Idle testboxes will not have an assigned test set, so enmStatus
79 # will be None. Skip these.
80 if enmStatus:
81 dict_update(dTestBoxes, sTextBoxName, enmStatus)
82
83 if "testbox_os" not in dTestBoxes[sTextBoxName]:
84 dTestBoxes[sTextBoxName].update({"testbox_os": sTestBoxOs})
85
86 if "sched_group" not in dTestBoxes[sTextBoxName]:
87 dTestBoxes[sTextBoxName].update({"sched_group": sSchedGroupNames})
88 elif sSchedGroupNames not in dTestBoxes[sTextBoxName]["sched_group"]:
89 dTestBoxes[sTextBoxName]["sched_group"] += "," + sSchedGroupNames
90
91 if enmStatus == "running":
92 dTestBoxes[sTextBoxName].update({"hours_running": timeDeltaToHours(oTimeDeltaSinceStarted)})
93
94 return dTestBoxes;
95
96
97def os_results_separating(dResult, sTestName, sTestBoxOs, enmStatus):
98 if sTestBoxOs == "linux":
99 dict_update(dResult, sTestName + " / linux", enmStatus)
100 elif sTestBoxOs == "win":
101 dict_update(dResult, sTestName + " / windows", enmStatus)
102 elif sTestBoxOs == "darwin":
103 dict_update(dResult, sTestName + " / darwin", enmStatus)
104 elif sTestBoxOs == "solaris":
105 dict_update(dResult, sTestName + " / solaris", enmStatus)
106 else:
107 dict_update(dResult, sTestName + " / other", enmStatus)
108
109
110## Template dictionary for new dTarget[] entries in dict_update.
111# This _MUST_ include all values of the TestStatus_T SQL enum type.
112g_kdTestStatuses = {
113 'running': 0,
114 'success': 0,
115 'skipped': 0,
116 'bad-testbox': 0,
117 'aborted': 0,
118 'failure': 0,
119 'timed-out': 0,
120 'rebooted': 0,
121}
122
123def dict_update(dTarget, sKeyName, enmStatus):
124 if sKeyName not in dTarget:
125 dTarget.update({sKeyName: g_kdTestStatuses.copy()})
126 dTarget[sKeyName][enmStatus] += 1
127
128
129def formatDataEntry(sKey, dEntry):
130 # There are variations in the first and second "columns".
131 if "hours_running" in dEntry:
132 sRet = "%s;%s;%s | running: %s;%s" \
133 % (sKey, dEntry["testbox_os"], dEntry["sched_group"], dEntry["running"], dEntry["hours_running"]);
134 else:
135 if "testbox_os" in dEntry:
136 sRet = "%s;%s;%s" % (sKey, dEntry["testbox_os"], dEntry["sched_group"],);
137 else:
138 sRet = sKey;
139 sRet += " | running: %s" % (dEntry["running"],)
140
141 # The rest is currently identical:
142 sRet += " | success: %s | skipped: %s | bad-testbox: %s | aborted: %s | failure: %s | timed-out: %s | rebooted: %s | \n" \
143 % (dEntry["success"], dEntry["skipped"], dEntry["bad-testbox"], dEntry["aborted"],
144 dEntry["failure"], dEntry["timed-out"], dEntry["rebooted"],);
145 return sRet;
146
147
148def format_data(dData, fSorted):
149 sRet = "";
150 if not fSorted:
151 for sKey in dData:
152 sRet += formatDataEntry(sKey, dData[sKey]);
153 else:
154 for sKey in sorted(dData.keys()):
155 sRet += formatDataEntry(sKey, dData[sKey]);
156 return sRet;
157
158######
159
160class StatusDispatcherException(TMExceptionBase):
161 """
162 Exception class for TestBoxController.
163 """
164 pass; # pylint: disable=unnecessary-pass
165
166
167class StatusDispatcher(object): # pylint: disable=too-few-public-methods
168 """
169 Status dispatcher class.
170 """
171
172
173 def __init__(self, oSrvGlue):
174 """
175 Won't raise exceptions.
176 """
177 self._oSrvGlue = oSrvGlue;
178 self._sAction = None; # _getStandardParams / dispatchRequest sets this later on.
179 self._dParams = None; # _getStandardParams / dispatchRequest sets this later on.
180 self._asCheckedParams = [];
181 self._dActions = \
182 {
183 'MagicMirrorTestResults': self._actionMagicMirrorTestResults,
184 'MagicMirrorTestBoxes': self._actionMagicMirrorTestBoxes,
185 };
186
187 def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None):
188 """
189 Gets a string parameter (stripped).
190
191 Raises exception if not found and no default is provided, or if the
192 value isn't found in asValidValues.
193 """
194 if sName not in self._dParams:
195 if sDefValue is None:
196 raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName));
197 return sDefValue;
198 sValue = self._dParams[sName];
199 if fStrip:
200 sValue = sValue.strip();
201
202 if sName not in self._asCheckedParams:
203 self._asCheckedParams.append(sName);
204
205 if asValidValues is not None and sValue not in asValidValues:
206 raise StatusDispatcherException('%s parameter %s value "%s" not in %s '
207 % (self._sAction, sName, sValue, asValidValues));
208 return sValue;
209
210 def _getIntParam(self, sName, iMin = None, iMax = None, iDefValue = None):
211 """
212 Gets a string parameter.
213 Raises exception if not found, not a valid integer, or if the value
214 isn't in the range defined by iMin and iMax.
215 """
216 if sName not in self._dParams:
217 if iDefValue is None:
218 raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName));
219 return iDefValue;
220 sValue = self._dParams[sName];
221 try:
222 iValue = int(sValue, 0);
223 except:
224 raise StatusDispatcherException('%s parameter %s value "%s" cannot be convert to an integer'
225 % (self._sAction, sName, sValue));
226 if sName not in self._asCheckedParams:
227 self._asCheckedParams.append(sName);
228
229 if (iMin is not None and iValue < iMin) \
230 or (iMax is not None and iValue > iMax):
231 raise StatusDispatcherException('%s parameter %s value %d is out of range [%s..%s]'
232 % (self._sAction, sName, iValue, iMin, iMax));
233 return iValue;
234
235 def _getBoolParam(self, sName, fDefValue = None):
236 """
237 Gets a boolean parameter.
238
239 Raises exception if not found and no default is provided, or if not a
240 valid boolean.
241 """
242 sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue));
243 return sValue in ('True', 'true', '1',);
244
245 def _checkForUnknownParameters(self):
246 """
247 Check if we've handled all parameters, raises exception if anything
248 unknown was found.
249 """
250
251 if len(self._asCheckedParams) != len(self._dParams):
252 sUnknownParams = '';
253 for sKey in self._dParams:
254 if sKey not in self._asCheckedParams:
255 sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey];
256 raise StatusDispatcherException('Unknown parameters: ' + sUnknownParams);
257
258 return True;
259
260 def _connectToDb(self):
261 """
262 Connects to the database.
263
264 Returns (TMDatabaseConnection, (more later perhaps) ) on success.
265 Returns (None, ) on failure after sending the box an appropriate response.
266 May raise exception on DB error.
267 """
268 return (TMDatabaseConnection(self._oSrvGlue.dprint),);
269
270 def _actionMagicMirrorTestBoxes(self):
271 """
272 Produces test result status for the magic mirror dashboard
273 """
274
275 #
276 # Parse arguments and connect to the database.
277 #
278 cHoursBack = self._getIntParam('cHours', 1, 24*14, 12);
279 fSorted = self._getBoolParam('fSorted', False);
280 self._checkForUnknownParameters();
281
282 #
283 # Get the data.
284 #
285 # - The first part of the select is about fetching all finished tests
286 # for last cHoursBack hours
287 #
288 # - The second part is fetching all tests which isn't done. (Both old
289 # (running more than cHoursBack) and fresh (less than cHoursBack) ones
290 # because we want to know if there's a hanging tests together with
291 # currently running).
292 #
293 # - There are also testsets without status at all, likely because disabled
294 # testboxes still have an assigned testsets.
295 #
296 # Note! We're not joining on TestBoxesWithStrings.idTestBox = TestSets.idGenTestBox
297 # here because of indexes. This is also more consistent with the
298 # rest of the query.
299 #
300 # Note! The original SQL is slow because of the 'OR TestSets.tsDone'
301 # part, using AND and UNION is significatly faster because
302 # it matches the TestSetsGraphBoxIdx (index).
303 #
304 (oDb,) = self._connectToDb();
305 if oDb is None:
306 return False;
307
308 oDb.execute('''
309( SELECT TestBoxesWithStrings.sName,
310 TestSets.enmStatus,
311 CURRENT_TIMESTAMP - TestSets.tsCreated,
312 TestBoxesWithStrings.sOS,
313 SchedGroupNames.sSchedGroupNames
314 FROM (
315 SELECT TestBoxesInSchedGroups.idTestBox AS idTestBox,
316 STRING_AGG(SchedGroups.sName, ',') AS sSchedGroupNames
317 FROM TestBoxesInSchedGroups
318 INNER JOIN SchedGroups
319 ON SchedGroups.idSchedGroup = TestBoxesInSchedGroups.idSchedGroup
320 WHERE TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP
321 AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP
322 GROUP BY TestBoxesInSchedGroups.idTestBox
323 ) AS SchedGroupNames,
324 TestBoxesWithStrings
325 LEFT OUTER JOIN TestSets
326 ON TestSets.idTestBox = TestBoxesWithStrings.idTestBox
327 AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval)
328 AND TestSets.tsDone IS NOT NULL
329 WHERE TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
330 AND SchedGroupNames.idTestBox = TestBoxesWithStrings.idTestBox
331) UNION (
332 SELECT TestBoxesWithStrings.sName,
333 TestSets.enmStatus,
334 CURRENT_TIMESTAMP - TestSets.tsCreated,
335 TestBoxesWithStrings.sOS,
336 SchedGroupNames.sSchedGroupNames
337 FROM (
338 SELECT TestBoxesInSchedGroups.idTestBox AS idTestBox,
339 STRING_AGG(SchedGroups.sName, ',') AS sSchedGroupNames
340 FROM TestBoxesInSchedGroups
341 INNER JOIN SchedGroups
342 ON SchedGroups.idSchedGroup = TestBoxesInSchedGroups.idSchedGroup
343 WHERE TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP
344 AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP
345 GROUP BY TestBoxesInSchedGroups.idTestBox
346 ) AS SchedGroupNames,
347 TestBoxesWithStrings
348 LEFT OUTER JOIN TestSets
349 ON TestSets.idTestBox = TestBoxesWithStrings.idTestBox
350 AND TestSets.tsDone IS NULL
351 WHERE TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
352 AND SchedGroupNames.idTestBox = TestBoxesWithStrings.idTestBox
353)
354''', (cHoursBack, ));
355
356
357 #
358 # Process, format and output data.
359 #
360 dResult = testbox_data_processing(oDb);
361 self._oSrvGlue.setContentType('text/plain');
362 self._oSrvGlue.write(format_data(dResult, fSorted));
363
364 return True;
365
366 def _actionMagicMirrorTestResults(self):
367 """
368 Produces test result status for the magic mirror dashboard
369 """
370
371 #
372 # Parse arguments and connect to the database.
373 #
374 sBranch = self._getStringParam('sBranch');
375 cHoursBack = self._getIntParam('cHours', 1, 24*14, 6); ## @todo why 6 hours here and 12 for test boxes?
376 fSorted = self._getBoolParam('fSorted', False);
377 self._checkForUnknownParameters();
378
379 #
380 # Get the data.
381 #
382 # Note! These queries should be joining TestBoxesWithStrings and TestSets
383 # on idGenTestBox rather than on idTestBox and tsExpire=inf, but
384 # we don't have any index matching those. So, we'll ignore tests
385 # performed by deleted testboxes for the present as that doesn't
386 # happen often and we want the ~1000x speedup.
387 #
388 (oDb,) = self._connectToDb();
389 if oDb is None:
390 return False;
391
392 if sBranch == 'all':
393 oDb.execute('''
394SELECT TestSets.enmStatus,
395 TestCases.sName,
396 TestBoxesWithStrings.sOS
397FROM TestSets
398INNER JOIN TestCases
399 ON TestCases.idGenTestCase = TestSets.idGenTestCase
400INNER JOIN TestBoxesWithStrings
401 ON TestBoxesWithStrings.idTestBox = TestSets.idTestBox
402 AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
403WHERE TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval)
404''', (cHoursBack,));
405 else:
406 oDb.execute('''
407SELECT TestSets.enmStatus,
408 TestCases.sName,
409 TestBoxesWithStrings.sOS
410FROM TestSets
411INNER JOIN BuildCategories
412 ON BuildCategories.idBuildCategory = TestSets.idBuildCategory
413 AND BuildCategories.sBranch = %s
414INNER JOIN TestCases
415 ON TestCases.idGenTestCase = TestSets.idGenTestCase
416INNER JOIN TestBoxesWithStrings
417 ON TestBoxesWithStrings.idTestBox = TestSets.idTestBox
418 AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
419WHERE TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval)
420''', (sBranch, cHoursBack,));
421
422 # Process the data
423 dResult = {};
424 while True:
425 aoRow = oDb.fetchOne();
426 if aoRow is None:
427 break;
428 os_results_separating(dResult, aoRow[1], aoRow[2], aoRow[0]) # save all test results
429
430 # Format and output it.
431 self._oSrvGlue.setContentType('text/plain');
432 self._oSrvGlue.write(format_data(dResult, fSorted));
433
434 return True;
435
436 def _getStandardParams(self, dParams):
437 """
438 Gets the standard parameters and validates them.
439
440 The parameters are returned as a tuple: sAction, (more later, maybe)
441 Note! the sTextBoxId can be None if it's a SIGNON request.
442
443 Raises StatusDispatcherException on invalid input.
444 """
445 #
446 # Get the action parameter and validate it.
447 #
448 if constants.tbreq.ALL_PARAM_ACTION not in dParams:
449 raise StatusDispatcherException('No "%s" parameter in request (params: %s)'
450 % (constants.tbreq.ALL_PARAM_ACTION, dParams,));
451 sAction = dParams[constants.tbreq.ALL_PARAM_ACTION];
452
453 if sAction not in self._dActions:
454 raise StatusDispatcherException('Unknown action "%s" in request (params: %s; action: %s)'
455 % (sAction, dParams, self._dActions));
456 #
457 # Update the list of checked parameters.
458 #
459 self._asCheckedParams.extend([constants.tbreq.ALL_PARAM_ACTION,]);
460
461 return (sAction,);
462
463 def dispatchRequest(self):
464 """
465 Dispatches the incoming request.
466
467 Will raise StatusDispatcherException on failure.
468 """
469
470 #
471 # Must be a GET request.
472 #
473 try:
474 sMethod = self._oSrvGlue.getMethod();
475 except Exception as oXcpt:
476 raise StatusDispatcherException('Error retriving request method: %s' % (oXcpt,));
477 if sMethod != 'GET':
478 raise StatusDispatcherException('Error expected POST request not "%s"' % (sMethod,));
479
480 #
481 # Get the parameters and checks for duplicates.
482 #
483 try:
484 dParams = self._oSrvGlue.getParameters();
485 except Exception as oXcpt:
486 raise StatusDispatcherException('Error retriving parameters: %s' % (oXcpt,));
487 for sKey in dParams.keys():
488 if len(dParams[sKey]) > 1:
489 raise StatusDispatcherException('Parameter "%s" is given multiple times: %s' % (sKey, dParams[sKey]));
490 dParams[sKey] = dParams[sKey][0];
491 self._dParams = dParams;
492
493 #
494 # Get+validate the standard action parameters and dispatch the request.
495 #
496 (self._sAction, ) = self._getStandardParams(dParams);
497 return self._dActions[self._sAction]();
498
499
500def main():
501 """
502 Main function a la C/C++. Returns exit code.
503 """
504
505 oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False);
506 try:
507 oDisp = StatusDispatcher(oSrvGlue);
508 oDisp.dispatchRequest();
509 oSrvGlue.flush();
510 except Exception as oXcpt:
511 return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info());
512
513 return 0;
514
515if __name__ == '__main__':
516 if config.g_kfProfileAdmin:
517 from testmanager.debug import cgiprofiling;
518 sys.exit(cgiprofiling.profileIt(main));
519 else:
520 sys.exit(main());
521
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