VirtualBox

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

Last change on this file since 86936 was 86936, checked in by vboxsync, 4 years ago

testmanager/status.py: Fixes.

  • 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 86936 2020-11-20 15:14:02Z vboxsync $
4
5"""
6CGI - Administrator Web-UI.
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2012-2020 Oracle Corporation
12
13This file is part of VirtualBox Open Source Edition (OSE), as
14available from http://www.virtualbox.org. This file is free software;
15you can redistribute it and/or modify it under the terms of the GNU
16General Public License (GPL) as published by the Free Software
17Foundation, in version 2 as it comes in the "COPYING" file of the
18VirtualBox OSE distribution. VirtualBox OSE is distributed in the
19hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
20
21The contents of this file may alternatively be used under the terms
22of the Common Development and Distribution License Version 1.0
23(CDDL) only, as it comes in the "COPYING.CDDL" file of the
24VirtualBox OSE distribution, in which case the provisions of the
25CDDL are applicable instead of those of the GPL.
26
27You may elect to license modified versions of this file under the
28terms and conditions of either the GPL or the CDDL or both.
29"""
30__version__ = "$Revision: 86936 $"
31
32
33# Standard python imports.
34import os
35import sys
36import datetime
37
38# Only the main script needs to modify the path.
39g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
40sys.path.append(g_ksValidationKitDir);
41
42# Validation Kit imports.
43from testmanager import config;
44from testmanager.core.webservergluecgi import WebServerGlueCgi;
45
46from common import constants;
47from testmanager.core.base import TMExceptionBase;
48from testmanager.core.db import TMDatabaseConnection;
49
50
51
52def how_many_days_in_month(year, month):
53 def leap_year_check(year):
54 if year % 4 == 0 and year % 100 != 0:
55 return True
56 if year % 100 == 0 and year % 400 == 0:
57 return True
58 return False
59
60 month31 = (1, 3, 5, 7, 8, 10, 12)
61 month30 = (4, 6, 9, 11)
62 if month in month31:
63 days = 31
64 elif month in month30:
65 days = 30
66 else:
67 if leap_year_check(year):
68 days = 29
69 else:
70 days = 28
71 return days
72
73
74def target_date_from_time_span(cur_date, time_span_hours):
75 cur_year = cur_date.year
76 cur_month = cur_date.month
77 cur_day = cur_date.day
78 cur_hour = cur_date.hour
79 if cur_hour >= time_span_hours:
80 return cur_date.replace(hour=cur_hour-time_span_hours)
81 if cur_day > 1:
82 return cur_date.replace(day=cur_day-1,
83 hour=24+cur_hour-time_span_hours)
84 if cur_month > 1:
85 return cur_date.replace(month=cur_month-1,
86 day=how_many_days_in_month(cur_year, cur_month-1),
87 hour=24+cur_hour-time_span_hours)
88 return cur_date.replace(year=cur_year-1,
89 month=12,
90 day=31,
91 hour=24+cur_hour-time_span_hours)
92
93
94def find_test_duration(created):
95 now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc)
96 diff = now - created
97 days, seconds = diff.days, diff.seconds
98 hours = days * 24 + seconds // 3600
99 return hours
100
101
102def testbox_data_processing(oDb):
103 testboxes_dict = {}
104 while True:
105 line = oDb.fetchOne();
106 if line is None:
107 break;
108 testbox_name = line[0]
109 test_result = line[1]
110 test_created = line[2]
111 test_box_os = line[3]
112 test_sched_group = line[4]
113 testboxes_dict = dict_update(testboxes_dict, testbox_name, test_result)
114
115 if "testbox_os" not in testboxes_dict[testbox_name]:
116 testboxes_dict[testbox_name].update({"testbox_os": test_box_os})
117
118 if "sched_group" not in testboxes_dict[testbox_name]:
119 testboxes_dict[testbox_name].update({"sched_group": test_sched_group})
120 elif test_sched_group not in testboxes_dict[testbox_name]["sched_group"]:
121 testboxes_dict[testbox_name]["sched_group"] += "," + test_sched_group
122
123 if test_result == "running":
124 testboxes_dict[testbox_name].update({"hours_running": find_test_duration(test_created)})
125
126 return testboxes_dict;
127
128
129def os_results_separating(vb_dict, test_name, testbox_os, test_result):
130 if testbox_os == "linux":
131 dict_update(vb_dict, test_name + " / linux", test_result)
132 elif testbox_os == "win":
133 dict_update(vb_dict, test_name + " / windows", test_result)
134 elif testbox_os == "darwin":
135 dict_update(vb_dict, test_name + " / darwin", test_result)
136 elif testbox_os == "solaris":
137 dict_update(vb_dict, test_name + " / solaris", test_result)
138# else:
139# dict_update(vb_dict, test_name + " / other", test_result)
140
141
142# const/immutable.
143g_kdTestStatuses = {
144 'running': 0,
145 'success': 0,
146 'skipped': 0,
147 'bad-testbox': 0,
148 'aborted': 0,
149 'failure': 0,
150 'timed-out': 0,
151 'rebooted': 0,
152}
153
154def dict_update(target_dict, key_name, test_result):
155 if key_name not in target_dict:
156 target_dict.update({key_name: g_kdTestStatuses.copy()})
157 if test_result in g_kdTestStatuses:
158 target_dict[key_name][test_result] += 1
159 return target_dict
160
161
162def format_data(target_dict):
163 content = ""
164 for key in target_dict:
165 if "hours_running" in target_dict[key].keys():
166 content += "{};{};{} | running: {};{} | success: {} | skipped: {} | ".format(key,
167 target_dict[key]["testbox_os"],
168 target_dict[key]["sched_group"],
169 target_dict[key]["running"],
170 target_dict[key]["hours_running"],
171 target_dict[key]["success"],
172 target_dict[key]["skipped"],
173 )
174 elif "testbox_os" in target_dict[key].keys():
175 content += "{};{};{} | running: {} | success: {} | skipped: {} | ".format(key,
176 target_dict[key]["testbox_os"],
177 target_dict[key]["sched_group"],
178 target_dict[key]["running"],
179 target_dict[key]["success"],
180 target_dict[key]["skipped"],
181 )
182 else:
183 content += "{} | running: {} | success: {} | skipped: {} | ".format(key,
184 target_dict[key]["running"],
185 target_dict[key]["success"],
186 target_dict[key]["skipped"],
187 )
188 content += "bad-testbox: {} | aborted: {} | failure: {} | ".format(
189 target_dict[key]["bad-testbox"],
190 target_dict[key]["aborted"],
191 target_dict[key]["failure"],
192 )
193 content += "timed-out: {} | rebooted: {} | \n".format(
194 target_dict[key]["timed-out"],
195 target_dict[key]["rebooted"],
196 )
197 return content
198
199######
200
201class StatusDispatcherException(TMExceptionBase):
202 """
203 Exception class for TestBoxController.
204 """
205 pass; # pylint: disable=unnecessary-pass
206
207
208class StatusDispatcher(object): # pylint: disable=too-few-public-methods
209 """
210 Status dispatcher class.
211 """
212
213
214 def __init__(self, oSrvGlue):
215 """
216 Won't raise exceptions.
217 """
218 self._oSrvGlue = oSrvGlue;
219 self._sAction = None; # _getStandardParams / dispatchRequest sets this later on.
220 self._dParams = None; # _getStandardParams / dispatchRequest sets this later on.
221 self._asCheckedParams = [];
222 self._dActions = \
223 {
224 'MagicMirrorTestResults': self._actionMagicMirrorTestResults,
225 'MagicMirrorTestBoxes': self._actionMagicMirrorTestBoxes,
226 };
227
228 def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None):
229 """
230 Gets a string parameter (stripped).
231
232 Raises exception if not found and no default is provided, or if the
233 value isn't found in asValidValues.
234 """
235 if sName not in self._dParams:
236 if sDefValue is None:
237 raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName));
238 return sDefValue;
239 sValue = self._dParams[sName];
240 if fStrip:
241 sValue = sValue.strip();
242
243 if sName not in self._asCheckedParams:
244 self._asCheckedParams.append(sName);
245
246 if asValidValues is not None and sValue not in asValidValues:
247 raise StatusDispatcherException('%s parameter %s value "%s" not in %s '
248 % (self._sAction, sName, sValue, asValidValues));
249 return sValue;
250
251 def _getIntParam(self, sName, iMin = None, iMax = None, iDefValue = None):
252 """
253 Gets a string parameter.
254 Raises exception if not found, not a valid integer, or if the value
255 isn't in the range defined by iMin and iMax.
256 """
257 if sName not in self._dParams:
258 if iDefValue is None:
259 raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName));
260 return iDefValue;
261 sValue = self._dParams[sName];
262 try:
263 iValue = int(sValue, 0);
264 except:
265 raise StatusDispatcherException('%s parameter %s value "%s" cannot be convert to an integer'
266 % (self._sAction, sName, sValue));
267 if sName not in self._asCheckedParams:
268 self._asCheckedParams.append(sName);
269
270 if (iMin is not None and iValue < iMin) \
271 or (iMax is not None and iValue > iMax):
272 raise StatusDispatcherException('%s parameter %s value %d is out of range [%s..%s]'
273 % (self._sAction, sName, iValue, iMin, iMax));
274 return iValue;
275
276 def _checkForUnknownParameters(self):
277 """
278 Check if we've handled all parameters, raises exception if anything
279 unknown was found.
280 """
281
282 if len(self._asCheckedParams) != len(self._dParams):
283 sUnknownParams = '';
284 for sKey in self._dParams:
285 if sKey not in self._asCheckedParams:
286 sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey];
287 raise StatusDispatcherException('Unknown parameters: ' + sUnknownParams);
288
289 return True;
290
291 def _connectToDb(self):
292 """
293 Connects to the database.
294
295 Returns (TMDatabaseConnection, (more later perhaps) ) on success.
296 Returns (None, ) on failure after sending the box an appropriate response.
297 May raise exception on DB error.
298 """
299 return (TMDatabaseConnection(self._oSrvGlue.dprint),);
300
301 def _actionMagicMirrorTestBoxes(self):
302 """
303 Produces test result status for the magic mirror dashboard
304 """
305
306 #
307 # Parse arguments and connect to the database.
308 #
309 cHoursBack = self._getIntParam('cHours', 1, 24*14, 12);
310 self._checkForUnknownParameters();
311
312 #
313 # Get the data.
314 # bird: I changed these to join on idGenTestBox and skipping the tsExpire condition.
315 # @todo The query isn't very efficient as postgresql probably will repeat the
316 # subselect in column 5 for each result row.
317 #
318 (oDb,) = self._connectToDb();
319 if oDb is None:
320 return False;
321
322 oDb.execute('''
323SELECT TestBoxesWithStrings.sName,
324 TestSets.enmStatus,
325 TestSets.tsCreated,
326 TestBoxesWithStrings.sOS,
327 ( SELECT STRING_AGG(SchedGroups.sName, ',')
328 FROM SchedGroups
329 INNER JOIN TestBoxesInSchedGroups
330 ON TestBoxesInSchedGroups.idSchedGroup = SchedGroups.idSchedGroup
331 AND TestBoxesInSchedGroups.idTestBox = TestBoxesWithStrings.idTestBox
332 WHERE TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP
333 AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP
334 ) AS SchedGroupName
335FROM TestBoxesWithStrings
336LEFT OUTER JOIN TestSets
337 ON TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox
338 AND ( TestSets.tsCreated > (CURRENT_TIMESTAMP - '%s hours'::interval)
339 OR TestSets.tsDone IS NULL)
340''', (cHoursBack,));
341
342 # Process the data
343 dResult = testbox_data_processing(oDb);
344
345 # Format and output it.
346 self._oSrvGlue.setContentType('text/plain');
347 self._oSrvGlue.write(format_data(dResult));
348
349 return True;
350
351 def _actionMagicMirrorTestResults(self):
352 """
353 Produces test result status for the magic mirror dashboard
354 """
355
356 #
357 # Parse arguments and connect to the database.
358 #
359 sBranch = self._getStringParam('sBranch');
360 cHoursBack = self._getIntParam('cHours', 1, 24*14, 6); ## @todo why 6 hours here and 12 for test boxes?
361 self._checkForUnknownParameters();
362
363 #
364 # Get the data.
365 # bird: I changed these to join on idGenTestBox and idGenTestCase instead of
366 # also needing to check the tsExpire columns.
367 #
368 (oDb,) = self._connectToDb();
369 if oDb is None:
370 return False;
371
372 if sBranch == 'all':
373 oDb.execute('''
374SELECT TestSets.enmStatus,
375 TestCases.sName,
376 TestBoxesWithStrings.sOS
377FROM TestSets
378INNER JOIN TestCases
379 ON TestCases.idGenTestCase = TestCases.idGenTestCase
380INNER JOIN TestBoxesWithStrings
381 ON TestBoxesWithStrings.idGenTestBox = TestSets.idGenTestBox
382WHERE TestSets.tsCreated > (CURRENT_TIMESTAMP - '%s hours'::interval)
383''', (cHoursBack,));
384 else:
385 oDb.execute('''
386SELECT TestSets.enmStatus,
387 TestCases.sName,
388 TestBoxesWithStrings.sOS
389FROM TestSets
390INNER JOIN BuildCategories
391 ON BuildCategories.idBuildCategory = TestSets.idBuildCategory
392 AND BuildCategories.sBuildCategories = '%s'
393INNER JOIN TestCases
394 ON TestCases.idGenTestCase = TestSets.idGenTestCase
395INNER JOIN TestBoxesWithStrings
396 ON TestBoxesWithStrings.idGenTestBox = TestSets.idGenTestBox
397WHERE TestSets.tsCreated > (CURRENT_TIMESTAMP - '%s hours'::interval)
398''', (sBranch, cHoursBack,));
399
400 # Process the data
401 dResult = {};
402 while True:
403 aoRow = oDb.fetchOne();
404 if aoRow is None:
405 break;
406 os_results_separating(dResult, aoRow[1], aoRow[2], aoRow[0]) # save all test results
407
408 # Format and output it.
409 self._oSrvGlue.setContentType('text/plain');
410 self._oSrvGlue.write(format_data(dResult));
411
412 return True;
413
414 def _getStandardParams(self, dParams):
415 """
416 Gets the standard parameters and validates them.
417
418 The parameters are returned as a tuple: sAction, (more later, maybe)
419 Note! the sTextBoxId can be None if it's a SIGNON request.
420
421 Raises StatusDispatcherException on invalid input.
422 """
423 #
424 # Get the action parameter and validate it.
425 #
426 if constants.tbreq.ALL_PARAM_ACTION not in dParams:
427 raise StatusDispatcherException('No "%s" parameter in request (params: %s)'
428 % (constants.tbreq.ALL_PARAM_ACTION, dParams,));
429 sAction = dParams[constants.tbreq.ALL_PARAM_ACTION];
430
431 if sAction not in self._dActions:
432 raise StatusDispatcherException('Unknown action "%s" in request (params: %s; action: %s)'
433 % (sAction, dParams, self._dActions));
434 #
435 # Update the list of checked parameters.
436 #
437 self._asCheckedParams.extend([constants.tbreq.ALL_PARAM_ACTION,]);
438
439 return (sAction,);
440
441 def dispatchRequest(self):
442 """
443 Dispatches the incoming request.
444
445 Will raise StatusDispatcherException on failure.
446 """
447
448 #
449 # Must be a GET request.
450 #
451 try:
452 sMethod = self._oSrvGlue.getMethod();
453 except Exception as oXcpt:
454 raise StatusDispatcherException('Error retriving request method: %s' % (oXcpt,));
455 if sMethod != 'GET':
456 raise StatusDispatcherException('Error expected POST request not "%s"' % (sMethod,));
457
458 #
459 # Get the parameters and checks for duplicates.
460 #
461 try:
462 dParams = self._oSrvGlue.getParameters();
463 except Exception as oXcpt:
464 raise StatusDispatcherException('Error retriving parameters: %s' % (oXcpt,));
465 for sKey in dParams.keys():
466 if len(dParams[sKey]) > 1:
467 raise StatusDispatcherException('Parameter "%s" is given multiple times: %s' % (sKey, dParams[sKey]));
468 dParams[sKey] = dParams[sKey][0];
469 self._dParams = dParams;
470
471 #
472 # Get+validate the standard action parameters and dispatch the request.
473 #
474 (self._sAction, ) = self._getStandardParams(dParams);
475 return self._dActions[self._sAction]();
476
477
478def main():
479 """
480 Main function a la C/C++. Returns exit code.
481 """
482
483 oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False);
484 try:
485 oDisp = StatusDispatcher(oSrvGlue);
486 oDisp.dispatchRequest();
487 oSrvGlue.flush();
488 except Exception as oXcpt:
489 return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info());
490
491 return 0;
492
493if __name__ == '__main__':
494 if config.g_kfProfileAdmin:
495 from testmanager.debug import cgiprofiling;
496 sys.exit(cgiprofiling.profileIt(main));
497 else:
498 sys.exit(main());
499
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