1 | #!/usr/bin/env python
|
---|
2 | # -*- coding: utf-8 -*-
|
---|
3 | # $Id: partial-db-dump.py 106061 2024-09-16 14:03:52Z vboxsync $
|
---|
4 | # pylint: disable=line-too-long
|
---|
5 |
|
---|
6 | """
|
---|
7 | Utility for dumping the last X days of data.
|
---|
8 | """
|
---|
9 |
|
---|
10 | __copyright__ = \
|
---|
11 | """
|
---|
12 | Copyright (C) 2012-2024 Oracle and/or its affiliates.
|
---|
13 |
|
---|
14 | This file is part of VirtualBox base platform packages, as
|
---|
15 | available from https://www.virtualbox.org.
|
---|
16 |
|
---|
17 | This program is free software; you can redistribute it and/or
|
---|
18 | modify it under the terms of the GNU General Public License
|
---|
19 | as published by the Free Software Foundation, in version 3 of the
|
---|
20 | License.
|
---|
21 |
|
---|
22 | This program is distributed in the hope that it will be useful, but
|
---|
23 | WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
24 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
---|
25 | General Public License for more details.
|
---|
26 |
|
---|
27 | You should have received a copy of the GNU General Public License
|
---|
28 | along with this program; if not, see <https://www.gnu.org/licenses>.
|
---|
29 |
|
---|
30 | The contents of this file may alternatively be used under the terms
|
---|
31 | of the Common Development and Distribution License Version 1.0
|
---|
32 | (CDDL), a copy of it is provided in the "COPYING.CDDL" file included
|
---|
33 | in the VirtualBox distribution, in which case the provisions of the
|
---|
34 | CDDL are applicable instead of those of the GPL.
|
---|
35 |
|
---|
36 | You may elect to license modified versions of this file under the
|
---|
37 | terms and conditions of either the GPL or the CDDL or both.
|
---|
38 |
|
---|
39 | SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
|
---|
40 | """
|
---|
41 | __version__ = "$Revision: 106061 $"
|
---|
42 |
|
---|
43 | # Standard python imports
|
---|
44 | import sys;
|
---|
45 | import os;
|
---|
46 | import zipfile;
|
---|
47 | from optparse import OptionParser;
|
---|
48 | import xml.etree.ElementTree as ET;
|
---|
49 |
|
---|
50 | # Add Test Manager's modules path
|
---|
51 | g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
|
---|
52 | sys.path.append(g_ksTestManagerDir);
|
---|
53 |
|
---|
54 | # Test Manager imports
|
---|
55 | from testmanager.core.db import TMDatabaseConnection;
|
---|
56 | from common import utils;
|
---|
57 |
|
---|
58 |
|
---|
59 | class PartialDbDump(object): # pylint: disable=too-few-public-methods
|
---|
60 | """
|
---|
61 | Dumps or loads the last X days of database data.
|
---|
62 |
|
---|
63 | This is a useful tool when hacking on the test manager locally. You can get
|
---|
64 | a small sample from the last few days from the production test manager server
|
---|
65 | without spending hours dumping, downloading, and loading the whole database
|
---|
66 | (because it is gigantic).
|
---|
67 |
|
---|
68 | """
|
---|
69 |
|
---|
70 | def __init__(self):
|
---|
71 | """
|
---|
72 | Parse command line.
|
---|
73 | """
|
---|
74 |
|
---|
75 | oParser = OptionParser()
|
---|
76 | oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true',
|
---|
77 | help = 'Quiet execution');
|
---|
78 | oParser.add_option('-f', '--filename', dest = 'sFilename', metavar = '<filename>',
|
---|
79 | default = 'partial-db-dump.zip', help = 'The name of the partial database zip file to write/load.');
|
---|
80 |
|
---|
81 | oParser.add_option('-t', '--tmp-file', dest = 'sTempFile', metavar = '<temp-file>',
|
---|
82 | default = '/tmp/tm-partial-db-dump.pgtxt',
|
---|
83 | help = 'Name of temporary file for duping tables. Must be absolute');
|
---|
84 | oParser.add_option('--days-to-dump', dest = 'cDays', metavar = '<days>', type = 'int', default = 14,
|
---|
85 | help = 'How many days to dump (counting backward from current date).');
|
---|
86 | oParser.add_option('--load-dump-into-database', dest = 'fLoadDumpIntoDatabase', action = 'store_true',
|
---|
87 | default = False, help = 'For loading instead of dumping.');
|
---|
88 | oParser.add_option('--store', dest = 'fStore', action = 'store_true',
|
---|
89 | default = False, help = 'Do not compress the zip file.');
|
---|
90 |
|
---|
91 | (self.oConfig, _) = oParser.parse_args();
|
---|
92 |
|
---|
93 |
|
---|
94 | ##
|
---|
95 | # Tables dumped in full because they're either needed in full or they normally
|
---|
96 | # aren't large enough to bother reducing.
|
---|
97 | kasTablesToDumpInFull = [
|
---|
98 | 'Users',
|
---|
99 | 'BuildBlacklist',
|
---|
100 | 'BuildCategories',
|
---|
101 | 'BuildSources',
|
---|
102 | 'FailureCategories',
|
---|
103 | 'FailureReasons',
|
---|
104 | 'GlobalResources',
|
---|
105 | 'Testcases',
|
---|
106 | 'TestcaseArgs',
|
---|
107 | 'TestcaseDeps',
|
---|
108 | 'TestcaseGlobalRsrcDeps',
|
---|
109 | 'TestGroups',
|
---|
110 | 'TestGroupMembers',
|
---|
111 | 'SchedGroups',
|
---|
112 | 'SchedGroupMembers', # ?
|
---|
113 | 'TestBoxesInSchedGroups', # ?
|
---|
114 | 'SchedQueues',
|
---|
115 | 'TestResultStrTab', # 36K rows, never mind complicated then.
|
---|
116 | ];
|
---|
117 |
|
---|
118 | ##
|
---|
119 | # Tables where we only dump partial info (the TestResult* tables are rather
|
---|
120 | # gigantic).
|
---|
121 | kasTablesToPartiallyDump = [
|
---|
122 | 'TestBoxes', # 2016-05-25: ca. 641 MB
|
---|
123 | 'TestSets', # 2016-05-25: ca. 525 MB
|
---|
124 | 'TestResults', # 2016-05-25: ca. 13 GB
|
---|
125 | 'TestResultFiles', # 2016-05-25: ca. 87 MB
|
---|
126 | 'TestResultMsgs', # 2016-05-25: ca. 29 MB
|
---|
127 | 'TestResultValues', # 2016-05-25: ca. 3728 MB
|
---|
128 | 'TestResultFailures',
|
---|
129 | 'Builds',
|
---|
130 | 'TestBoxStrTab',
|
---|
131 | 'SystemLog',
|
---|
132 | 'VcsRevisions',
|
---|
133 | ];
|
---|
134 |
|
---|
135 | def _doCopyTo(self, sTable, oZipFile, oDb, sSql, aoArgs = None):
|
---|
136 | """ Does one COPY TO job. """
|
---|
137 | print('Dumping %s...' % (sTable,));
|
---|
138 |
|
---|
139 | if aoArgs is not None:
|
---|
140 | sSql = oDb.formatBindArgs(sSql, aoArgs);
|
---|
141 |
|
---|
142 | oFile = open(self.oConfig.sTempFile, 'w');
|
---|
143 | oDb.copyExpert(sSql, oFile);
|
---|
144 | cRows = oDb.getRowCount();
|
---|
145 | oFile.close();
|
---|
146 | print('... %s rows.' % (cRows,));
|
---|
147 |
|
---|
148 | oZipFile.write(self.oConfig.sTempFile, sTable);
|
---|
149 | return True;
|
---|
150 |
|
---|
151 | def _doDump(self, oDb):
|
---|
152 | """ Does the dumping of the database. """
|
---|
153 |
|
---|
154 | enmCompression = zipfile.ZIP_DEFLATED;
|
---|
155 | if self.oConfig.fStore:
|
---|
156 | enmCompression = zipfile.ZIP_STORED;
|
---|
157 | oZipFile = zipfile.ZipFile(self.oConfig.sFilename, 'w', enmCompression);
|
---|
158 |
|
---|
159 | oDb.begin();
|
---|
160 |
|
---|
161 | # Dumping full tables is simple.
|
---|
162 | for sTable in self.kasTablesToDumpInFull:
|
---|
163 | self._doCopyTo(sTable, oZipFile, oDb, 'COPY ' + sTable + ' TO STDOUT WITH (FORMAT TEXT)');
|
---|
164 |
|
---|
165 | # Figure out how far back we need to go.
|
---|
166 | oDb.execute('SELECT CURRENT_TIMESTAMP - INTERVAL \'%s days\'' % (self.oConfig.cDays,));
|
---|
167 | tsEffective = oDb.fetchOne()[0];
|
---|
168 | oDb.execute('SELECT CURRENT_TIMESTAMP - INTERVAL \'%s days\'' % (self.oConfig.cDays + 2,));
|
---|
169 | tsEffectiveSafe = oDb.fetchOne()[0];
|
---|
170 | print('Going back to: %s (safe: %s)' % (tsEffective, tsEffectiveSafe));
|
---|
171 |
|
---|
172 | # We dump test boxes back to the safe timestamp because the test sets may
|
---|
173 | # use slightly dated test box references and we don't wish to have dangling
|
---|
174 | # references when loading.
|
---|
175 | for sTable in [ 'TestBoxes', ]:
|
---|
176 | self._doCopyTo(sTable, oZipFile, oDb,
|
---|
177 | 'COPY (SELECT * FROM ' + sTable + ' WHERE tsExpire >= %s) TO STDOUT WITH (FORMAT TEXT)',
|
---|
178 | (tsEffectiveSafe,));
|
---|
179 |
|
---|
180 | # The test results needs to start with test sets and then dump everything
|
---|
181 | # releated to them. So, figure the lowest (oldest) test set ID we'll be
|
---|
182 | # dumping first.
|
---|
183 | oDb.execute('SELECT idTestSet FROM TestSets WHERE tsCreated >= %s', (tsEffective, ));
|
---|
184 | idFirstTestSet = 0;
|
---|
185 | if oDb.getRowCount() > 0:
|
---|
186 | idFirstTestSet = oDb.fetchOne()[0];
|
---|
187 | print('First test set ID: %s' % (idFirstTestSet,));
|
---|
188 |
|
---|
189 | oDb.execute('SELECT MAX(idTestSet) FROM TestSets WHERE tsCreated >= %s', (tsEffective, ));
|
---|
190 | idLastTestSet = 0;
|
---|
191 | if oDb.getRowCount() > 0:
|
---|
192 | idLastTestSet = oDb.fetchOne()[0];
|
---|
193 | print('Last test set ID: %s' % (idLastTestSet,));
|
---|
194 |
|
---|
195 | oDb.execute('SELECT MAX(idTestResult) FROM TestResults WHERE tsCreated >= %s', (tsEffective, ));
|
---|
196 | idLastTestResult = 0;
|
---|
197 | if oDb.getRowCount() > 0:
|
---|
198 | idLastTestResult = oDb.fetchOne()[0];
|
---|
199 | print('Last test result ID: %s' % (idLastTestResult,));
|
---|
200 |
|
---|
201 | # Tables with idTestSet member.
|
---|
202 | for sTable in [ 'TestSets', 'TestResults', 'TestResultValues' ]:
|
---|
203 | self._doCopyTo(sTable, oZipFile, oDb,
|
---|
204 | 'COPY (SELECT *\n'
|
---|
205 | ' FROM ' + sTable + '\n'
|
---|
206 | ' WHERE idTestSet >= %s\n'
|
---|
207 | ' AND idTestSet <= %s\n'
|
---|
208 | ' AND idTestResult <= %s\n'
|
---|
209 | ') TO STDOUT WITH (FORMAT TEXT)'
|
---|
210 | , ( idFirstTestSet, idLastTestSet, idLastTestResult,));
|
---|
211 |
|
---|
212 | # Tables where we have to go via TestResult.
|
---|
213 | for sTable in [ 'TestResultFiles', 'TestResultMsgs', 'TestResultFailures' ]:
|
---|
214 | self._doCopyTo(sTable, oZipFile, oDb,
|
---|
215 | 'COPY (SELECT it.*\n'
|
---|
216 | ' FROM ' + sTable + ' it, TestResults tr\n'
|
---|
217 | ' WHERE tr.idTestSet >= %s\n'
|
---|
218 | ' AND tr.idTestSet <= %s\n'
|
---|
219 | ' AND tr.idTestResult <= %s\n'
|
---|
220 | ' AND tr.tsCreated >= %s\n' # performance hack.
|
---|
221 | ' AND it.idTestResult = tr.idTestResult\n'
|
---|
222 | ') TO STDOUT WITH (FORMAT TEXT)'
|
---|
223 | , ( idFirstTestSet, idLastTestSet, idLastTestResult, tsEffective,));
|
---|
224 |
|
---|
225 | # Tables which goes exclusively by tsCreated using tsEffectiveSafe.
|
---|
226 | for sTable in [ 'SystemLog', 'VcsRevisions' ]:
|
---|
227 | self._doCopyTo(sTable, oZipFile, oDb,
|
---|
228 | 'COPY (SELECT * FROM ' + sTable + ' WHERE tsCreated >= %s) TO STDOUT WITH (FORMAT TEXT)',
|
---|
229 | (tsEffectiveSafe,));
|
---|
230 |
|
---|
231 | # The builds table.
|
---|
232 | oDb.execute('SELECT MIN(idBuild), MIN(idBuildTestSuite) FROM TestSets WHERE idTestSet >= %s', (idFirstTestSet,));
|
---|
233 | idFirstBuild = 0;
|
---|
234 | if oDb.getRowCount() > 0:
|
---|
235 | idFirstBuild = min(oDb.fetchOne());
|
---|
236 | print('First build ID: %s' % (idFirstBuild,));
|
---|
237 | for sTable in [ 'Builds', ]:
|
---|
238 | self._doCopyTo(sTable, oZipFile, oDb,
|
---|
239 | 'COPY (SELECT * FROM ' + sTable + ' WHERE idBuild >= %s) TO STDOUT WITH (FORMAT TEXT)',
|
---|
240 | (idFirstBuild,));
|
---|
241 |
|
---|
242 | # The test box string table.
|
---|
243 | self._doCopyTo('TestBoxStrTab', oZipFile, oDb, '''
|
---|
244 | COPY (SELECT * FROM TestBoxStrTab WHERE idStr IN (
|
---|
245 | ( SELECT 0
|
---|
246 | ) UNION ( SELECT idStrComment FROM TestBoxes WHERE tsExpire >= %s
|
---|
247 | ) UNION ( SELECT idStrCpuArch FROM TestBoxes WHERE tsExpire >= %s
|
---|
248 | ) UNION ( SELECT idStrCpuName FROM TestBoxes WHERE tsExpire >= %s
|
---|
249 | ) UNION ( SELECT idStrCpuVendor FROM TestBoxes WHERE tsExpire >= %s
|
---|
250 | ) UNION ( SELECT idStrDescription FROM TestBoxes WHERE tsExpire >= %s
|
---|
251 | ) UNION ( SELECT idStrOS FROM TestBoxes WHERE tsExpire >= %s
|
---|
252 | ) UNION ( SELECT idStrOsVersion FROM TestBoxes WHERE tsExpire >= %s
|
---|
253 | ) UNION ( SELECT idStrReport FROM TestBoxes WHERE tsExpire >= %s
|
---|
254 | ) ) ) TO STDOUT WITH (FORMAT TEXT)
|
---|
255 | ''', (tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe,
|
---|
256 | tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe,));
|
---|
257 |
|
---|
258 | oZipFile.close();
|
---|
259 | print('Done!');
|
---|
260 | return 0;
|
---|
261 |
|
---|
262 | def _doLoad(self, oDb):
|
---|
263 | """ Does the loading of the dumped data into the database. """
|
---|
264 |
|
---|
265 | try:
|
---|
266 | oZipFile = zipfile.ZipFile(self.oConfig.sFilename, 'r');
|
---|
267 | except oXcpt:
|
---|
268 | print('error: Failed to open dump file "%s": %s' % (self.oConfig.sFilename, oXcpt));
|
---|
269 | return 1;
|
---|
270 |
|
---|
271 | asTablesInLoadOrder = [
|
---|
272 | 'Users',
|
---|
273 | 'BuildBlacklist',
|
---|
274 | 'BuildCategories',
|
---|
275 | 'BuildSources',
|
---|
276 | 'FailureCategories',
|
---|
277 | 'FailureReasons',
|
---|
278 | 'GlobalResources',
|
---|
279 | 'Testcases',
|
---|
280 | 'TestcaseArgs',
|
---|
281 | 'TestcaseDeps',
|
---|
282 | 'TestcaseGlobalRsrcDeps',
|
---|
283 | 'TestGroups',
|
---|
284 | 'TestGroupMembers',
|
---|
285 | 'SchedGroups',
|
---|
286 | 'TestBoxStrTab',
|
---|
287 | 'TestBoxes',
|
---|
288 | 'SchedGroupMembers',
|
---|
289 | 'TestBoxesInSchedGroups',
|
---|
290 | 'SchedQueues',
|
---|
291 | 'Builds',
|
---|
292 | 'SystemLog',
|
---|
293 | 'VcsRevisions',
|
---|
294 | 'TestResultStrTab',
|
---|
295 | 'TestSets',
|
---|
296 | 'TestResults',
|
---|
297 | 'TestResultFiles',
|
---|
298 | 'TestResultMsgs',
|
---|
299 | 'TestResultValues',
|
---|
300 | 'TestResultFailures',
|
---|
301 | ];
|
---|
302 | assert len(asTablesInLoadOrder) == len(self.kasTablesToDumpInFull) + len(self.kasTablesToPartiallyDump);
|
---|
303 |
|
---|
304 | oDb.begin();
|
---|
305 | oDb.execute('SET CONSTRAINTS ALL DEFERRED;');
|
---|
306 |
|
---|
307 | print('Checking if the database looks empty...\n');
|
---|
308 | for sTable in asTablesInLoadOrder + [ 'TestBoxStatuses', 'GlobalResourceStatuses' ]:
|
---|
309 | oDb.execute('SELECT COUNT(*) FROM ' + sTable);
|
---|
310 | cRows = oDb.fetchOne()[0];
|
---|
311 | cMaxRows = 0;
|
---|
312 | if sTable in [ 'SchedGroups', 'TestBoxStrTab', 'TestResultStrTab', 'Users' ]: cMaxRows = 1;
|
---|
313 | if cRows > cMaxRows:
|
---|
314 | print('error: Table %s has %u rows which is more than %u - refusing to delete and load.'
|
---|
315 | % (sTable, cRows, cMaxRows,));
|
---|
316 | print('info: Please drop and recreate the database before loading!');
|
---|
317 | return 1;
|
---|
318 |
|
---|
319 | print('Dropping default table content...\n');
|
---|
320 | for sTable in [ 'SchedGroups', 'TestBoxStrTab', 'TestResultStrTab', 'Users']:
|
---|
321 | oDb.execute('DELETE FROM ' + sTable);
|
---|
322 |
|
---|
323 | oDb.execute('ALTER TABLE TestSets DROP CONSTRAINT IF EXISTS TestSets_idTestResult_fkey');
|
---|
324 |
|
---|
325 | for sTable in asTablesInLoadOrder:
|
---|
326 | print('Loading %s...' % (sTable,));
|
---|
327 | oFile = oZipFile.open(sTable);
|
---|
328 | oDb.copyExpert('COPY ' + sTable + ' FROM STDIN WITH (FORMAT TEXT)', oFile);
|
---|
329 | cRows = oDb.getRowCount();
|
---|
330 | print('... %s rows.' % (cRows,));
|
---|
331 |
|
---|
332 | oDb.execute('ALTER TABLE TestSets ADD FOREIGN KEY (idTestResult) REFERENCES TestResults(idTestResult)');
|
---|
333 | oDb.commit();
|
---|
334 |
|
---|
335 | # Correct sequences.
|
---|
336 | atSequences = [
|
---|
337 | ( 'UserIdSeq', 'Users', 'uid' ),
|
---|
338 | ( 'GlobalResourceIdSeq', 'GlobalResources', 'idGlobalRsrc' ),
|
---|
339 | ( 'BuildSourceIdSeq', 'BuildSources', 'idBuildSrc' ),
|
---|
340 | ( 'TestCaseIdSeq', 'TestCases', 'idTestCase' ),
|
---|
341 | ( 'TestCaseGenIdSeq', 'TestCases', 'idGenTestCase' ),
|
---|
342 | ( 'TestCaseArgsIdSeq', 'TestCaseArgs', 'idTestCaseArgs' ),
|
---|
343 | ( 'TestCaseArgsGenIdSeq', 'TestCaseArgs', 'idGenTestCaseArgs' ),
|
---|
344 | ( 'TestGroupIdSeq', 'TestGroups', 'idTestGroup' ),
|
---|
345 | ( 'SchedGroupIdSeq', 'SchedGroups', 'idSchedGroup' ),
|
---|
346 | ( 'TestBoxStrTabIdSeq', 'TestBoxStrTab', 'idStr' ),
|
---|
347 | ( 'TestBoxIdSeq', 'TestBoxes', 'idTestBox' ),
|
---|
348 | ( 'TestBoxGenIdSeq', 'TestBoxes', 'idGenTestBox' ),
|
---|
349 | ( 'FailureCategoryIdSeq', 'FailureCategories', 'idFailureCategory' ),
|
---|
350 | ( 'FailureReasonIdSeq', 'FailureReasons', 'idFailureReason' ),
|
---|
351 | ( 'BuildBlacklistIdSeq', 'BuildBlacklist', 'idBlacklisting' ),
|
---|
352 | ( 'BuildCategoryIdSeq', 'BuildCategories', 'idBuildCategory' ),
|
---|
353 | ( 'BuildIdSeq', 'Builds', 'idBuild' ),
|
---|
354 | ( 'TestResultStrTabIdSeq', 'TestResultStrTab', 'idStr' ),
|
---|
355 | ( 'TestResultIdSeq', 'TestResults', 'idTestResult' ),
|
---|
356 | ( 'TestResultValueIdSeq', 'TestResultValues', 'idTestResultValue' ),
|
---|
357 | ( 'TestResultFileId', 'TestResultFiles', 'idTestResultFile' ),
|
---|
358 | ( 'TestResultMsgIdSeq', 'TestResultMsgs', 'idTestResultMsg' ),
|
---|
359 | ( 'TestSetIdSeq', 'TestSets', 'idTestSet' ),
|
---|
360 | ( 'SchedQueueItemIdSeq', 'SchedQueues', 'idItem' ),
|
---|
361 | ];
|
---|
362 | for (sSeq, sTab, sCol) in atSequences:
|
---|
363 | oDb.execute('SELECT MAX(%s) FROM %s' % (sCol, sTab,));
|
---|
364 | idMax = oDb.fetchOne()[0];
|
---|
365 | print('%s: idMax=%s' % (sSeq, idMax));
|
---|
366 | if idMax is not None:
|
---|
367 | oDb.execute('SELECT setval(\'%s\', %s)' % (sSeq, idMax));
|
---|
368 |
|
---|
369 | # Last step.
|
---|
370 | print('Analyzing...');
|
---|
371 | oDb.execute('ANALYZE');
|
---|
372 | oDb.commit();
|
---|
373 |
|
---|
374 | print('Done!');
|
---|
375 | return 0;
|
---|
376 |
|
---|
377 | def main(self):
|
---|
378 | """
|
---|
379 | Main function.
|
---|
380 | """
|
---|
381 | oDb = TMDatabaseConnection();
|
---|
382 |
|
---|
383 | if self.oConfig.fLoadDumpIntoDatabase is not True:
|
---|
384 | rc = self._doDump(oDb);
|
---|
385 | else:
|
---|
386 | rc = self._doLoad(oDb);
|
---|
387 |
|
---|
388 | oDb.close();
|
---|
389 | return 0;
|
---|
390 |
|
---|
391 | if __name__ == '__main__':
|
---|
392 | sys.exit(PartialDbDump().main());
|
---|