VirtualBox

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

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

Copyright year updates by scm.

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