diff options
Diffstat (limited to 'src/VBox/ValidationKit/testmanager/core/testbox.py')
-rwxr-xr-x | src/VBox/ValidationKit/testmanager/core/testbox.py | 1286 |
1 files changed, 1286 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testmanager/core/testbox.py b/src/VBox/ValidationKit/testmanager/core/testbox.py new file mode 100755 index 00000000..6686ca3b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testbox.py @@ -0,0 +1,1286 @@ +# -*- coding: utf-8 -*- +# $Id: testbox.py $ + +""" +Test Manager - TestBox. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2023 Oracle and/or its affiliates. + +This file is part of VirtualBox base platform packages, as +available from https://www.virtualbox.org. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, in version 3 of the +License. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, see <https://www.gnu.org/licenses>. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL), a copy of it is provided in the "COPYING.CDDL" file included +in the VirtualBox distribution, in which case the provisions of the +CDDL are applicable instead of those of the GPL. + +You may elect to license modified versions of this file under the +terms and conditions of either the GPL or the CDDL or both. + +SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +""" +__version__ = "$Revision: 155244 $" + + +# Standard python imports. +import copy; +import sys; +import unittest; + +# Validation Kit imports. +from testmanager.core import db; +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMInFligthCollision, \ + TMInvalidData, TMTooManyRows, TMRowNotFound, \ + ChangeLogEntry, AttributeChangeEntry, AttributeChangeEntryPre; +from testmanager.core.useraccount import UserAccountLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + xrange = range; # pylint: disable=redefined-builtin,invalid-name + + +class TestBoxInSchedGroupData(ModelDataBase): + """ + TestBox in SchedGroup data. + """ + + ksParam_idTestBox = 'TestBoxInSchedGroup_idTestBox'; + ksParam_idSchedGroup = 'TestBoxInSchedGroup_idSchedGroup'; + ksParam_tsEffective = 'TestBoxInSchedGroup_tsEffective'; + ksParam_tsExpire = 'TestBoxInSchedGroup_tsExpire'; + ksParam_uidAuthor = 'TestBoxInSchedGroup_uidAuthor'; + ksParam_iSchedPriority = 'TestBoxInSchedGroup_iSchedPriority'; + + kasAllowNullAttributes = [ 'tsEffective', 'tsExpire', 'uidAuthor', ] + + kiMin_iSchedPriority = 0; + kiMax_iSchedPriority = 32; + + kcDbColumns = 6; + + def __init__(self): + ModelDataBase.__init__(self); + self.idTestBox = None; + self.idSchedGroup = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.iSchedPriority = 16; + + def initFromDbRow(self, aoRow): + """ + Expecting the result from a query like this: + SELECT * FROM TestBoxesInSchedGroups + """ + if aoRow is None: + raise TMRowNotFound('TestBox/SchedGroup not found.'); + + self.idTestBox = aoRow[0]; + self.idSchedGroup = aoRow[1]; + self.tsEffective = aoRow[2]; + self.tsExpire = aoRow[3]; + self.uidAuthor = aoRow[4]; + self.iSchedPriority = aoRow[5]; + + return self; + +class TestBoxInSchedGroupDataEx(TestBoxInSchedGroupData): + """ + Extended version of TestBoxInSchedGroupData that contains the scheduling group. + """ + + def __init__(self): + TestBoxInSchedGroupData.__init__(self); + self.oSchedGroup = None # type: SchedGroupData + + def initFromDbRowEx(self, aoRow, oDb, tsNow = None, sPeriodBack = None): + """ + Extended version of initFromDbRow that fills in the rest from the database. + """ + from testmanager.core.schedgroup import SchedGroupData; + self.initFromDbRow(aoRow); + self.oSchedGroup = SchedGroupData().initFromDbWithId(oDb, self.idSchedGroup, tsNow, sPeriodBack); + return self; + +class TestBoxDataForSchedGroup(TestBoxInSchedGroupData): + """ + Extended version of TestBoxInSchedGroupData that adds the testbox data (if available). + Used by TestBoxLogic.fetchForSchedGroup + """ + + def __init__(self): + TestBoxInSchedGroupData.__init__(self); + self.oTestBox = None # type: TestBoxData + + def initFromDbRow(self, aoRow): + """ + The row is: TestBoxesInSchedGroups.*, TestBoxesWithStrings.* + """ + TestBoxInSchedGroupData.initFromDbRow(self, aoRow); + if aoRow[self.kcDbColumns]: + self.oTestBox = TestBoxData().initFromDbRow(aoRow[self.kcDbColumns:]); + else: + self.oTestBox = None; + return self; + + def getDataAttributes(self): + asAttributes = TestBoxInSchedGroupData.getDataAttributes(self); + asAttributes.remove('oTestBox'); + return asAttributes; + + def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other): + dErrors = TestBoxInSchedGroupData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor); + if self.ksParam_idTestBox not in dErrors: + self.oTestBox = TestBoxData(); + try: + self.oTestBox.initFromDbWithId(oDb, self.idTestBox); + except Exception as oXcpt: + self.oTestBox = TestBoxData() + dErrors[self.ksParam_idTestBox] = str(oXcpt); + return dErrors; + + +# pylint: disable=invalid-name +class TestBoxData(ModelDataBase): # pylint: disable=too-many-instance-attributes + """ + TestBox Data. + """ + + ## LomKind_T + ksLomKind_None = 'none'; + ksLomKind_ILOM = 'ilom'; + ksLomKind_ELOM = 'elom'; + ksLomKind_AppleXserveLom = 'apple-xserver-lom'; + kasLomKindValues = [ ksLomKind_None, ksLomKind_ILOM, ksLomKind_ELOM, ksLomKind_AppleXserveLom]; + kaoLomKindDescs = \ + [ + ( ksLomKind_None, 'None', ''), + ( ksLomKind_ILOM, 'ILOM', ''), + ( ksLomKind_ELOM, 'ELOM', ''), + ( ksLomKind_AppleXserveLom, 'Apple Xserve LOM', ''), + ]; + + + ## TestBoxCmd_T + ksTestBoxCmd_None = 'none'; + ksTestBoxCmd_Abort = 'abort'; + ksTestBoxCmd_Reboot = 'reboot'; + ksTestBoxCmd_Upgrade = 'upgrade'; + ksTestBoxCmd_UpgradeAndReboot = 'upgrade-and-reboot'; + ksTestBoxCmd_Special = 'special'; + kasTestBoxCmdValues = [ ksTestBoxCmd_None, ksTestBoxCmd_Abort, ksTestBoxCmd_Reboot, ksTestBoxCmd_Upgrade, + ksTestBoxCmd_UpgradeAndReboot, ksTestBoxCmd_Special]; + kaoTestBoxCmdDescs = \ + [ + ( ksTestBoxCmd_None, 'None', ''), + ( ksTestBoxCmd_Abort, 'Abort current test', ''), + ( ksTestBoxCmd_Reboot, 'Reboot TestBox', ''), + ( ksTestBoxCmd_Upgrade, 'Upgrade TestBox Script', ''), + ( ksTestBoxCmd_UpgradeAndReboot, 'Upgrade TestBox Script and reboot', ''), + ( ksTestBoxCmd_Special, 'Special (reserved)', ''), + ]; + + + ksIdAttr = 'idTestBox'; + ksIdGenAttr = 'idGenTestBox'; + + ksParam_idTestBox = 'TestBox_idTestBox'; + ksParam_tsEffective = 'TestBox_tsEffective'; + ksParam_tsExpire = 'TestBox_tsExpire'; + ksParam_uidAuthor = 'TestBox_uidAuthor'; + ksParam_idGenTestBox = 'TestBox_idGenTestBox'; + ksParam_ip = 'TestBox_ip'; + ksParam_uuidSystem = 'TestBox_uuidSystem'; + ksParam_sName = 'TestBox_sName'; + ksParam_sDescription = 'TestBox_sDescription'; + ksParam_fEnabled = 'TestBox_fEnabled'; + ksParam_enmLomKind = 'TestBox_enmLomKind'; + ksParam_ipLom = 'TestBox_ipLom'; + ksParam_pctScaleTimeout = 'TestBox_pctScaleTimeout'; + ksParam_sComment = 'TestBox_sComment'; + ksParam_sOs = 'TestBox_sOs'; + ksParam_sOsVersion = 'TestBox_sOsVersion'; + ksParam_sCpuVendor = 'TestBox_sCpuVendor'; + ksParam_sCpuArch = 'TestBox_sCpuArch'; + ksParam_sCpuName = 'TestBox_sCpuName'; + ksParam_lCpuRevision = 'TestBox_lCpuRevision'; + ksParam_cCpus = 'TestBox_cCpus'; + ksParam_fCpuHwVirt = 'TestBox_fCpuHwVirt'; + ksParam_fCpuNestedPaging = 'TestBox_fCpuNestedPaging'; + ksParam_fCpu64BitGuest = 'TestBox_fCpu64BitGuest'; + ksParam_fChipsetIoMmu = 'TestBox_fChipsetIoMmu'; + ksParam_fRawMode = 'TestBox_fRawMode'; + ksParam_cMbMemory = 'TestBox_cMbMemory'; + ksParam_cMbScratch = 'TestBox_cMbScratch'; + ksParam_sReport = 'TestBox_sReport'; + ksParam_iTestBoxScriptRev = 'TestBox_iTestBoxScriptRev'; + ksParam_iPythonHexVersion = 'TestBox_iPythonHexVersion'; + ksParam_enmPendingCmd = 'TestBox_enmPendingCmd'; + + kasInternalAttributes = [ 'idStrDescription', 'idStrComment', 'idStrOs', 'idStrOsVersion', 'idStrCpuVendor', + 'idStrCpuArch', 'idStrCpuName', 'idStrReport', ]; + kasMachineSettableOnly = [ 'sOs', 'sOsVersion', 'sCpuVendor', 'sCpuArch', 'sCpuName', 'lCpuRevision', 'cCpus', + 'fCpuHwVirt', 'fCpuNestedPaging', 'fCpu64BitGuest', 'fChipsetIoMmu', 'fRawMode', + 'cMbMemory', 'cMbScratch', 'sReport', 'iTestBoxScriptRev', 'iPythonHexVersion', ]; + kasAllowNullAttributes = ['idTestBox', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestBox', 'sDescription', + 'ipLom', 'sComment', ] + kasMachineSettableOnly + kasInternalAttributes; + + kasValidValues_enmLomKind = kasLomKindValues; + kasValidValues_enmPendingCmd = kasTestBoxCmdValues; + kiMin_pctScaleTimeout = 11; + kiMax_pctScaleTimeout = 19999; + kcchMax_sReport = 65535; + + kcDbColumns = 40; # including the 7 string joins columns + + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestBox = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.idGenTestBox = None; + self.ip = None; + self.uuidSystem = None; + self.sName = None; + self.idStrDescription = None; + self.fEnabled = False; + self.enmLomKind = self.ksLomKind_None; + self.ipLom = None; + self.pctScaleTimeout = 100; + self.idStrComment = None; + self.idStrOs = None; + self.idStrOsVersion = None; + self.idStrCpuVendor = None; + self.idStrCpuArch = None; + self.idStrCpuName = None; + self.lCpuRevision = None; + self.cCpus = 1; + self.fCpuHwVirt = False; + self.fCpuNestedPaging = False; + self.fCpu64BitGuest = False; + self.fChipsetIoMmu = False; + self.fRawMode = None; + self.cMbMemory = 1; + self.cMbScratch = 0; + self.idStrReport = None; + self.iTestBoxScriptRev = 0; + self.iPythonHexVersion = 0; + self.enmPendingCmd = self.ksTestBoxCmd_None; + # String table values. + self.sDescription = None; + self.sComment = None; + self.sOs = None; + self.sOsVersion = None; + self.sCpuVendor = None; + self.sCpuArch = None; + self.sCpuName = None; + self.sReport = None; + + def initFromDbRow(self, aoRow): + """ + Internal worker for initFromDbWithId and initFromDbWithGenId as well as + from TestBoxLogic. Expecting the result from a query like this: + SELECT TestBoxesWithStrings.* FROM TestBoxesWithStrings + """ + if aoRow is None: + raise TMRowNotFound('TestBox not found.'); + + self.idTestBox = aoRow[0]; + self.tsEffective = aoRow[1]; + self.tsExpire = aoRow[2]; + self.uidAuthor = aoRow[3]; + self.idGenTestBox = aoRow[4]; + self.ip = aoRow[5]; + self.uuidSystem = aoRow[6]; + self.sName = aoRow[7]; + self.idStrDescription = aoRow[8]; + self.fEnabled = aoRow[9]; + self.enmLomKind = aoRow[10]; + self.ipLom = aoRow[11]; + self.pctScaleTimeout = aoRow[12]; + self.idStrComment = aoRow[13]; + self.idStrOs = aoRow[14]; + self.idStrOsVersion = aoRow[15]; + self.idStrCpuVendor = aoRow[16]; + self.idStrCpuArch = aoRow[17]; + self.idStrCpuName = aoRow[18]; + self.lCpuRevision = aoRow[19]; + self.cCpus = aoRow[20]; + self.fCpuHwVirt = aoRow[21]; + self.fCpuNestedPaging = aoRow[22]; + self.fCpu64BitGuest = aoRow[23]; + self.fChipsetIoMmu = aoRow[24]; + self.fRawMode = aoRow[25]; + self.cMbMemory = aoRow[26]; + self.cMbScratch = aoRow[27]; + self.idStrReport = aoRow[28]; + self.iTestBoxScriptRev = aoRow[29]; + self.iPythonHexVersion = aoRow[30]; + self.enmPendingCmd = aoRow[31]; + + # String table values. + if len(aoRow) > 32: + self.sDescription = aoRow[32]; + self.sComment = aoRow[33]; + self.sOs = aoRow[34]; + self.sOsVersion = aoRow[35]; + self.sCpuVendor = aoRow[36]; + self.sCpuArch = aoRow[37]; + self.sCpuName = aoRow[38]; + self.sReport = aoRow[39]; + + return self; + + def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE idTestBox = %s\n' + , ( idTestBox, ), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestBox=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestBox, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None): + """ + Initialize the object from the database. + """ + _ = tsNow; # Only useful for extended data classes. + oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE idGenTestBox = %s\n' + , (idGenTestBox, ) ); + return self.initFromDbRow(oDb.fetchOne()); + + def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other): + # Override to do extra ipLom checks. + dErrors = ModelDataBase._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor); + if self.ksParam_ipLom not in dErrors \ + and self.ksParam_enmLomKind not in dErrors \ + and self.enmLomKind != self.ksLomKind_None \ + and self.ipLom is None: + dErrors[self.ksParam_ipLom] = 'Light-out-management IP is mandatory and a LOM is selected.' + return dErrors; + + @staticmethod + def formatPythonVersionEx(iPythonHexVersion): + """ Unbuttons the version number and formats it as a version string. """ + if iPythonHexVersion is None: + return 'N/A'; + return 'v%d.%d.%d.%d' \ + % ( iPythonHexVersion >> 24, + (iPythonHexVersion >> 16) & 0xff, + (iPythonHexVersion >> 8) & 0xff, + iPythonHexVersion & 0xff); + + def formatPythonVersion(self): + """ Unbuttons the version number and formats it as a version string. """ + return self.formatPythonVersionEx(self.iPythonHexVersion); + + + @staticmethod + def getCpuFamilyEx(lCpuRevision): + """ Returns the CPU family for a x86 or amd64 testboxes.""" + if lCpuRevision is None: + return 0; + return (lCpuRevision >> 24 & 0xff); + + def getCpuFamily(self): + """ Returns the CPU family for a x86 or amd64 testboxes.""" + return self.getCpuFamilyEx(self.lCpuRevision); + + @staticmethod + def getCpuModelEx(lCpuRevision): + """ Returns the CPU model for a x86 or amd64 testboxes.""" + if lCpuRevision is None: + return 0; + return (lCpuRevision >> 8 & 0xffff); + + def getCpuModel(self): + """ Returns the CPU model for a x86 or amd64 testboxes.""" + return self.getCpuModelEx(self.lCpuRevision); + + @staticmethod + def getCpuSteppingEx(lCpuRevision): + """ Returns the CPU stepping for a x86 or amd64 testboxes.""" + if lCpuRevision is None: + return 0; + return (lCpuRevision & 0xff); + + def getCpuStepping(self): + """ Returns the CPU stepping for a x86 or amd64 testboxes.""" + return self.getCpuSteppingEx(self.lCpuRevision); + + + # The following is a translation of the g_aenmIntelFamily06 array in CPUMR3CpuId.cpp: + kdIntelFamily06 = { + 0x00: 'P6', + 0x01: 'P6', + 0x03: 'P6_II', + 0x05: 'P6_II', + 0x06: 'P6_II', + 0x07: 'P6_III', + 0x08: 'P6_III', + 0x09: 'P6_M_Banias', + 0x0a: 'P6_III', + 0x0b: 'P6_III', + 0x0d: 'P6_M_Dothan', + 0x0e: 'Core_Yonah', + 0x0f: 'Core2_Merom', + 0x15: 'P6_M_Dothan', + 0x16: 'Core2_Merom', + 0x17: 'Core2_Penryn', + 0x1a: 'Core7_Nehalem', + 0x1c: 'Atom_Bonnell', + 0x1d: 'Core2_Penryn', + 0x1e: 'Core7_Nehalem', + 0x1f: 'Core7_Nehalem', + 0x25: 'Core7_Westmere', + 0x26: 'Atom_Lincroft', + 0x27: 'Atom_Saltwell', + 0x2a: 'Core7_SandyBridge', + 0x2c: 'Core7_Westmere', + 0x2d: 'Core7_SandyBridge', + 0x2e: 'Core7_Nehalem', + 0x2f: 'Core7_Westmere', + 0x35: 'Atom_Saltwell', + 0x36: 'Atom_Saltwell', + 0x37: 'Atom_Silvermont', + 0x3a: 'Core7_IvyBridge', + 0x3c: 'Core7_Haswell', + 0x3d: 'Core7_Broadwell', + 0x3e: 'Core7_IvyBridge', + 0x3f: 'Core7_Haswell', + 0x45: 'Core7_Haswell', + 0x46: 'Core7_Haswell', + 0x47: 'Core7_Broadwell', + 0x4a: 'Atom_Silvermont', + 0x4c: 'Atom_Airmount', + 0x4d: 'Atom_Silvermont', + 0x4e: 'Core7_Skylake', + 0x4f: 'Core7_Broadwell', + 0x55: 'Core7_Skylake', + 0x56: 'Core7_Broadwell', + 0x5a: 'Atom_Silvermont', + 0x5c: 'Atom_Goldmont', + 0x5d: 'Atom_Silvermont', + 0x5e: 'Core7_Skylake', + 0x66: 'Core7_Cannonlake', + }; + # Also from CPUMR3CpuId.cpp, but the switch. + kdIntelFamily15 = { + 0x00: 'NB_Willamette', + 0x01: 'NB_Willamette', + 0x02: 'NB_Northwood', + 0x03: 'NB_Prescott', + 0x04: 'NB_Prescott2M', + 0x05: 'NB_Unknown', + 0x06: 'NB_CedarMill', + 0x07: 'NB_Gallatin', + }; + + @staticmethod + def queryCpuMicroarchEx(lCpuRevision, sCpuVendor): + """ Try guess the microarch name for the cpu. Returns None if we cannot. """ + if lCpuRevision is None or sCpuVendor is None: + return None; + uFam = TestBoxData.getCpuFamilyEx(lCpuRevision); + uMod = TestBoxData.getCpuModelEx(lCpuRevision); + if sCpuVendor == 'GenuineIntel': + if uFam == 6: + return TestBoxData.kdIntelFamily06.get(uMod, None); + if uFam == 15: + return TestBoxData.kdIntelFamily15.get(uMod, None); + elif sCpuVendor == 'AuthenticAMD': + if uFam == 0xf: + if uMod < 0x10: return 'K8_130nm'; + if 0x60 <= uMod < 0x80: return 'K8_65nm'; + if uMod >= 0x40: return 'K8_90nm_AMDV'; + if uMod in [0x21, 0x23, 0x2b, 0x37, 0x3f]: return 'K8_90nm_DualCore'; + return 'AMD_K8_90nm'; + if uFam == 0x10: return 'K10'; + if uFam == 0x11: return 'K10_Lion'; + if uFam == 0x12: return 'K10_Llano'; + if uFam == 0x14: return 'Bobcat'; + if uFam == 0x15: + if uMod <= 0x01: return 'Bulldozer'; + if uMod in [0x02, 0x10, 0x13]: return 'Piledriver'; + return None; + if uFam == 0x16: + return 'Jaguar'; + elif sCpuVendor == 'CentaurHauls': + if uFam == 0x05: + if uMod == 0x01: return 'Centaur_C6'; + if uMod == 0x04: return 'Centaur_C6'; + if uMod == 0x08: return 'Centaur_C2'; + if uMod == 0x09: return 'Centaur_C3'; + if uFam == 0x06: + if uMod == 0x05: return 'VIA_C3_M2'; + if uMod == 0x06: return 'VIA_C3_C5A'; + if uMod == 0x07: return 'VIA_C3_C5B' if TestBoxData.getCpuSteppingEx(lCpuRevision) < 8 else 'VIA_C3_C5C'; + if uMod == 0x08: return 'VIA_C3_C5N'; + if uMod == 0x09: return 'VIA_C3_C5XL' if TestBoxData.getCpuSteppingEx(lCpuRevision) < 8 else 'VIA_C3_C5P'; + if uMod == 0x0a: return 'VIA_C7_C5J'; + if uMod == 0x0f: return 'VIA_Isaiah'; + elif sCpuVendor == ' Shanghai ': + if uFam == 0x07: + if uMod == 0x0b: return 'Shanghai_KX-5000'; + return None; + + def queryCpuMicroarch(self): + """ Try guess the microarch name for the cpu. Returns None if we cannot. """ + return self.queryCpuMicroarchEx(self.lCpuRevision, self.sCpuVendor); + + @staticmethod + def getPrettyCpuVersionEx(lCpuRevision, sCpuVendor): + """ Pretty formatting of the family/model/stepping with microarch optimizations. """ + if lCpuRevision is None or sCpuVendor is None: + return u'<none>'; + sMarch = TestBoxData.queryCpuMicroarchEx(lCpuRevision, sCpuVendor); + if sMarch is not None: + return '%s %02x:%x' \ + % (sMarch, TestBoxData.getCpuModelEx(lCpuRevision), TestBoxData.getCpuSteppingEx(lCpuRevision)); + return 'fam%02X m%02X s%02X' \ + % ( TestBoxData.getCpuFamilyEx(lCpuRevision), TestBoxData.getCpuModelEx(lCpuRevision), + TestBoxData.getCpuSteppingEx(lCpuRevision)); + + def getPrettyCpuVersion(self): + """ Pretty formatting of the family/model/stepping with microarch optimizations. """ + return self.getPrettyCpuVersionEx(self.lCpuRevision, self.sCpuVendor); + + def getArchBitString(self): + """ Returns 32-bit, 64-bit, <none>, or sCpuArch. """ + if self.sCpuArch is None: + return '<none>'; + if self.sCpuArch in [ 'x86',]: + return '32-bit'; + if self.sCpuArch in [ 'amd64',]: + return '64-bit'; + return self.sCpuArch; + + def getPrettyCpuVendor(self): + """ Pretty vendor name.""" + if self.sCpuVendor is None: + return '<none>'; + if self.sCpuVendor == 'GenuineIntel': return 'Intel'; + if self.sCpuVendor == 'AuthenticAMD': return 'AMD'; + if self.sCpuVendor == 'CentaurHauls': return 'VIA'; + if self.sCpuVendor == ' Shanghai ': return 'Shanghai'; + return self.sCpuVendor; + + +class TestBoxDataEx(TestBoxData): + """ + TestBox data. + """ + + ksParam_aoInSchedGroups = 'TestBox_aoInSchedGroups'; + + # Use [] instead of None. + kasAltArrayNull = [ 'aoInSchedGroups', ]; + + ## Helper parameter containing the comma separated list with the IDs of + # potential members found in the parameters. + ksParam_aidSchedGroups = 'TestBoxDataEx_aidSchedGroups'; + + def __init__(self): + TestBoxData.__init__(self); + self.aoInSchedGroups = [] # type: list[TestBoxInSchedGroupData] + + def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None): + """ + Worker shared by the initFromDb* methods. + Returns self. Raises exception if no row or database error. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM TestBoxesInSchedGroups\n' + 'WHERE idTestBox = %s\n' + , (self.idTestBox,), tsNow, sPeriodBack) + + 'ORDER BY idSchedGroup\n' ); + self.aoInSchedGroups = []; + for aoRow in oDb.fetchAll(): + self.aoInSchedGroups.append(TestBoxInSchedGroupDataEx().initFromDbRowEx(aoRow, oDb, tsNow, sPeriodBack)); + return self; + + def initFromDbRowEx(self, aoRow, oDb, tsNow = None): + """ + Reinitialize from a SELECT * FROM TestBoxesWithStrings row. Will query the + necessary additional data from oDb using tsNow. + Returns self. Raises exception if no row or database error. + """ + TestBoxData.initFromDbRow(self, aoRow); + return self._initExtraMembersFromDb(oDb, tsNow); + + def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + TestBoxData.initFromDbWithId(self, oDb, idTestBox, tsNow, sPeriodBack); + return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack); + + def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None): + """ + Initialize the object from the database. + """ + TestBoxData.initFromDbWithGenId(self, oDb, idGenTestBox); + if tsNow is None and not oDb.isTsInfinity(self.tsExpire): + tsNow = self.tsEffective; + return self._initExtraMembersFromDb(oDb, tsNow); + + def getAttributeParamNullValues(self, sAttr): # Necessary? + if sAttr in ['aoInSchedGroups', ]: + return [[], '']; + return TestBoxData.getAttributeParamNullValues(self, sAttr); + + def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict): + """ + For dealing with the in-scheduling-group list. + """ + if sAttr != 'aoInSchedGroups': + return TestBoxData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict); + + aoNewValues = []; + aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = []); + asIds = oDisp.getStringParam(self.ksParam_aidSchedGroups, sDefault = '').split(','); + for idSchedGroup in asIds: + try: idSchedGroup = int(idSchedGroup); + except: pass; + oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestBoxDataEx.ksParam_aoInSchedGroups, idSchedGroup,)) + oMember = TestBoxInSchedGroupData().initFromParams(oDispWrapper, fStrict = False); + if idSchedGroup in aidSelected: + aoNewValues.append(oMember); + return aoNewValues; + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): # pylint: disable=too-many-locals + """ + Validate special arrays and requirement expressions. + + Some special needs for the in-scheduling-group list. + """ + if sAttr != 'aoInSchedGroups': + return TestBoxData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + + asErrors = []; + aoNewValues = []; + + # Note! We'll be returning an error dictionary instead of an string here. + dErrors = {}; + + # HACK ALERT! idTestBox might not have been validated and converted yet, but we need detect + # adding so we can ignore idTestBox being NIL when validating group memberships. + ## @todo make base.py pass us the ksValidateFor_Xxxx value. + fIsAdding = bool(self.idTestBox in [ None, -1, '-1', 'None', '' ]) + + for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups): + oInSchedGroup = copy.copy(oInSchedGroup); + oInSchedGroup.idTestBox = self.idTestBox; + if fIsAdding: + dCurErrors = oInSchedGroup.validateAndConvertEx(['idTestBox',] + oInSchedGroup.kasAllowNullAttributes, + oDb, ModelDataBase.ksValidateFor_Add); + else: + dCurErrors = oInSchedGroup.validateAndConvert(oDb, ModelDataBase.ksValidateFor_Other); + if not dCurErrors: + pass; ## @todo figure out the ID? + else: + asErrors = []; + for sKey in dCurErrors: + asErrors.append('%s: %s' % (sKey[len('TestBoxInSchedGroup_'):], + dCurErrors[sKey] + ('{%s}' % self.idTestBox))) + dErrors[iInGrp] = '<br>\n'.join(asErrors) + aoNewValues.append(oInSchedGroup); + + for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups): + for iInGrp2 in xrange(iInGrp + 1, len(self.aoInSchedGroups)): + if self.aoInSchedGroups[iInGrp2].idSchedGroup == oInSchedGroup.idSchedGroup: + sMsg = 'Duplicate scheduling group #%s".' % (oInSchedGroup.idSchedGroup,); + if iInGrp in dErrors: dErrors[iInGrp] += '<br>\n' + sMsg; + else: dErrors[iInGrp] = sMsg; + if iInGrp2 in dErrors: dErrors[iInGrp2] += '<br>\n' + sMsg; + else: dErrors[iInGrp2] = sMsg; + break; + + return (aoNewValues, dErrors if dErrors else None); + + +class TestBoxLogic(ModelLogicBase): + """ + TestBox logic. + """ + + kiSortColumn_sName = 1; + kiSortColumn_sOs = 2; + kiSortColumn_sOsVersion = 3; + kiSortColumn_sCpuVendor = 4; + kiSortColumn_sCpuArch = 5; + kiSortColumn_lCpuRevision = 6; + kiSortColumn_cCpus = 7; + kiSortColumn_cMbMemory = 8; + kiSortColumn_cMbScratch = 9; + kiSortColumn_fCpuNestedPaging = 10; + kiSortColumn_iTestBoxScriptRev = 11; + kiSortColumn_iPythonHexVersion = 12; + kiSortColumn_enmPendingCmd = 13; + kiSortColumn_fEnabled = 14; + kiSortColumn_enmState = 15; + kiSortColumn_tsUpdated = 16; + kcMaxSortColumns = 17; + kdSortColumnMap = { + 0: 'TestBoxesWithStrings.sName', + kiSortColumn_sName: "regexp_replace(TestBoxesWithStrings.sName,'[0-9]*', '', 'g'), " \ + "RIGHT(CONCAT(regexp_replace(TestBoxesWithStrings.sName,'[^0-9]*','', 'g'),'0'),8)::int", + -kiSortColumn_sName: "regexp_replace(TestBoxesWithStrings.sName,'[0-9]*', '', 'g') DESC, " \ + "RIGHT(CONCAT(regexp_replace(TestBoxesWithStrings.sName,'[^0-9]*','', 'g'),'0'),8)::int DESC", + kiSortColumn_sOs: 'TestBoxesWithStrings.sOs', + -kiSortColumn_sOs: 'TestBoxesWithStrings.sOs DESC', + kiSortColumn_sOsVersion: 'TestBoxesWithStrings.sOsVersion', + -kiSortColumn_sOsVersion: 'TestBoxesWithStrings.sOsVersion DESC', + kiSortColumn_sCpuVendor: 'TestBoxesWithStrings.sCpuVendor', + -kiSortColumn_sCpuVendor: 'TestBoxesWithStrings.sCpuVendor DESC', + kiSortColumn_sCpuArch: 'TestBoxesWithStrings.sCpuArch', + -kiSortColumn_sCpuArch: 'TestBoxesWithStrings.sCpuArch DESC', + kiSortColumn_lCpuRevision: 'TestBoxesWithStrings.lCpuRevision', + -kiSortColumn_lCpuRevision: 'TestBoxesWithStrings.lCpuRevision DESC', + kiSortColumn_cCpus: 'TestBoxesWithStrings.cCpus', + -kiSortColumn_cCpus: 'TestBoxesWithStrings.cCpus DESC', + kiSortColumn_cMbMemory: 'TestBoxesWithStrings.cMbMemory', + -kiSortColumn_cMbMemory: 'TestBoxesWithStrings.cMbMemory DESC', + kiSortColumn_cMbScratch: 'TestBoxesWithStrings.cMbScratch', + -kiSortColumn_cMbScratch: 'TestBoxesWithStrings.cMbScratch DESC', + kiSortColumn_fCpuNestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging', + -kiSortColumn_fCpuNestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging DESC', + kiSortColumn_iTestBoxScriptRev: 'TestBoxesWithStrings.iTestBoxScriptRev', + -kiSortColumn_iTestBoxScriptRev: 'TestBoxesWithStrings.iTestBoxScriptRev DESC', + kiSortColumn_iPythonHexVersion: 'TestBoxesWithStrings.iPythonHexVersion', + -kiSortColumn_iPythonHexVersion: 'TestBoxesWithStrings.iPythonHexVersion DESC', + kiSortColumn_enmPendingCmd: 'TestBoxesWithStrings.enmPendingCmd', + -kiSortColumn_enmPendingCmd: 'TestBoxesWithStrings.enmPendingCmd DESC', + kiSortColumn_fEnabled: 'TestBoxesWithStrings.fEnabled', + -kiSortColumn_fEnabled: 'TestBoxesWithStrings.fEnabled DESC', + kiSortColumn_enmState: 'TestBoxStatuses.enmState', + -kiSortColumn_enmState: 'TestBoxStatuses.enmState DESC', + kiSortColumn_tsUpdated: 'TestBoxStatuses.tsUpdated', + -kiSortColumn_tsUpdated: 'TestBoxStatuses.tsUpdated DESC', + }; + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb); + self.dCache = None; + + def tryFetchTestBoxByUuid(self, sTestBoxUuid): + """ + Tries to fetch a testbox by its UUID alone. + """ + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE uuidSystem = %s\n' + ' AND tsExpire = \'infinity\'::timestamp\n' + 'ORDER BY tsEffective DESC\n', + (sTestBoxUuid,)); + if self._oDb.getRowCount() == 0: + return None; + if self._oDb.getRowCount() != 1: + raise TMTooManyRows('Database integrity error: %u hits' % (self._oDb.getRowCount(),)); + oData = TestBoxData(); + oData.initFromDbRow(self._oDb.fetchOne()); + return oData; + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches testboxes for listing. + + Returns an array (list) of TestBoxDataForListing items, empty list if none. + The TestBoxDataForListing instances are just TestBoxData with two extra + members, an extra oStatus member that is either None or a TestBoxStatusData + instance, and a member tsCurrent holding CURRENT_TIMESTAMP. + + Raises exception on error. + """ + class TestBoxDataForListing(TestBoxDataEx): + """ We add two members for the listing. """ + def __init__(self): + TestBoxDataEx.__init__(self); + self.tsCurrent = None; # CURRENT_TIMESTAMP + self.oStatus = None # type: TestBoxStatusData + + from testmanager.core.testboxstatus import TestBoxStatusData; + + if not aiSortColumns: + aiSortColumns = [self.kiSortColumn_sName,]; + + if tsNow is None: + self._oDb.execute('SELECT TestBoxesWithStrings.*,\n' + ' TestBoxStatuses.*\n' + 'FROM TestBoxesWithStrings\n' + ' LEFT OUTER JOIN TestBoxStatuses\n' + ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n' + 'WHERE TestBoxesWithStrings.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY ' + (', '.join([self.kdSortColumnMap[i] for i in aiSortColumns])) + '\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT TestBoxesWithStrings.*,\n' + ' TestBoxStatuses.*\n' + 'FROM TestBoxesWithStrings\n' + ' LEFT OUTER JOIN TestBoxStatuses\n' + ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY ' + (', '.join([self.kdSortColumnMap[i] for i in aiSortColumns])) + '\n' + 'LIMIT %s OFFSET %s\n' + , ( tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = []; + for aoOne in self._oDb.fetchAll(): + oTestBox = TestBoxDataForListing().initFromDbRowEx(aoOne, self._oDb, tsNow); + oTestBox.tsCurrent = self._oDb.getCurrentTimestamp(); + if aoOne[TestBoxData.kcDbColumns] is not None: + oTestBox.oStatus = TestBoxStatusData().initFromDbRow(aoOne[TestBoxData.kcDbColumns:]); + aoRows.append(oTestBox); + return aoRows; + + def fetchForSchedGroup(self, idSchedGroup, tsNow, aiSortColumns = None): + """ + Fetches testboxes for listing. + + Returns an array (list) of TestBoxDataForSchedGroup items, empty list if none. + + Raises exception on error. + """ + if not aiSortColumns: + aiSortColumns = [self.kiSortColumn_sName,]; + asSortColumns = [self.kdSortColumnMap[i] for i in aiSortColumns]; + asSortColumns.append('TestBoxesInSchedGroups.idTestBox'); + + if tsNow is None: + self._oDb.execute(''' +SELECT TestBoxesInSchedGroups.*, + TestBoxesWithStrings.* +FROM TestBoxesInSchedGroups + LEFT OUTER JOIN TestBoxesWithStrings + ON TestBoxesWithStrings.idTestBox = TestBoxesInSchedGroups.idTestBox + AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP +WHERE TestBoxesInSchedGroups.idSchedGroup = %s + AND TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP +ORDER BY ''' + ', '.join(asSortColumns), (idSchedGroup, )); + else: + self._oDb.execute(''' +SELECT TestBoxesInSchedGroups.*, + TestBoxesWithStrings.* +FROM TestBoxesInSchedGroups + LEFT OUTER JOIN TestBoxesWithStrings + ON TestBoxesWithStrings.idTestBox = TestBoxesInSchedGroups.idTestBox + AND TestBoxesWithStrings.tsExpire > %s + AND TestBoxesWithStrings.tsEffective <= %s +WHERE TestBoxesInSchedGroups.idSchedGroup = %s + AND TestBoxesInSchedGroups.tsExpire > %s + AND TestBoxesInSchedGroups.tsEffective <= %s +ORDER BY ''' + ', '.join(asSortColumns), (tsNow, tsNow, idSchedGroup, tsNow, tsNow, )); + + aoRows = []; + for aoOne in self._oDb.fetchAll(): + aoRows.append(TestBoxDataForSchedGroup().initFromDbRow(aoOne)); + return aoRows; + + def fetchForChangeLog(self, idTestBox, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals + """ + Fetches change log entries for a testbox. + + Returns an array of ChangeLogEntry instance and an indicator whether + there are more entries. + Raises exception on error. + """ + + ## @todo calc changes to scheduler group! + + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE TestBoxesWithStrings.tsEffective <= %s\n' + ' AND TestBoxesWithStrings.idTestBox = %s\n' + 'ORDER BY TestBoxesWithStrings.tsExpire DESC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, idTestBox, cMaxRows + 1, iStart,)); + + aoRows = []; + for aoDbRow in self._oDb.fetchAll(): + aoRows.append(TestBoxData().initFromDbRow(aoDbRow)); + + # Calculate the changes. + aoEntries = []; + for i in xrange(0, len(aoRows) - 1): + oNew = aoRows[i]; + oOld = aoRows[i + 1]; + aoChanges = []; + for sAttr in oNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]: + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr != oNewAttr: + if sAttr == 'sReport': + aoChanges.append(AttributeChangeEntryPre(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + else: + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges)); + + # If we're at the end of the log, add the initial entry. + if len(aoRows) <= cMaxRows and aoRows: + oNew = aoRows[-1]; + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, [])); + + UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries); + return (aoEntries, len(aoRows) > cMaxRows); + + def _validateAndConvertData(self, oData, enmValidateFor): + # type: (TestBoxDataEx, str) -> None + """ + Helper for addEntry and editEntry that validates the scheduling group IDs in + addtion to what's covered by the default validateAndConvert of the data object. + + Raises exception on invalid input. + """ + dDataErrors = oData.validateAndConvert(self._oDb, enmValidateFor); + if dDataErrors: + raise TMInvalidData('TestBoxLogic.addEntry: %s' % (dDataErrors,)); + if isinstance(oData, TestBoxDataEx): + if oData.aoInSchedGroups: + sSchedGrps = ', '.join('(%s)' % oCur.idSchedGroup for oCur in oData.aoInSchedGroups); + self._oDb.execute('SELECT SchedGroupIDs.idSchedGroup\n' + 'FROM (VALUES ' + sSchedGrps + ' ) AS SchedGroupIDs(idSchedGroup)\n' + ' LEFT OUTER JOIN SchedGroups\n' + ' ON SchedGroupIDs.idSchedGroup = SchedGroups.idSchedGroup\n' + ' AND SchedGroups.tsExpire = \'infinity\'::TIMESTAMP\n' + 'WHERE SchedGroups.idSchedGroup IS NULL\n'); + aaoRows = self._oDb.fetchAll(); + if aaoRows: + raise TMInvalidData('TestBoxLogic.addEntry missing scheduling groups: %s' + % (', '.join(str(aoRow[0]) for aoRow in aaoRows),)); + return None; + + def addEntry(self, oData, uidAuthor, fCommit = False): + # type: (TestBoxDataEx, int, bool) -> (int, int, datetime.datetime) + """ + Creates a testbox in the database. + Returns the testbox ID, testbox generation ID and effective timestamp + of the created testbox on success. Throws error on failure. + """ + + # + # Validate. Extra work because of missing foreign key (due to history). + # + self._validateAndConvertData(oData, oData.ksValidateFor_Add); + + # + # Do it. + # + self._oDb.callProc('TestBoxLogic_addEntry' + , ( uidAuthor, + oData.ip, # Should we allow setting the IP? + oData.uuidSystem, + oData.sName, + oData.sDescription, + oData.fEnabled, + oData.enmLomKind, + oData.ipLom, + oData.pctScaleTimeout, + oData.sComment, + oData.enmPendingCmd, ) ); + (idTestBox, idGenTestBox, tsEffective) = self._oDb.fetchOne(); + + for oInSchedGrp in oData.aoInSchedGroups: + self._oDb.callProc('TestBoxLogic_addGroupEntry', + ( uidAuthor, idTestBox, oInSchedGrp.idSchedGroup, oInSchedGrp.iSchedPriority,) ); + + self._oDb.maybeCommit(fCommit); + return (idTestBox, idGenTestBox, tsEffective); + + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Data edit update, web UI is the primary user. + + oData is either TestBoxDataEx or TestBoxData. The latter is for enabling + Returns the new generation ID and effective date. + """ + + # + # Validate. + # + self._validateAndConvertData(oData, oData.ksValidateFor_Edit); + + # + # Get current data. + # + oOldData = TestBoxDataEx().initFromDbWithId(self._oDb, oData.idTestBox); + + # + # Do it. + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', 'aoInSchedGroups', ] + + TestBoxData.kasMachineSettableOnly ): + self._oDb.callProc('TestBoxLogic_editEntry' + , ( uidAuthor, + oData.idTestBox, + oData.ip, # Should we allow setting the IP? + oData.uuidSystem, + oData.sName, + oData.sDescription, + oData.fEnabled, + oData.enmLomKind, + oData.ipLom, + oData.pctScaleTimeout, + oData.sComment, + oData.enmPendingCmd, )); + (idGenTestBox, tsEffective) = self._oDb.fetchOne(); + else: + idGenTestBox = oOldData.idGenTestBox; + tsEffective = oOldData.tsEffective; + + if isinstance(oData, TestBoxDataEx): + # Calc in-group changes. + aoRemoved = list(oOldData.aoInSchedGroups); + aoNew = []; + aoUpdated = []; + for oNewInGroup in oData.aoInSchedGroups: + oOldInGroup = None; + for iCur, oCur in enumerate(aoRemoved): + if oCur.idSchedGroup == oNewInGroup.idSchedGroup: + oOldInGroup = aoRemoved.pop(iCur); + break; + if oOldInGroup is None: + aoNew.append(oNewInGroup); + elif oNewInGroup.iSchedPriority != oOldInGroup.iSchedPriority: + aoUpdated.append(oNewInGroup); + + # Remove in-groups. + for oInGroup in aoRemoved: + self._oDb.callProc('TestBoxLogic_removeGroupEntry', (uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, )); + + # Add new ones. + for oInGroup in aoNew: + self._oDb.callProc('TestBoxLogic_addGroupEntry', + ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) ); + + # Edit existing ones. + for oInGroup in aoUpdated: + self._oDb.callProc('TestBoxLogic_editGroupEntry', + ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) ); + else: + assert isinstance(oData, TestBoxData); + + self._oDb.maybeCommit(fCommit); + return (idGenTestBox, tsEffective); + + + def removeEntry(self, uidAuthor, idTestBox, fCascade = False, fCommit = False): + """ + Delete test box and scheduling group associations. + """ + self._oDb.callProc('TestBoxLogic_removeEntry' + , ( uidAuthor, idTestBox, fCascade,)); + self._oDb.maybeCommit(fCommit); + return True; + + + def updateOnSignOn(self, idTestBox, idGenTestBox, sTestBoxAddr, sOs, sOsVersion, # pylint: disable=too-many-arguments,too-many-locals + sCpuVendor, sCpuArch, sCpuName, lCpuRevision, cCpus, fCpuHwVirt, fCpuNestedPaging, fCpu64BitGuest, + fChipsetIoMmu, fRawMode, cMbMemory, cMbScratch, sReport, iTestBoxScriptRev, iPythonHexVersion): + """ + Update the testbox attributes automatically on behalf of the testbox script. + Returns the new generation id on success, raises an exception on failure. + """ + _ = idGenTestBox; + self._oDb.callProc('TestBoxLogic_updateOnSignOn' + , ( idTestBox, + sTestBoxAddr, + sOs, + sOsVersion, + sCpuVendor, + sCpuArch, + sCpuName, + lCpuRevision, + cCpus, + fCpuHwVirt, + fCpuNestedPaging, + fCpu64BitGuest, + fChipsetIoMmu, + fRawMode, + cMbMemory, + cMbScratch, + sReport, + iTestBoxScriptRev, + iPythonHexVersion,)); + return self._oDb.fetchOne()[0]; + + + def setCommand(self, idTestBox, sOldCommand, sNewCommand, uidAuthor = None, fCommit = False, sComment = None): + """ + Sets or resets the pending command on a testbox. + Returns (idGenTestBox, tsEffective) of the new row. + """ + ## @todo throw TMInFligthCollision again... + self._oDb.callProc('TestBoxLogic_setCommand' + , ( uidAuthor, idTestBox, sOldCommand, sNewCommand, sComment,)); + aoRow = self._oDb.fetchOne(); + self._oDb.maybeCommit(fCommit); + return (aoRow[0], aoRow[1]); + + + def getAll(self): + """ + Retrieve list of all registered Test Box records from DB. + """ + self._oDb.execute('SELECT *\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE tsExpire=\'infinity\'::timestamp\n' + 'ORDER BY sName') + + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestBoxData().initFromDbRow(aoRow)) + return aoRet + + + def cachedLookup(self, idTestBox): + # type: (int) -> TestBoxDataEx + """ + Looks up the most recent TestBoxData object for idTestBox via + an object cache. + + Returns a shared TestBoxDataEx object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('TestBoxData'); + oEntry = self.dCache.get(idTestBox, None); + if oEntry is None: + fNeedNow = False; + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE idTestBox = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestBox, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE idTestBox = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idTestBox, )); + fNeedNow = True; + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestBox)); + + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + if not fNeedNow: + oEntry = TestBoxDataEx().initFromDbRowEx(aaoRow, self._oDb); + else: + oEntry = TestBoxDataEx().initFromDbRow(aaoRow); + oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow = db.dbTimestampMinusOneTick(oEntry.tsExpire)); + self.dCache[idTestBox] = oEntry; + return oEntry; + + + + # + # The virtual test sheriff interface. + # + + def hasTestBoxRecentlyBeenRebooted(self, idTestBox, cHoursBack = 2, tsNow = None): + """ + Checks if the testbox has been rebooted in the specified time period. + + This does not include already pending reboots, though under some + circumstances it may. These being the test box entry being edited for + other reasons. + + Returns True / False. + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + self._oDb.execute('SELECT COUNT(idTestBox)\n' + 'FROM TestBoxes\n' + 'WHERE idTestBox = %s\n' + ' AND tsExpire < %s\n' + ' AND tsExpire >= %s - interval \'%s hours\'\n' + ' AND enmPendingCmd IN (%s, %s)\n' + , ( idTestBox, tsNow, tsNow, cHoursBack, + TestBoxData.ksTestBoxCmd_Reboot, TestBoxData.ksTestBoxCmd_UpgradeAndReboot, )); + return self._oDb.fetchOne()[0] > 0; + + + def rebootTestBox(self, idTestBox, uidAuthor, sComment, sOldCommand = TestBoxData.ksTestBoxCmd_None, fCommit = False): + """ + Issues a reboot command for the given test box. + Return True on succes, False on in-flight collision. + May raise DB exception on other trouble. + """ + try: + self.setCommand(idTestBox, sOldCommand, TestBoxData.ksTestBoxCmd_Reboot, + uidAuthor = uidAuthor, fCommit = fCommit, sComment = sComment); + except TMInFligthCollision: + return False; + return True; + + + def disableTestBox(self, idTestBox, uidAuthor, sComment, fCommit = False): + """ + Disables the given test box. + + Raises exception on trouble, without rollback. + """ + oTestBox = TestBoxData().initFromDbWithId(self._oDb, idTestBox); + if oTestBox.fEnabled: + oTestBox.fEnabled = False; + if sComment is not None: + oTestBox.sComment = sComment; + self.editEntry(oTestBox, uidAuthor = uidAuthor, fCommit = fCommit); + return None; + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestBoxDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestBoxData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + |