VirtualBox

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

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

testmanager/status.py: Dropping unused cHoursBack argument to the select as its %s was dropped by r154325. 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.8 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# $Id: status.py 98536 2023-02-10 17:39:28Z 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.215389.xyz.
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: 98536 $"
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 testboxes_dict = {}
67 while True:
68 line = oDb.fetchOne();
69 if line is None:
70 break;
71 testbox_name = line[0]
72 test_result = line[1]
73 oTimeDeltaSinceStarted = line[2]
74 test_box_os = line[3]
75 test_sched_group = line[4]
76
77 # idle testboxes might have an assigned testsets, skipping them
78 if test_result not in g_kdTestStatuses:
79 continue
80
81 testboxes_dict = dict_update(testboxes_dict, testbox_name, test_result)
82
83 if "testbox_os" not in testboxes_dict[testbox_name]:
84 testboxes_dict[testbox_name].update({"testbox_os": test_box_os})
85
86 if "sched_group" not in testboxes_dict[testbox_name]:
87 testboxes_dict[testbox_name].update({"sched_group": test_sched_group})
88 elif test_sched_group not in testboxes_dict[testbox_name]["sched_group"]:
89 testboxes_dict[testbox_name]["sched_group"] += "," + test_sched_group
90
91 if test_result == "running":
92 testboxes_dict[testbox_name].update({"hours_running": timeDeltaToHours(oTimeDeltaSinceStarted)})
93
94 return testboxes_dict;
95
96
97def os_results_separating(vb_dict, test_name, testbox_os, test_result):
98 if testbox_os == "linux":
99 dict_update(vb_dict, test_name + " / linux", test_result)
100 elif testbox_os == "win":
101 dict_update(vb_dict, test_name + " / windows", test_result)
102 elif testbox_os == "darwin":
103 dict_update(vb_dict, test_name + " / darwin", test_result)
104 elif testbox_os == "solaris":
105 dict_update(vb_dict, test_name + " / solaris", test_result)
106 else:
107 dict_update(vb_dict, test_name + " / other", test_result)
108
109
110# const/immutable.
111g_kdTestStatuses = {
112 'running': 0,
113 'success': 0,
114 'skipped': 0,
115 'bad-testbox': 0,
116 'aborted': 0,
117 'failure': 0,
118 'timed-out': 0,
119 'rebooted': 0,
120}
121
122def dict_update(target_dict, key_name, test_result):
123 if key_name not in target_dict:
124 target_dict.update({key_name: g_kdTestStatuses.copy()})
125 if test_result in g_kdTestStatuses:
126 target_dict[key_name][test_result] += 1
127 return target_dict
128
129
130def formatDataEntry(sKey, dEntry):
131 # There are variations in the first and second "columns".
132 if "hours_running" in dEntry:
133 sRet = "%s;%s;%s | running: %s;%s" \
134 % (sKey, dEntry["testbox_os"], dEntry["sched_group"], dEntry["running"], dEntry["hours_running"]);
135 else:
136 if "testbox_os" in dEntry:
137 sRet = "%s;%s;%s" % (sKey, dEntry["testbox_os"], dEntry["sched_group"],);
138 else:
139 sRet = sKey;
140 sRet += " | running: %s" % (dEntry["running"],)
141
142 # The rest is currently identical:
143 sRet += " | success: %s | skipped: %s | bad-testbox: %s | aborted: %s | failure: %s | timed-out: %s | rebooted: %s | \n" \
144 % (dEntry["success"], dEntry["skipped"], dEntry["bad-testbox"], dEntry["aborted"],
145 dEntry["failure"], dEntry["timed-out"], dEntry["rebooted"],);
146 return sRet;
147
148
149def format_data(dData, fSorted):
150 sRet = "";
151 if not fSorted:
152 for sKey in dData:
153 sRet += formatDataEntry(sKey, dData[sKey]);
154 else:
155 for sKey in sorted(dData.keys()):
156 sRet += formatDataEntry(sKey, dData[sKey]);
157 return sRet;
158
159######
160
161class StatusDispatcherException(TMExceptionBase):
162 """
163 Exception class for TestBoxController.
164 """
165 pass; # pylint: disable=unnecessary-pass
166
167
168class StatusDispatcher(object): # pylint: disable=too-few-public-methods
169 """
170 Status dispatcher class.
171 """
172
173
174 def __init__(self, oSrvGlue):
175 """
176 Won't raise exceptions.
177 """
178 self._oSrvGlue = oSrvGlue;
179 self._sAction = None; # _getStandardParams / dispatchRequest sets this later on.
180 self._dParams = None; # _getStandardParams / dispatchRequest sets this later on.
181 self._asCheckedParams = [];
182 self._dActions = \
183 {
184 'MagicMirrorTestResults': self._actionMagicMirrorTestResults,
185 'MagicMirrorTestBoxes': self._actionMagicMirrorTestBoxes,
186 };
187
188 def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None):
189 """
190 Gets a string parameter (stripped).
191
192 Raises exception if not found and no default is provided, or if the
193 value isn't found in asValidValues.
194 """
195 if sName not in self._dParams:
196 if sDefValue is None:
197 raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName));
198 return sDefValue;
199 sValue = self._dParams[sName];
200 if fStrip:
201 sValue = sValue.strip();
202
203 if sName not in self._asCheckedParams:
204 self._asCheckedParams.append(sName);
205
206 if asValidValues is not None and sValue not in asValidValues:
207 raise StatusDispatcherException('%s parameter %s value "%s" not in %s '
208 % (self._sAction, sName, sValue, asValidValues));
209 return sValue;
210
211 def _getIntParam(self, sName, iMin = None, iMax = None, iDefValue = None):
212 """
213 Gets a string parameter.
214 Raises exception if not found, not a valid integer, or if the value
215 isn't in the range defined by iMin and iMax.
216 """
217 if sName not in self._dParams:
218 if iDefValue is None:
219 raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName));
220 return iDefValue;
221 sValue = self._dParams[sName];
222 try:
223 iValue = int(sValue, 0);
224 except:
225 raise StatusDispatcherException('%s parameter %s value "%s" cannot be convert to an integer'
226 % (self._sAction, sName, sValue));
227 if sName not in self._asCheckedParams:
228 self._asCheckedParams.append(sName);
229
230 if (iMin is not None and iValue < iMin) \
231 or (iMax is not None and iValue > iMax):
232 raise StatusDispatcherException('%s parameter %s value %d is out of range [%s..%s]'
233 % (self._sAction, sName, iValue, iMin, iMax));
234 return iValue;
235
236 def _getBoolParam(self, sName, fDefValue = None):
237 """
238 Gets a boolean parameter.
239
240 Raises exception if not found and no default is provided, or if not a
241 valid boolean.
242 """
243 sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue));
244 return sValue in ('True', 'true', '1',);
245
246 def _checkForUnknownParameters(self):
247 """
248 Check if we've handled all parameters, raises exception if anything
249 unknown was found.
250 """
251
252 if len(self._asCheckedParams) != len(self._dParams):
253 sUnknownParams = '';
254 for sKey in self._dParams:
255 if sKey not in self._asCheckedParams:
256 sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey];
257 raise StatusDispatcherException('Unknown parameters: ' + sUnknownParams);
258
259 return True;
260
261 def _connectToDb(self):
262 """
263 Connects to the database.
264
265 Returns (TMDatabaseConnection, (more later perhaps) ) on success.
266 Returns (None, ) on failure after sending the box an appropriate response.
267 May raise exception on DB error.
268 """
269 return (TMDatabaseConnection(self._oSrvGlue.dprint),);
270
271 def _actionMagicMirrorTestBoxes(self):
272 """
273 Produces test result status for the magic mirror dashboard
274 """
275
276 #
277 # Parse arguments and connect to the database.
278 #
279 cHoursBack = self._getIntParam('cHours', 1, 24*14, 12);
280 fSorted = self._getBoolParam('fSorted', False);
281 self._checkForUnknownParameters();
282
283 #
284 # Get the data.
285 #
286 # - The first part of the select is about fetching all finished tests
287 # for last cHoursBack hours
288 #
289 # - The second part is fetching all tests which isn't done. (Both old
290 # (running more than cHoursBack) and fresh (less than cHoursBack) ones
291 # because we want to know if there's a hanging tests together with
292 # currently running).
293 #
294 # - There are also testsets without status at all, likely because disabled
295 # testboxes still have an assigned testsets.
296 #
297 # Note! We're not joining on TestBoxesWithStrings.idTestBox = TestSets.idGenTestBox
298 # here because of indexes. This is also more consistent with the
299 # rest of the query.
300 #
301 # Note! The original SQL is slow because of the 'OR TestSets.tsDone'
302 # part, using AND and UNION is significatly faster because
303 # it matches the TestSetsGraphBoxIdx (index).
304 #
305 (oDb,) = self._connectToDb();
306 if oDb is None:
307 return False;
308
309 oDb.execute('''
310( SELECT TestBoxesWithStrings.sName,
311 TestSets.enmStatus,
312 CURRENT_TIMESTAMP - TestSets.tsCreated,
313 TestBoxesWithStrings.sOS,
314 SchedGroupNames.sSchedGroupNames
315 FROM (
316 SELECT TestBoxesInSchedGroups.idTestBox AS idTestBox,
317 STRING_AGG(SchedGroups.sName, ',') AS sSchedGroupNames
318 FROM TestBoxesInSchedGroups
319 INNER JOIN SchedGroups
320 ON SchedGroups.idSchedGroup = TestBoxesInSchedGroups.idSchedGroup
321 WHERE TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP
322 AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP
323 GROUP BY TestBoxesInSchedGroups.idTestBox
324 ) AS SchedGroupNames,
325 TestBoxesWithStrings
326 LEFT OUTER JOIN TestSets
327 ON TestSets.idTestBox = TestBoxesWithStrings.idTestBox
328 AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval)
329 AND TestSets.tsDone IS NOT NULL
330 WHERE TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
331 AND SchedGroupNames.idTestBox = TestBoxesWithStrings.idTestBox
332) UNION (
333 SELECT TestBoxesWithStrings.sName,
334 TestSets.enmStatus,
335 CURRENT_TIMESTAMP - TestSets.tsCreated,
336 TestBoxesWithStrings.sOS,
337 SchedGroupNames.sSchedGroupNames
338 FROM (
339 SELECT TestBoxesInSchedGroups.idTestBox AS idTestBox,
340 STRING_AGG(SchedGroups.sName, ',') AS sSchedGroupNames
341 FROM TestBoxesInSchedGroups
342 INNER JOIN SchedGroups
343 ON SchedGroups.idSchedGroup = TestBoxesInSchedGroups.idSchedGroup
344 WHERE TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP
345 AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP
346 GROUP BY TestBoxesInSchedGroups.idTestBox
347 ) AS SchedGroupNames,
348 TestBoxesWithStrings
349 LEFT OUTER JOIN TestSets
350 ON TestSets.idTestBox = TestBoxesWithStrings.idTestBox
351 AND TestSets.tsDone IS NULL
352 WHERE TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
353 AND SchedGroupNames.idTestBox = TestBoxesWithStrings.idTestBox
354)
355''', (cHoursBack, ));
356
357
358 #
359 # Process, format and output data.
360 #
361 dResult = testbox_data_processing(oDb);
362 self._oSrvGlue.setContentType('text/plain');
363 self._oSrvGlue.write(format_data(dResult, fSorted));
364
365 return True;
366
367 def _actionMagicMirrorTestResults(self):
368 """
369 Produces test result status for the magic mirror dashboard
370 """
371
372 #
373 # Parse arguments and connect to the database.
374 #
375 sBranch = self._getStringParam('sBranch');
376 cHoursBack = self._getIntParam('cHours', 1, 24*14, 6); ## @todo why 6 hours here and 12 for test boxes?
377 fSorted = self._getBoolParam('fSorted', False);
378 self._checkForUnknownParameters();
379
380 #
381 # Get the data.
382 #
383 # Note! These queries should be joining TestBoxesWithStrings and TestSets
384 # on idGenTestBox rather than on idTestBox and tsExpire=inf, but
385 # we don't have any index matching those. So, we'll ignore tests
386 # performed by deleted testboxes for the present as that doesn't
387 # happen often and we want the ~1000x speedup.
388 #
389 (oDb,) = self._connectToDb();
390 if oDb is None:
391 return False;
392
393 if sBranch == 'all':
394 oDb.execute('''
395SELECT TestSets.enmStatus,
396 TestCases.sName,
397 TestBoxesWithStrings.sOS
398FROM TestSets
399INNER JOIN TestCases
400 ON TestCases.idGenTestCase = TestSets.idGenTestCase
401INNER JOIN TestBoxesWithStrings
402 ON TestBoxesWithStrings.idTestBox = TestSets.idTestBox
403 AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
404WHERE TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval)
405''', (cHoursBack,));
406 else:
407 oDb.execute('''
408SELECT TestSets.enmStatus,
409 TestCases.sName,
410 TestBoxesWithStrings.sOS
411FROM TestSets
412INNER JOIN BuildCategories
413 ON BuildCategories.idBuildCategory = TestSets.idBuildCategory
414 AND BuildCategories.sBranch = %s
415INNER JOIN TestCases
416 ON TestCases.idGenTestCase = TestSets.idGenTestCase
417INNER JOIN TestBoxesWithStrings
418 ON TestBoxesWithStrings.idTestBox = TestSets.idTestBox
419 AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP
420WHERE TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval)
421''', (sBranch, cHoursBack,));
422
423 # Process the data
424 dResult = {};
425 while True:
426 aoRow = oDb.fetchOne();
427 if aoRow is None:
428 break;
429 os_results_separating(dResult, aoRow[1], aoRow[2], aoRow[0]) # save all test results
430
431 # Format and output it.
432 self._oSrvGlue.setContentType('text/plain');
433 self._oSrvGlue.write(format_data(dResult, fSorted));
434
435 return True;
436
437 def _getStandardParams(self, dParams):
438 """
439 Gets the standard parameters and validates them.
440
441 The parameters are returned as a tuple: sAction, (more later, maybe)
442 Note! the sTextBoxId can be None if it's a SIGNON request.
443
444 Raises StatusDispatcherException on invalid input.
445 """
446 #
447 # Get the action parameter and validate it.
448 #
449 if constants.tbreq.ALL_PARAM_ACTION not in dParams:
450 raise StatusDispatcherException('No "%s" parameter in request (params: %s)'
451 % (constants.tbreq.ALL_PARAM_ACTION, dParams,));
452 sAction = dParams[constants.tbreq.ALL_PARAM_ACTION];
453
454 if sAction not in self._dActions:
455 raise StatusDispatcherException('Unknown action "%s" in request (params: %s; action: %s)'
456 % (sAction, dParams, self._dActions));
457 #
458 # Update the list of checked parameters.
459 #
460 self._asCheckedParams.extend([constants.tbreq.ALL_PARAM_ACTION,]);
461
462 return (sAction,);
463
464 def dispatchRequest(self):
465 """
466 Dispatches the incoming request.
467
468 Will raise StatusDispatcherException on failure.
469 """
470
471 #
472 # Must be a GET request.
473 #
474 try:
475 sMethod = self._oSrvGlue.getMethod();
476 except Exception as oXcpt:
477 raise StatusDispatcherException('Error retriving request method: %s' % (oXcpt,));
478 if sMethod != 'GET':
479 raise StatusDispatcherException('Error expected POST request not "%s"' % (sMethod,));
480
481 #
482 # Get the parameters and checks for duplicates.
483 #
484 try:
485 dParams = self._oSrvGlue.getParameters();
486 except Exception as oXcpt:
487 raise StatusDispatcherException('Error retriving parameters: %s' % (oXcpt,));
488 for sKey in dParams.keys():
489 if len(dParams[sKey]) > 1:
490 raise StatusDispatcherException('Parameter "%s" is given multiple times: %s' % (sKey, dParams[sKey]));
491 dParams[sKey] = dParams[sKey][0];
492 self._dParams = dParams;
493
494 #
495 # Get+validate the standard action parameters and dispatch the request.
496 #
497 (self._sAction, ) = self._getStandardParams(dParams);
498 return self._dActions[self._sAction]();
499
500
501def main():
502 """
503 Main function a la C/C++. Returns exit code.
504 """
505
506 oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False);
507 try:
508 oDisp = StatusDispatcher(oSrvGlue);
509 oDisp.dispatchRequest();
510 oSrvGlue.flush();
511 except Exception as oXcpt:
512 return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info());
513
514 return 0;
515
516if __name__ == '__main__':
517 if config.g_kfProfileAdmin:
518 from testmanager.debug import cgiprofiling;
519 sys.exit(cgiprofiling.profileIt(main));
520 else:
521 sys.exit(main());
522
Note: See TracBrowser for help on using the repository browser.

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