VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/testbox.py@ 62548

Last change on this file since 62548 was 62484, checked in by vboxsync, 8 years ago

(C) 2016

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 45.3 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testbox.py 62484 2016-07-22 18:35:33Z vboxsync $
3
4"""
5Test Manager - TestBox.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2016 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.virtualbox.org. This file is free software;
14you can redistribute it and/or modify it under the terms of the GNU
15General Public License (GPL) as published by the Free Software
16Foundation, in version 2 as it comes in the "COPYING" file of the
17VirtualBox OSE distribution. VirtualBox OSE is distributed in the
18hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
19
20The contents of this file may alternatively be used under the terms
21of the Common Development and Distribution License Version 1.0
22(CDDL) only, as it comes in the "COPYING.CDDL" file of the
23VirtualBox OSE distribution, in which case the provisions of the
24CDDL are applicable instead of those of the GPL.
25
26You may elect to license modified versions of this file under the
27terms and conditions of either the GPL or the CDDL or both.
28"""
29__version__ = "$Revision: 62484 $"
30
31
32# Standard python imports.
33import copy;
34import unittest;
35
36# Validation Kit imports.
37from testmanager.core import db;
38from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMInFligthCollision, \
39 TMInvalidData, TMTooManyRows, TMRowNotFound, \
40 ChangeLogEntry, AttributeChangeEntry;
41from testmanager.core.useraccount import UserAccountLogic;
42
43
44class TestBoxInSchedGroupData(ModelDataBase):
45 """
46 TestBox in SchedGroup data.
47 """
48
49 ksParam_idTestBox = 'TestBoxInSchedGroup_idTestBox';
50 ksParam_idSchedGroup = 'TestBoxInSchedGroup_idSchedGroup';
51 ksParam_tsEffective = 'TestBoxInSchedGroup_tsEffective';
52 ksParam_tsExpire = 'TestBoxInSchedGroup_tsExpire';
53 ksParam_uidAuthor = 'TestBoxInSchedGroup_uidAuthor';
54 ksParam_iSchedPriority = 'TestBoxInSchedGroup_iSchedPriority';
55
56 kasAllowNullAttributes = [ 'idTestBox', 'tsEffective', 'tsExpire', 'uidAuthor', ]
57
58 kiMin_iSchedPriority = 0;
59 kiMax_iSchedPriority = 32;
60
61 kcDbColumns = 6;
62
63 def __init__(self):
64 ModelDataBase.__init__(self);
65 self.idTestBox = None;
66 self.idSchedGroup = None;
67 self.tsEffective = None;
68 self.tsExpire = None;
69 self.uidAuthor = None;
70 self.iSchedPriority = 16;
71
72 def initFromDbRow(self, aoRow):
73 """
74 Expecting the result from a query like this:
75 SELECT * FROM TestBoxesInSchedGroups
76 """
77 if aoRow is None:
78 raise TMRowNotFound('TestBox/SchedGroup not found.');
79
80 self.idTestBox = aoRow[0];
81 self.idSchedGroup = aoRow[1];
82 self.tsEffective = aoRow[2];
83 self.tsExpire = aoRow[3];
84 self.uidAuthor = aoRow[4];
85 self.iSchedPriority = aoRow[5];
86
87 return self;
88
89class TestBoxInSchedGroupDataEx(TestBoxInSchedGroupData):
90 """
91 Extended version of TestBoxInSchedGroupData that contains the scheduling group.
92 """
93
94 def __init__(self):
95 TestBoxInSchedGroupData.__init__(self);
96 self.oSchedGroup = None; # type: SchedGroupData
97
98 def initFromDbRowEx(self, aoRow, oDb, tsNow = None, sPeriodBack = None):
99 """
100 Extended version of initFromDbRow that fills in the rest from the database.
101 """
102 from testmanager.core.schedgroup import SchedGroupData;
103 self.initFromDbRow(aoRow);
104 self.oSchedGroup = SchedGroupData().initFromDbWithId(oDb, self.idSchedGroup, tsNow, sPeriodBack);
105 return self;
106
107
108# pylint: disable=C0103
109class TestBoxData(ModelDataBase): # pylint: disable=R0902
110 """
111 TestBox Data.
112 """
113
114 ## LomKind_T
115 ksLomKind_None = 'none';
116 ksLomKind_ILOM = 'ilom';
117 ksLomKind_ELOM = 'elom';
118 ksLomKind_AppleXserveLom = 'apple-xserver-lom';
119 kasLomKindValues = [ ksLomKind_None, ksLomKind_ILOM, ksLomKind_ELOM, ksLomKind_AppleXserveLom];
120 kaoLomKindDescs = \
121 [
122 ( ksLomKind_None, 'None', ''),
123 ( ksLomKind_ILOM, 'ILOM', ''),
124 ( ksLomKind_ELOM, 'ELOM', ''),
125 ( ksLomKind_AppleXserveLom, 'Apple Xserve LOM', ''),
126 ];
127
128
129 ## TestBoxCmd_T
130 ksTestBoxCmd_None = 'none';
131 ksTestBoxCmd_Abort = 'abort';
132 ksTestBoxCmd_Reboot = 'reboot';
133 ksTestBoxCmd_Upgrade = 'upgrade';
134 ksTestBoxCmd_UpgradeAndReboot = 'upgrade-and-reboot';
135 ksTestBoxCmd_Special = 'special';
136 kasTestBoxCmdValues = [ ksTestBoxCmd_None, ksTestBoxCmd_Abort, ksTestBoxCmd_Reboot, ksTestBoxCmd_Upgrade,
137 ksTestBoxCmd_UpgradeAndReboot, ksTestBoxCmd_Special];
138 kaoTestBoxCmdDescs = \
139 [
140 ( ksTestBoxCmd_None, 'None', ''),
141 ( ksTestBoxCmd_Abort, 'Abort current test', ''),
142 ( ksTestBoxCmd_Reboot, 'Reboot TestBox', ''),
143 ( ksTestBoxCmd_Upgrade, 'Upgrade TestBox Script', ''),
144 ( ksTestBoxCmd_UpgradeAndReboot, 'Upgrade TestBox Script and reboot', ''),
145 ( ksTestBoxCmd_Special, 'Special (reserved)', ''),
146 ];
147
148
149 ksIdAttr = 'idTestBox';
150 ksIdGenAttr = 'idGenTestBox';
151
152 ksParam_idTestBox = 'TestBox_idTestBox';
153 ksParam_tsEffective = 'TestBox_tsEffective';
154 ksParam_tsExpire = 'TestBox_tsExpire';
155 ksParam_uidAuthor = 'TestBox_uidAuthor';
156 ksParam_idGenTestBox = 'TestBox_idGenTestBox';
157 ksParam_ip = 'TestBox_ip';
158 ksParam_uuidSystem = 'TestBox_uuidSystem';
159 ksParam_sName = 'TestBox_sName';
160 ksParam_sDescription = 'TestBox_sDescription';
161 ksParam_fEnabled = 'TestBox_fEnabled';
162 ksParam_enmLomKind = 'TestBox_enmLomKind';
163 ksParam_ipLom = 'TestBox_ipLom';
164 ksParam_pctScaleTimeout = 'TestBox_pctScaleTimeout';
165 ksParam_sComment = 'TestBox_sComment';
166 ksParam_sOs = 'TestBox_sOs';
167 ksParam_sOsVersion = 'TestBox_sOsVersion';
168 ksParam_sCpuVendor = 'TestBox_sCpuVendor';
169 ksParam_sCpuArch = 'TestBox_sCpuArch';
170 ksParam_sCpuName = 'TestBox_sCpuName';
171 ksParam_lCpuRevision = 'TestBox_lCpuRevision';
172 ksParam_cCpus = 'TestBox_cCpus';
173 ksParam_fCpuHwVirt = 'TestBox_fCpuHwVirt';
174 ksParam_fCpuNestedPaging = 'TestBox_fCpuNestedPaging';
175 ksParam_fCpu64BitGuest = 'TestBox_fCpu64BitGuest';
176 ksParam_fChipsetIoMmu = 'TestBox_fChipsetIoMmu';
177 ksParam_fRawMode = 'TestBox_fRawMode';
178 ksParam_cMbMemory = 'TestBox_cMbMemory';
179 ksParam_cMbScratch = 'TestBox_cMbScratch';
180 ksParam_sReport = 'TestBox_sReport';
181 ksParam_iTestBoxScriptRev = 'TestBox_iTestBoxScriptRev';
182 ksParam_iPythonHexVersion = 'TestBox_iPythonHexVersion';
183 ksParam_enmPendingCmd = 'TestBox_enmPendingCmd';
184
185 kasInternalAttributes = [ 'idStrDescription', 'idStrComment', 'idStrOs', 'idStrOsVersion', 'idStrCpuVendor',
186 'idStrCpuArch', 'idStrCpuName', 'idStrReport', ];
187 kasMachineSettableOnly = [ 'sOs', 'sOsVersion', 'sCpuVendor', 'sCpuArch', 'sCpuName', 'lCpuRevision', 'cCpus',
188 'fCpuHwVirt', 'fCpuNestedPaging', 'fCpu64BitGuest', 'fChipsetIoMmu', 'fRawMode',
189 'cMbMemory', 'cMbScratch', 'sReport', 'iTestBoxScriptRev', 'iPythonHexVersion', ];
190 kasAllowNullAttributes = ['idTestBox', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestBox', 'sDescription',
191 'ipLom', 'sComment', ] + kasMachineSettableOnly + kasInternalAttributes;
192
193 kasValidValues_enmLomKind = kasLomKindValues;
194 kasValidValues_enmPendingCmd = kasTestBoxCmdValues;
195 kiMin_pctScaleTimeout = 11;
196 kiMax_pctScaleTimeout = 19999;
197 kcchMax_sReport = 65535;
198
199 kcDbColumns = 40; # including the 7 string joins columns
200
201
202 def __init__(self):
203 ModelDataBase.__init__(self);
204
205 #
206 # Initialize with defaults.
207 # See the database for explanations of each of these fields.
208 #
209 self.idTestBox = None;
210 self.tsEffective = None;
211 self.tsExpire = None;
212 self.uidAuthor = None;
213 self.idGenTestBox = None;
214 self.ip = None;
215 self.uuidSystem = None;
216 self.sName = None;
217 self.idStrDescription = None;
218 self.fEnabled = False;
219 self.enmLomKind = self.ksLomKind_None;
220 self.ipLom = None;
221 self.pctScaleTimeout = 100;
222 self.idStrComment = None;
223 self.idStrOs = None;
224 self.idStrOsVersion = None;
225 self.idStrCpuVendor = None;
226 self.idStrCpuArch = None;
227 self.idStrCpuName = None;
228 self.lCpuRevision = None;
229 self.cCpus = 1;
230 self.fCpuHwVirt = False;
231 self.fCpuNestedPaging = False;
232 self.fCpu64BitGuest = False;
233 self.fChipsetIoMmu = False;
234 self.fRawMode = None;
235 self.cMbMemory = 1;
236 self.cMbScratch = 0;
237 self.idStrReport = None;
238 self.iTestBoxScriptRev = 0;
239 self.iPythonHexVersion = 0;
240 self.enmPendingCmd = self.ksTestBoxCmd_None;
241 # String table values.
242 self.sDescription = None;
243 self.sComment = None;
244 self.sOs = None;
245 self.sOsVersion = None;
246 self.sCpuVendor = None;
247 self.sCpuArch = None;
248 self.sCpuName = None;
249 self.sReport = None;
250
251 def initFromDbRow(self, aoRow):
252 """
253 Internal worker for initFromDbWithId and initFromDbWithGenId as well as
254 from TestBoxLogic. Expecting the result from a query like this:
255 SELECT TestBoxesWithStrings.* FROM TestBoxesWithStrings
256 """
257 if aoRow is None:
258 raise TMRowNotFound('TestBox not found.');
259
260 self.idTestBox = aoRow[0];
261 self.tsEffective = aoRow[1];
262 self.tsExpire = aoRow[2];
263 self.uidAuthor = aoRow[3];
264 self.idGenTestBox = aoRow[4];
265 self.ip = aoRow[5];
266 self.uuidSystem = aoRow[6];
267 self.sName = aoRow[7];
268 self.idStrDescription = aoRow[8];
269 self.fEnabled = aoRow[9];
270 self.enmLomKind = aoRow[10];
271 self.ipLom = aoRow[11];
272 self.pctScaleTimeout = aoRow[12];
273 self.idStrComment = aoRow[13];
274 self.idStrOs = aoRow[14];
275 self.idStrOsVersion = aoRow[15];
276 self.idStrCpuVendor = aoRow[16];
277 self.idStrCpuArch = aoRow[17];
278 self.idStrCpuName = aoRow[18];
279 self.lCpuRevision = aoRow[19];
280 self.cCpus = aoRow[20];
281 self.fCpuHwVirt = aoRow[21];
282 self.fCpuNestedPaging = aoRow[22];
283 self.fCpu64BitGuest = aoRow[23];
284 self.fChipsetIoMmu = aoRow[24];
285 self.fRawMode = aoRow[25];
286 self.cMbMemory = aoRow[26];
287 self.cMbScratch = aoRow[27];
288 self.idStrReport = aoRow[28];
289 self.iTestBoxScriptRev = aoRow[29];
290 self.iPythonHexVersion = aoRow[30];
291 self.enmPendingCmd = aoRow[31];
292
293 # String table values.
294 if len(aoRow) > 32:
295 self.sDescription = aoRow[32];
296 self.sComment = aoRow[33];
297 self.sOs = aoRow[34];
298 self.sOsVersion = aoRow[35];
299 self.sCpuVendor = aoRow[36];
300 self.sCpuArch = aoRow[37];
301 self.sCpuName = aoRow[38];
302 self.sReport = aoRow[39];
303
304 return self;
305
306 def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None):
307 """
308 Initialize the object from the database.
309 """
310 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
311 'SELECT TestBoxesWithStrings.*\n'
312 'FROM TestBoxesWithStrings\n'
313 'WHERE idTestBox = %s\n'
314 , ( idTestBox, ), tsNow, sPeriodBack));
315 aoRow = oDb.fetchOne()
316 if aoRow is None:
317 raise TMRowNotFound('idTestBox=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestBox, tsNow, sPeriodBack,));
318 return self.initFromDbRow(aoRow);
319
320 def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None):
321 """
322 Initialize the object from the database.
323 """
324 _ = tsNow; # Only useful for extended data classes.
325 oDb.execute('SELECT TestBoxesWithStrings.*\n'
326 'FROM TestBoxesWithStrings\n'
327 'WHERE idGenTestBox = %s\n'
328 , (idGenTestBox, ) );
329 return self.initFromDbRow(oDb.fetchOne());
330
331 def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other):
332 # Override to do extra ipLom checks.
333 dErrors = ModelDataBase._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor);
334 if self.ksParam_ipLom not in dErrors \
335 and self.ksParam_enmLomKind not in dErrors \
336 and self.enmLomKind != self.ksLomKind_None \
337 and self.ipLom is None:
338 dErrors[self.ksParam_ipLom] = 'Light-out-management IP is mandatory and a LOM is selected.'
339 return dErrors;
340
341 def formatPythonVersion(self):
342 """
343 Unbuttons the version number and formats it as a version string.
344 """
345 if self.iPythonHexVersion is None:
346 return 'N/A';
347 return 'v%d.%d.%d.%d' \
348 % ( self.iPythonHexVersion >> 24,
349 (self.iPythonHexVersion >> 16) & 0xff,
350 (self.iPythonHexVersion >> 8) & 0xff,
351 self.iPythonHexVersion & 0xff);
352
353 def getCpuFamily(self):
354 """ Returns the CPU family for a x86 or amd64 testboxes."""
355 if self.lCpuRevision is None:
356 return 0;
357 return (self.lCpuRevision >> 24 & 0xff);
358
359 def getCpuModel(self):
360 """ Returns the CPU model for a x86 or amd64 testboxes."""
361 if self.lCpuRevision is None:
362 return 0;
363 return (self.lCpuRevision >> 8 & 0xffff);
364
365 def getCpuStepping(self):
366 """ Returns the CPU stepping for a x86 or amd64 testboxes."""
367 if self.lCpuRevision is None:
368 return 0;
369 return (self.lCpuRevision & 0xff);
370
371 # The following is a translation of the g_aenmIntelFamily06 array in CPUMR3CpuId.cpp:
372 kdIntelFamily06 = {
373 0x00: 'P6',
374 0x01: 'P6',
375 0x03: 'P6_II',
376 0x05: 'P6_II',
377 0x06: 'P6_II',
378 0x07: 'P6_III',
379 0x08: 'P6_III',
380 0x09: 'P6_M_Banias',
381 0x0a: 'P6_III',
382 0x0b: 'P6_III',
383 0x0d: 'P6_M_Dothan',
384 0x0e: 'Core_Yonah',
385 0x0f: 'Core2_Merom',
386 0x15: 'P6_M_Dothan',
387 0x16: 'Core2_Merom',
388 0x17: 'Core2_Penryn',
389 0x1a: 'Core7_Nehalem',
390 0x1c: 'Atom_Bonnell',
391 0x1d: 'Core2_Penryn',
392 0x1e: 'Core7_Nehalem',
393 0x1f: 'Core7_Nehalem',
394 0x25: 'Core7_Westmere',
395 0x26: 'Atom_Lincroft',
396 0x27: 'Atom_Saltwell',
397 0x2a: 'Core7_SandyBridge',
398 0x2c: 'Core7_Westmere',
399 0x2d: 'Core7_SandyBridge',
400 0x2e: 'Core7_Nehalem',
401 0x2f: 'Core7_Westmere',
402 0x35: 'Atom_Saltwell',
403 0x36: 'Atom_Saltwell',
404 0x37: 'Atom_Silvermont',
405 0x3a: 'Core7_IvyBridge',
406 0x3c: 'Core7_Haswell',
407 0x3d: 'Core7_Broadwell',
408 0x3e: 'Core7_IvyBridge',
409 0x3f: 'Core7_Haswell',
410 0x45: 'Core7_Haswell',
411 0x46: 'Core7_Haswell',
412 0x47: 'Core7_Broadwell',
413 0x4a: 'Atom_Silvermont',
414 0x4c: 'Atom_Airmount',
415 0x4d: 'Atom_Silvermont',
416 0x4e: 'Core7_Skylake',
417 0x4f: 'Core7_Broadwell',
418 0x55: 'Core7_Skylake',
419 0x56: 'Core7_Broadwell',
420 0x5a: 'Atom_Silvermont',
421 0x5c: 'Atom_Goldmont',
422 0x5d: 'Atom_Silvermont',
423 0x5e: 'Core7_Skylake',
424 0x66: 'Core7_Cannonlake',
425 };
426 # Also from CPUMR3CpuId.cpp, but the switch.
427 kdIntelFamily15 = {
428 0x00: 'NB_Willamette',
429 0x01: 'NB_Willamette',
430 0x02: 'NB_Northwood',
431 0x03: 'NB_Prescott',
432 0x04: 'NB_Prescott2M',
433 0x05: 'NB_Unknown',
434 0x06: 'NB_CedarMill',
435 0x07: 'NB_Gallatin',
436 };
437
438 def queryCpuMicroarch(self):
439 """ Try guess the microarch name for the cpu. Returns None if we cannot. """
440 if self.lCpuRevision is None or self.sCpuVendor is None:
441 return None;
442 uFam = self.getCpuFamily();
443 uMod = self.getCpuModel();
444 if self.sCpuVendor == 'GenuineIntel':
445 if uFam == 6:
446 return self.kdIntelFamily06.get(uMod, None);
447 if uFam == 15:
448 return self.kdIntelFamily15.get(uMod, None);
449 elif self.sCpuVendor == 'AuthenticAMD':
450 if uFam == 0xf:
451 if uMod < 0x10: return 'K8_130nm';
452 if uMod >= 0x60 and uMod < 0x80: return 'K8_65nm';
453 if uMod >= 0x40: return 'K8_90nm_AMDV';
454 if uMod in [0x21, 0x23, 0x2b, 0x37, 0x3f]: return 'K8_90nm_DualCore';
455 return 'AMD_K8_90nm';
456 if uFam == 0x10: return 'K10';
457 if uFam == 0x11: return 'K10_Lion';
458 if uFam == 0x12: return 'K10_Llano';
459 if uFam == 0x14: return 'Bobcat';
460 if uFam == 0x15:
461 if uMod <= 0x01: return 'Bulldozer';
462 if uMod in [0x02, 0x10, 0x13]: return 'Piledriver';
463 return None;
464 if uFam == 0x16:
465 return 'Jaguar';
466 elif self.sCpuVendor == 'CentaurHauls':
467 if uFam == 0x05:
468 if uMod == 0x01: return 'Centaur_C6';
469 if uMod == 0x04: return 'Centaur_C6';
470 if uMod == 0x08: return 'Centaur_C2';
471 if uMod == 0x09: return 'Centaur_C3';
472 if uFam == 0x06:
473 if uMod == 0x05: return 'VIA_C3_M2';
474 if uMod == 0x06: return 'VIA_C3_C5A';
475 if uMod == 0x07: return 'VIA_C3_C5B' if self.getCpuStepping() < 8 else 'VIA_C3_C5C';
476 if uMod == 0x08: return 'VIA_C3_C5N';
477 if uMod == 0x09: return 'VIA_C3_C5XL' if self.getCpuStepping() < 8 else 'VIA_C3_C5P';
478 if uMod == 0x0a: return 'VIA_C7_C5J';
479 if uMod == 0x0f: return 'VIA_Isaiah';
480 return None;
481
482 def getPrettyCpuVersion(self):
483 """ Pretty formatting of the family/model/stepping with microarch optimizations. """
484 if self.lCpuRevision is None or self.sCpuVendor is None:
485 return u'<none>';
486 sMarch = self.queryCpuMicroarch();
487 if sMarch is not None:
488 return '%s m%02X s%02X' % (sMarch, self.getCpuModel(), self.getCpuStepping());
489 return 'fam%02X m%02X s%02X' % (self.getCpuFamily(), self.getCpuModel(), self.getCpuStepping());
490
491 def getArchBitString(self):
492 """ Returns 32-bit, 64-bit, <none>, or sCpuArch. """
493 if self.sCpuArch is None:
494 return '<none>';
495 if self.sCpuArch in [ 'x86',]:
496 return '32-bit';
497 if self.sCpuArch in [ 'amd64',]:
498 return '64-bit';
499 return self.sCpuArch;
500
501 def getPrettyCpuVendor(self):
502 """ Pretty vendor name."""
503 if self.sCpuVendor is None:
504 return '<none>';
505 if self.sCpuVendor == 'GenuineIntel': return 'Intel';
506 if self.sCpuVendor == 'AuthenticAMD': return 'AMD';
507 if self.sCpuVendor == 'CentaurHauls': return 'VIA';
508 return self.sCpuVendor;
509
510
511class TestBoxDataEx(TestBoxData):
512 """
513 TestBox data.
514 """
515
516 ksParam_aoInSchedGroups = 'TestBox_aoInSchedGroups';
517
518 # Use [] instead of None.
519 kasAltArrayNull = [ 'aoInSchedGroups', ];
520
521 ## Helper parameter containing the comma separated list with the IDs of
522 # potential members found in the parameters.
523 ksParam_aidSchedGroups = 'TestBoxDataEx_aidSchedGroups';
524
525 def __init__(self):
526 TestBoxData.__init__(self);
527 self.aoInSchedGroups = []; # type: list[TestBoxInSchedGroupData]
528
529 def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None):
530 """
531 Worker shared by the initFromDb* methods.
532 Returns self. Raises exception if no row or database error.
533 """
534 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
535 'SELECT *\n'
536 'FROM TestBoxesInSchedGroups\n'
537 'WHERE idTestBox = %s\n'
538 , (self.idTestBox,), tsNow, sPeriodBack)
539 + 'ORDER BY idSchedGroup\n' );
540 self.aoInSchedGroups = [];
541 for aoRow in oDb.fetchAll():
542 self.aoInSchedGroups.append(TestBoxInSchedGroupDataEx().initFromDbRowEx(aoRow, oDb, tsNow, sPeriodBack));
543 return self;
544
545 def initFromDbRowEx(self, aoRow, oDb, tsNow = None):
546 """
547 Reinitialize from a SELECT * FROM TestBoxesWithStrings row. Will query the
548 necessary additional data from oDb using tsNow.
549 Returns self. Raises exception if no row or database error.
550 """
551 TestBoxData.initFromDbRow(self, aoRow);
552 return self._initExtraMembersFromDb(oDb, tsNow);
553
554 def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None):
555 """
556 Initialize the object from the database.
557 """
558 TestBoxData.initFromDbWithId(self, oDb, idTestBox, tsNow, sPeriodBack);
559 return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack);
560
561 def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None):
562 """
563 Initialize the object from the database.
564 """
565 TestBoxData.initFromDbWithGenId(self, oDb, idGenTestBox);
566 if tsNow is None and not oDb.isTsInfinity(self.tsExpire):
567 tsNow = self.tsEffective;
568 return self._initExtraMembersFromDb(oDb, tsNow);
569
570 def getAttributeParamNullValues(self, sAttr): # Necessary?
571 if sAttr in ['aoInSchedGroups', ]:
572 return [[], ''];
573 return TestBoxData.getAttributeParamNullValues(self, sAttr);
574
575 def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict):
576 """
577 For dealing with the in-scheduling-group list.
578 """
579 if sAttr != 'aoInSchedGroups':
580 return TestBoxData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict);
581
582 aoNewValues = [];
583 aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = []);
584 asIds = oDisp.getStringParam(self.ksParam_aidSchedGroups, sDefault = '').split(',');
585 for idSchedGroup in asIds:
586 try: idSchedGroup = int(idSchedGroup);
587 except: pass;
588 oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestBoxDataEx.ksParam_aoInSchedGroups, idSchedGroup,))
589 oMember = TestBoxInSchedGroupData().initFromParams(oDispWrapper, fStrict = False);
590 if idSchedGroup in aidSelected:
591 aoNewValues.append(oMember);
592 return aoNewValues;
593
594 def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): # pylint: disable=R0914
595 """
596 Validate special arrays and requirement expressions.
597
598 Some special needs for the in-scheduling-group list.
599 """
600 if sAttr != 'aoInSchedGroups':
601 return TestBoxData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
602
603 asErrors = [];
604 aoNewValues = [];
605
606 # Note! We'll be returning an error dictionary instead of an string here.
607 dErrors = {};
608
609 for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups):
610 oInSchedGroup = copy.copy(oInSchedGroup);
611 oInSchedGroup.idTestBox = self.idTestBox;
612 dCurErrors = oInSchedGroup.validateAndConvert(oDb, ModelDataBase.ksValidateFor_Other);
613 if len(dCurErrors) == 0:
614 pass; ## @todo figure out the ID?
615 else:
616 asErrors = [];
617 for sKey in dCurErrors:
618 asErrors.append('%s: %s' % (sKey[len('TestBoxInSchedGroup_'):], dCurErrors[sKey]));
619 dErrors[iInGrp] = '<br>\n'.join(asErrors)
620 aoNewValues.append(oInSchedGroup);
621
622 for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups):
623 for iInGrp2 in xrange(iInGrp + 1, len(self.aoInSchedGroups)):
624 if self.aoInSchedGroups[iInGrp2].idSchedGroup == oInSchedGroup.idSchedGroup:
625 sMsg = 'Duplicate scheduling group #%s".' % (oInSchedGroup.idSchedGroup,);
626 if iInGrp in dErrors: dErrors[iInGrp] += '<br>\n' + sMsg;
627 else: dErrors[iInGrp] = sMsg;
628 if iInGrp2 in dErrors: dErrors[iInGrp2] += '<br>\n' + sMsg;
629 else: dErrors[iInGrp2] = sMsg;
630 break;
631
632 return (aoNewValues, dErrors if len(dErrors) > 0 else None);
633
634
635class TestBoxLogic(ModelLogicBase):
636 """
637 TestBox logic.
638 """
639
640
641 def __init__(self, oDb):
642 ModelLogicBase.__init__(self, oDb);
643 self.dCache = None;
644
645 def tryFetchTestBoxByUuid(self, sTestBoxUuid):
646 """
647 Tries to fetch a testbox by its UUID alone.
648 """
649 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
650 'FROM TestBoxesWithStrings\n'
651 'WHERE uuidSystem = %s\n'
652 ' AND tsExpire = \'infinity\'::timestamp\n'
653 'ORDER BY tsEffective DESC\n',
654 (sTestBoxUuid,));
655 if self._oDb.getRowCount() == 0:
656 return None;
657 if self._oDb.getRowCount() != 1:
658 raise TMTooManyRows('Database integrity error: %u hits' % (self._oDb.getRowCount(),));
659 oData = TestBoxData();
660 oData.initFromDbRow(self._oDb.fetchOne());
661 return oData;
662
663 def fetchForListing(self, iStart, cMaxRows, tsNow):
664 """
665 Fetches testboxes for listing.
666
667 Returns an array (list) of TestBoxDataForListing items, empty list if none.
668 The TestBoxDataForListing instances are just TestBoxData with two extra
669 members, an extra oStatus member that is either None or a TestBoxStatusData
670 instance, and a member tsCurrent holding CURRENT_TIMESTAMP.
671
672 Raises exception on error.
673 """
674 class TestBoxDataForListing(TestBoxDataEx):
675 """ We add two members for the listing. """
676 def __init__(self):
677 TestBoxDataEx.__init__(self);
678 self.tsCurrent = None; # CURRENT_TIMESTAMP
679 self.oStatus = None; # type: TestBoxStatusData
680
681 from testmanager.core.testboxstatus import TestBoxStatusData;
682
683 if tsNow is None:
684 self._oDb.execute('SELECT TestBoxesWithStrings.*,\n'
685 ' TestBoxStatuses.*\n'
686 'FROM TestBoxesWithStrings\n'
687 ' LEFT OUTER JOIN TestBoxStatuses\n'
688 ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n'
689 'WHERE TestBoxesWithStrings.tsExpire = \'infinity\'::TIMESTAMP\n'
690 'ORDER BY TestBoxesWithStrings.sName\n'
691 'LIMIT %s OFFSET %s\n'
692 , (cMaxRows, iStart,));
693 else:
694 self._oDb.execute('SELECT TestBoxesWithStrings.*,\n'
695 ' TestBoxStatuses.*\n'
696 'FROM TestBoxesWithStrings\n'
697 ' LEFT OUTER JOIN TestBoxStatuses\n'
698 ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n'
699 'WHERE tsExpire > %s\n'
700 ' AND tsEffective <= %s\n'
701 'ORDER BY TestBoxesWithStrings.sName\n'
702 'LIMIT %s OFFSET %s\n'
703 , ( tsNow, tsNow, cMaxRows, iStart,));
704
705 aoRows = [];
706 for aoOne in self._oDb.fetchAll():
707 oTestBox = TestBoxDataForListing().initFromDbRowEx(aoOne, self._oDb, tsNow);
708 oTestBox.tsCurrent = self._oDb.getCurrentTimestamp();
709 if aoOne[TestBoxData.kcDbColumns] is not None:
710 oTestBox.oStatus = TestBoxStatusData().initFromDbRow(aoOne[TestBoxData.kcDbColumns:]);
711 aoRows.append(oTestBox);
712 return aoRows;
713
714 def fetchForChangeLog(self, idTestBox, iStart, cMaxRows, tsNow): # pylint: disable=R0914
715 """
716 Fetches change log entries for a testbox.
717
718 Returns an array of ChangeLogEntry instance and an indicator whether
719 there are more entries.
720 Raises exception on error.
721 """
722
723 ## @todo calc changes to scheduler group!
724
725 if tsNow is None:
726 tsNow = self._oDb.getCurrentTimestamp();
727
728 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
729 'FROM TestBoxesWithStrings\n'
730 'WHERE TestBoxesWithStrings.tsEffective <= %s\n'
731 ' AND TestBoxesWithStrings.idTestBox = %s\n'
732 'ORDER BY TestBoxesWithStrings.tsExpire DESC\n'
733 'LIMIT %s OFFSET %s\n'
734 , (tsNow, idTestBox, cMaxRows + 1, iStart,));
735
736 aoRows = [];
737 for aoDbRow in self._oDb.fetchAll():
738 aoRows.append(TestBoxData().initFromDbRow(aoDbRow));
739
740 # Calculate the changes.
741 aoEntries = [];
742 for i in xrange(0, len(aoRows) - 1):
743 oNew = aoRows[i];
744 oOld = aoRows[i + 1];
745 aoChanges = [];
746 for sAttr in oNew.getDataAttributes():
747 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
748 oOldAttr = getattr(oOld, sAttr);
749 oNewAttr = getattr(oNew, sAttr);
750 if oOldAttr != oNewAttr:
751 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
752 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
753
754 # If we're at the end of the log, add the initial entry.
755 if len(aoRows) <= cMaxRows and len(aoRows) > 0:
756 oNew = aoRows[-1];
757 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
758
759 UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries);
760 return (aoEntries, len(aoRows) > cMaxRows);
761
762 def _validateAndConvertData(self, oData, enmValidateFor):
763 # type: (TestBoxDataEx, str) -> None
764 """
765 Helper for addEntry and editEntry that validates the scheduling group IDs in
766 addtion to what's covered by the default validateAndConvert of the data object.
767
768 Raises exception on invalid input.
769 """
770 dDataErrors = oData.validateAndConvert(self._oDb, enmValidateFor);
771 if len(dDataErrors) > 0:
772 raise TMInvalidData('TestBoxLogic.addEntry: %s' % (dDataErrors,));
773 if isinstance(oData, TestBoxDataEx):
774 if len(oData.aoInSchedGroups):
775 sSchedGrps = ', '.join('(%s)' % oCur.idSchedGroup for oCur in oData.aoInSchedGroups);
776 self._oDb.execute('SELECT SchedGroupIDs.idSchedGroup\n'
777 'FROM (VALUES ' + sSchedGrps + ' ) AS SchedGroupIDs(idSchedGroup)\n'
778 ' LEFT OUTER JOIN SchedGroups\n'
779 ' ON SchedGroupIDs.idSchedGroup = SchedGroups.idSchedGroup\n'
780 ' AND SchedGroups.tsExpire = \'infinity\'::TIMESTAMP\n'
781 'WHERE SchedGroups.idSchedGroup IS NULL\n');
782 aaoRows = self._oDb.fetchAll();
783 if len(aaoRows) > 0:
784 raise TMInvalidData('TestBoxLogic.addEntry missing scheduling groups: %s'
785 % (', '.join(str(aoRow[0]) for aoRow in aaoRows),));
786 return None;
787
788 def addEntry(self, oData, uidAuthor, fCommit = False):
789 # type: (TestBoxDataEx, int, bool) -> (int, int, datetime.datetime)
790 """
791 Creates a testbox in the database.
792 Returns the testbox ID, testbox generation ID and effective timestamp
793 of the created testbox on success. Throws error on failure.
794 """
795
796 #
797 # Validate. Extra work because of missing foreign key (due to history).
798 #
799 self._validateAndConvertData(oData, oData.ksValidateFor_Add);
800
801 #
802 # Do it.
803 #
804 self._oDb.callProc('TestBoxLogic_addEntry'
805 , ( uidAuthor,
806 oData.ip, # Should we allow setting the IP?
807 oData.uuidSystem,
808 oData.sName,
809 oData.sDescription,
810 oData.fEnabled,
811 oData.enmLomKind,
812 oData.ipLom,
813 oData.pctScaleTimeout,
814 oData.sComment,
815 oData.enmPendingCmd, ) );
816 (idTestBox, idGenTestBox, tsEffective) = self._oDb.fetchOne();
817
818 for oInSchedGrp in oData.aoInSchedGroups:
819 self._oDb.callProc('TestBoxLogic_addGroupEntry',
820 ( uidAuthor, idTestBox, oInSchedGrp.idSchedGroup, oInSchedGrp.iSchedPriority,) );
821
822 self._oDb.maybeCommit(fCommit);
823 return (idTestBox, idGenTestBox, tsEffective);
824
825
826 def editEntry(self, oData, uidAuthor, fCommit = False):
827 """
828 Data edit update, web UI is the primary user.
829
830 oData is either TestBoxDataEx or TestBoxData. The latter is for enabling
831 Returns the new generation ID and effective date.
832 """
833
834 #
835 # Validate.
836 #
837 self._validateAndConvertData(oData, oData.ksValidateFor_Edit);
838
839 #
840 # Get current data.
841 #
842 oOldData = TestBoxDataEx().initFromDbWithId(self._oDb, oData.idTestBox);
843
844 #
845 # Do it.
846 #
847 if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', 'aoInSchedGroups', ]
848 + TestBoxData.kasMachineSettableOnly ):
849 self._oDb.callProc('TestBoxLogic_editEntry'
850 , ( uidAuthor,
851 oData.idTestBox,
852 oData.ip, # Should we allow setting the IP?
853 oData.uuidSystem,
854 oData.sName,
855 oData.sDescription,
856 oData.fEnabled,
857 oData.enmLomKind,
858 oData.ipLom,
859 oData.pctScaleTimeout,
860 oData.sComment,
861 oData.enmPendingCmd, ));
862 (idGenTestBox, tsEffective) = self._oDb.fetchOne();
863 else:
864 idGenTestBox = oOldData.idGenTestBox;
865 tsEffective = oOldData.tsEffective;
866
867 if isinstance(oData, TestBoxDataEx):
868 # Calc in-group changes.
869 aoRemoved = list(oOldData.aoInSchedGroups);
870 aoNew = [];
871 aoUpdated = [];
872 for oNewInGroup in oData.aoInSchedGroups:
873 oOldInGroup = None;
874 for iCur, oCur in enumerate(aoRemoved):
875 if oCur.idSchedGroup == oNewInGroup.idSchedGroup:
876 oOldInGroup = aoRemoved.pop(iCur);
877 break;
878 if oOldInGroup is None:
879 aoNew.append(oNewInGroup);
880 elif oNewInGroup.iSchedPriority != oOldInGroup.iSchedPriority:
881 aoUpdated.append(oNewInGroup);
882
883 # Remove in-groups.
884 for oInGroup in aoRemoved:
885 self._oDb.callProc('TestBoxLogic_removeGroupEntry', (uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, ));
886
887 # Add new ones.
888 for oInGroup in aoNew:
889 self._oDb.callProc('TestBoxLogic_addGroupEntry',
890 ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) );
891
892 # Edit existing ones.
893 for oInGroup in aoUpdated:
894 self._oDb.callProc('TestBoxLogic_editGroupEntry',
895 ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) );
896 else:
897 assert isinstance(oData, TestBoxData);
898
899 self._oDb.maybeCommit(fCommit);
900 return (idGenTestBox, tsEffective);
901
902
903 def removeEntry(self, uidAuthor, idTestBox, fCascade = False, fCommit = False):
904 """
905 Delete test box and scheduling group associations.
906 """
907 self._oDb.callProc('TestBoxLogic_removeEntry'
908 , ( uidAuthor, idTestBox, fCascade,));
909 self._oDb.maybeCommit(fCommit);
910 return True;
911
912
913 def updateOnSignOn(self, idTestBox, idGenTestBox, sTestBoxAddr, sOs, sOsVersion, # pylint: disable=R0913,R0914
914 sCpuVendor, sCpuArch, sCpuName, lCpuRevision, cCpus, fCpuHwVirt, fCpuNestedPaging, fCpu64BitGuest,
915 fChipsetIoMmu, fRawMode, cMbMemory, cMbScratch, sReport, iTestBoxScriptRev, iPythonHexVersion):
916 """
917 Update the testbox attributes automatically on behalf of the testbox script.
918 Returns the new generation id on success, raises an exception on failure.
919 """
920 _ = idGenTestBox;
921 self._oDb.callProc('TestBoxLogic_updateOnSignOn'
922 , ( idTestBox,
923 sTestBoxAddr,
924 sOs,
925 sOsVersion,
926 sCpuVendor,
927 sCpuArch,
928 sCpuName,
929 lCpuRevision,
930 cCpus,
931 fCpuHwVirt,
932 fCpuNestedPaging,
933 fCpu64BitGuest,
934 fChipsetIoMmu,
935 fRawMode,
936 cMbMemory,
937 cMbScratch,
938 sReport,
939 iTestBoxScriptRev,
940 iPythonHexVersion,));
941 return self._oDb.fetchOne()[0];
942
943
944 def setCommand(self, idTestBox, sOldCommand, sNewCommand, uidAuthor = None, fCommit = False, sComment = None):
945 """
946 Sets or resets the pending command on a testbox.
947 Returns (idGenTestBox, tsEffective) of the new row.
948 """
949 ## @todo throw TMInFligthCollision again...
950 self._oDb.callProc('TestBoxLogic_setCommand'
951 , ( uidAuthor, idTestBox, sOldCommand, sNewCommand, sComment,));
952 aoRow = self._oDb.fetchOne();
953 self._oDb.maybeCommit(fCommit);
954 return (aoRow[0], aoRow[1]);
955
956
957 def getAll(self):
958 """
959 Retrieve list of all registered Test Box records from DB.
960 """
961 self._oDb.execute('SELECT *\n'
962 'FROM TestBoxesWithStrings\n'
963 'WHERE tsExpire=\'infinity\'::timestamp;')
964
965 aaoRows = self._oDb.fetchAll()
966 aoRet = []
967 for aoRow in aaoRows:
968 aoRet.append(TestBoxData().initFromDbRow(aoRow))
969 return aoRet
970
971
972 def cachedLookup(self, idTestBox):
973 # type: (int) -> TestBoxDataEx
974 """
975 Looks up the most recent TestBoxData object for idTestBox via
976 an object cache.
977
978 Returns a shared TestBoxDataEx object. None if not found.
979 Raises exception on DB error.
980 """
981 if self.dCache is None:
982 self.dCache = self._oDb.getCache('TestBoxData');
983 oEntry = self.dCache.get(idTestBox, None);
984 if oEntry is None:
985 fNeedNow = False;
986 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
987 'FROM TestBoxesWithStrings\n'
988 'WHERE idTestBox = %s\n'
989 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
990 , (idTestBox, ));
991 if self._oDb.getRowCount() == 0:
992 # Maybe it was deleted, try get the last entry.
993 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
994 'FROM TestBoxes\n'
995 'WHERE idTestBox = %s\n'
996 'ORDER BY tsExpire DESC\n'
997 'LIMIT 1\n'
998 , (idTestBox, ));
999 fNeedNow = True;
1000 elif self._oDb.getRowCount() > 1:
1001 raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestBox));
1002
1003 if self._oDb.getRowCount() == 1:
1004 aaoRow = self._oDb.fetchOne();
1005 if not fNeedNow:
1006 oEntry = TestBoxDataEx().initFromDbRowEx(aaoRow, self._oDb);
1007 else:
1008 oEntry = TestBoxDataEx().initFromDbRow(aaoRow);
1009 oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow = db.dbTimestampMinusOneTick(oEntry.tsExpire));
1010 self.dCache[idTestBox] = oEntry;
1011 return oEntry;
1012
1013
1014
1015 #
1016 # The virtual test sheriff interface.
1017 #
1018
1019 def hasTestBoxRecentlyBeenRebooted(self, idTestBox, cHoursBack = 2, tsNow = None):
1020 """
1021 Checks if the testbox has been rebooted in the specified time period.
1022
1023 This does not include already pending reboots, though under some
1024 circumstances it may. These being the test box entry being edited for
1025 other reasons.
1026
1027 Returns True / False.
1028 """
1029 if tsNow is None:
1030 tsNow = self._oDb.getCurrentTimestamp();
1031 self._oDb.execute('SELECT COUNT(idTestBox)\n'
1032 'FROM TestBoxes\n'
1033 'WHERE idTestBox = %s\n'
1034 ' AND tsExpire < %s\n'
1035 ' AND tsExpire >= %s - interval \'%s hours\'\n'
1036 ' AND enmPendingCmd IN (%s, %s)\n'
1037 , ( idTestBox, tsNow, tsNow, cHoursBack,
1038 TestBoxData.ksTestBoxCmd_Reboot, TestBoxData.ksTestBoxCmd_UpgradeAndReboot, ));
1039 return self._oDb.fetchOne()[0] > 0;
1040
1041
1042 def rebootTestBox(self, idTestBox, uidAuthor, sComment, sOldCommand = TestBoxData.ksTestBoxCmd_None, fCommit = False):
1043 """
1044 Issues a reboot command for the given test box.
1045 Return True on succes, False on in-flight collision.
1046 May raise DB exception on other trouble.
1047 """
1048 try:
1049 self.setCommand(idTestBox, sOldCommand, TestBoxData.ksTestBoxCmd_Reboot,
1050 uidAuthor = uidAuthor, fCommit = fCommit, sComment = sComment);
1051 except TMInFligthCollision:
1052 return False;
1053 except:
1054 raise;
1055 return True;
1056
1057
1058 def disableTestBox(self, idTestBox, uidAuthor, sComment, fCommit = False):
1059 """
1060 Disables the given test box.
1061
1062 Raises exception on trouble, without rollback.
1063 """
1064 oTestBox = TestBoxData().initFromDbWithId(self._oDb, idTestBox);
1065 if oTestBox.fEnabled:
1066 oTestBox.fEnabled = False;
1067 if sComment is not None:
1068 oTestBox.sComment = sComment;
1069 self.editEntry(oTestBox, uidAuthor = uidAuthor, fCommit = fCommit);
1070 return None;
1071
1072
1073#
1074# Unit testing.
1075#
1076
1077# pylint: disable=C0111
1078class TestBoxDataTestCase(ModelDataBaseTestCase):
1079 def setUp(self):
1080 self.aoSamples = [TestBoxData(),];
1081
1082if __name__ == '__main__':
1083 unittest.main();
1084 # not reached.
1085
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