1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 import time
21 import os, os.path
22 import glob
23 from weakref import proxy as weakref
24
25 from sqlutils import sqlite, executeSQL, sql_esc, sql_esc_glob
26 import yum.misc as misc
27 import yum.constants
28 from yum.constants import *
29 from yum.packages import YumInstalledPackage, YumAvailablePackage, PackageObject
30 from yum.i18n import to_unicode
31
32
33 _history_dir = '/var/lib/yum/history'
34
35
36
37
38 _stcode2sttxt = {TS_UPDATE : 'Update',
39 TS_UPDATED : 'Updated',
40 TS_ERASE: 'Erase',
41 TS_INSTALL: 'Install',
42 TS_TRUEINSTALL : 'True-Install',
43 TS_OBSOLETED: 'Obsoleted',
44 TS_OBSOLETING: 'Obsoleting'}
45
46 _sttxt2stcode = {'Update' : TS_UPDATE,
47 'Updated' : TS_UPDATED,
48 'Erase' : TS_ERASE,
49 'Install' : TS_INSTALL,
50 'True-Install' : TS_TRUEINSTALL,
51 'Dep-Install' : TS_INSTALL,
52 'Reinstall' : TS_INSTALL,
53 'Downgrade' : TS_INSTALL,
54 'Downgraded' : TS_INSTALL,
55 'Obsoleted' : TS_OBSOLETED,
56 'Obsoleting' : TS_OBSOLETING}
57
58
59
60 -def _setupHistorySearchSQL(patterns=None, ignore_case=False):
61 """Setup need_full and patterns for _yieldSQLDataList, also see if
62 we can get away with just using searchNames(). """
63
64 if patterns is None:
65 patterns = []
66
67 fields = ['name', 'sql_nameArch', 'sql_nameVerRelArch',
68 'sql_nameVer', 'sql_nameVerRel',
69 'sql_envra', 'sql_nevra']
70 need_full = False
71 for pat in patterns:
72 if yum.misc.re_full_search_needed(pat):
73 need_full = True
74 break
75
76 pat_max = PATTERNS_MAX
77 if not need_full:
78 fields = ['name']
79 pat_max = PATTERNS_INDEXED_MAX
80 if len(patterns) > pat_max:
81 patterns = []
82 if ignore_case:
83 patterns = sql_esc_glob(patterns)
84 else:
85 tmp = []
86 need_glob = False
87 for pat in patterns:
88 if misc.re_glob(pat):
89 tmp.append((pat, 'glob'))
90 need_glob = True
91 else:
92 tmp.append((pat, '='))
93 if not need_full and not need_glob and patterns:
94 return (need_full, patterns, fields, True)
95 patterns = tmp
96 return (need_full, patterns, fields, False)
97
98
99 -class YumHistoryPackage(PackageObject):
100
101 - def __init__(self, name, arch, epoch, version, release, checksum):
102 self.name = name
103 self.version = version
104 self.release = release
105 self.epoch = epoch
106 self.arch = arch
107 self.pkgtup = (self.name, self.arch,
108 self.epoch, self.version, self.release)
109 if checksum is None:
110 self._checksums = []
111 else:
112 chk = checksum.split(':')
113 self._checksums = [(chk[0], chk[1], 0)]
114
116 """ Holder for a history transaction. """
117
118 - def __init__(self, history, row):
119 self._history = weakref(history)
120
121 self.tid = row[0]
122 self.beg_timestamp = row[1]
123 self.beg_rpmdbversion = row[2]
124 self.end_timestamp = row[3]
125 self.end_rpmdbversion = row[4]
126 self.loginuid = row[5]
127 self.return_code = row[6]
128
129 self._loaded_TW = None
130 self._loaded_TD = None
131
132 self._loaded_ER = None
133 self._loaded_OT = None
134
135 self.altered_lt_rpmdb = None
136 self.altered_gt_rpmdb = None
137
138 - def __cmp__(self, other):
139 if other is None:
140 return 1
141 ret = cmp(self.beg_timestamp, other.beg_timestamp)
142 if ret: return -ret
143 ret = cmp(self.end_timestamp, other.end_timestamp)
144 if ret: return ret
145 ret = cmp(self.tid, other.tid)
146 return -ret
147
148 - def _getTransWith(self):
149 if self._loaded_TW is None:
150 self._loaded_TW = sorted(self._history._old_with_pkgs(self.tid))
151 return self._loaded_TW
152 - def _getTransData(self):
153 if self._loaded_TD is None:
154 self._loaded_TD = sorted(self._history._old_data_pkgs(self.tid))
155 return self._loaded_TD
156
157 trans_with = property(fget=lambda self: self._getTransWith())
158 trans_data = property(fget=lambda self: self._getTransData())
159
160 - def _getErrors(self):
161 if self._loaded_ER is None:
162 self._loaded_ER = self._history._load_errors(self.tid)
163 return self._loaded_ER
164 - def _getOutput(self):
165 if self._loaded_OT is None:
166 self._loaded_OT = self._history._load_output(self.tid)
167 return self._loaded_OT
168
169 errors = property(fget=lambda self: self._getErrors())
170 output = property(fget=lambda self: self._getOutput())
171
173 """ API for accessing the history sqlite data. """
174
175 - def __init__(self, root='/', db_path=_history_dir):
176 self._conn = None
177
178 self.conf = yum.misc.GenericHolder()
179 self.conf.db_path = os.path.normpath(root + '/' + db_path)
180 self.conf.writable = False
181
182 if not os.path.exists(self.conf.db_path):
183 try:
184 os.makedirs(self.conf.db_path)
185 except (IOError, OSError), e:
186
187 return
188 self.conf.writable = True
189 else:
190 if os.access(self.conf.db_path, os.W_OK):
191 self.conf.writable = True
192
193 DBs = glob.glob('%s/history-*-*-*.sqlite' % self.conf.db_path)
194 self._db_file = None
195 for d in reversed(sorted(DBs)):
196 fname = os.path.basename(d)
197 fname = fname[len("history-"):-len(".sqlite")]
198 pieces = fname.split('-', 4)
199 if len(pieces) != 3:
200 continue
201 try:
202 map(int, pieces)
203 except ValueError:
204 continue
205
206 self._db_file = d
207 break
208
209 if self._db_file is None:
210 self._create_db_file()
211
214
215 - def _get_cursor(self):
216 if self._conn is None:
217 self._conn = sqlite.connect(self._db_file)
218 return self._conn.cursor()
220 return self._conn.commit()
221
223 if self._conn is not None:
224 self._conn.close()
225 self._conn = None
226
227 - def _pkgtup2pid(self, pkgtup, checksum=None):
228 cur = self._get_cursor()
229 executeSQL(cur, """SELECT pkgtupid, checksum FROM pkgtups
230 WHERE name=? AND arch=? AND
231 epoch=? AND version=? AND release=?""", pkgtup)
232 for sql_pkgtupid, sql_checksum in cur:
233 if checksum is None and sql_checksum is None:
234 return sql_pkgtupid
235 if checksum is None:
236 continue
237 if sql_checksum is None:
238 continue
239 if checksum == sql_checksum:
240 return sql_pkgtupid
241
242 (n,a,e,v,r) = pkgtup
243 (n,a,e,v,r) = (to_unicode(n),to_unicode(a),
244 to_unicode(e),to_unicode(v),to_unicode(r))
245 if checksum is not None:
246 res = executeSQL(cur,
247 """INSERT INTO pkgtups
248 (name, arch, epoch, version, release, checksum)
249 VALUES (?, ?, ?, ?, ?, ?)""", (n,a,e,v,r,
250 checksum))
251 else:
252 res = executeSQL(cur,
253 """INSERT INTO pkgtups
254 (name, arch, epoch, version, release)
255 VALUES (?, ?, ?, ?, ?)""", (n,a,e,v,r))
256 return cur.lastrowid
257 - def _apkg2pid(self, po):
258 csum = po.returnIdSum()
259 if csum is not None:
260 csum = "%s:%s" % (str(csum[0]), str(csum[1]))
261 return self._pkgtup2pid(po.pkgtup, csum)
262 - def _ipkg2pid(self, po):
263 csum = None
264 yumdb = po.yumdb_info
265 if 'checksum_type' in yumdb and 'checksum_data' in yumdb:
266 csum = "%s:%s" % (yumdb.checksum_type, yumdb.checksum_data)
267 return self._pkgtup2pid(po.pkgtup, csum)
268 - def pkg2pid(self, po):
269 if isinstance(po, YumInstalledPackage):
270 return self._ipkg2pid(po)
271 if isinstance(po, YumAvailablePackage):
272 return self._apkg2pid(po)
273 return self._pkgtup2pid(po.pkgtup, None)
274
275 @staticmethod
276 - def txmbr2state(txmbr):
277 state = None
278 if txmbr.output_state in (TS_INSTALL, TS_TRUEINSTALL):
279 if hasattr(txmbr, 'reinstall'):
280 state = 'Reinstall'
281 elif txmbr.downgrades:
282 state = 'Downgrade'
283 if txmbr.output_state == TS_ERASE:
284 if txmbr.downgraded_by:
285 state = 'Downgraded'
286 if state is None:
287 state = _stcode2sttxt.get(txmbr.output_state)
288 if state == 'Install' and txmbr.isDep:
289 state = 'Dep-Install'
290 return state
291
292 - def trans_with_pid(self, pid):
293 cur = self._get_cursor()
294 res = executeSQL(cur,
295 """INSERT INTO trans_with_pkgs
296 (tid, pkgtupid)
297 VALUES (?, ?)""", (self._tid, pid))
298 return cur.lastrowid
299
300 - def trans_data_pid_beg(self, pid, state):
301 assert state is not None
302 if not hasattr(self, '_tid') or state is None:
303 return
304 cur = self._get_cursor()
305 res = executeSQL(cur,
306 """INSERT INTO trans_data_pkgs
307 (tid, pkgtupid, state)
308 VALUES (?, ?, ?)""", (self._tid, pid, state))
309 return cur.lastrowid
310 - def trans_data_pid_end(self, pid, state):
311
312 if not hasattr(self, '_tid') or state is None:
313 return
314
315 cur = self._get_cursor()
316 res = executeSQL(cur,
317 """UPDATE trans_data_pkgs SET done = ?
318 WHERE tid = ? AND pkgtupid = ? AND state = ?
319 """, ('TRUE', self._tid, pid, state))
320 self._commit()
321 return cur.lastrowid
322
323 - def beg(self, rpmdb_version, using_pkgs, txmbrs):
324 cur = self._get_cursor()
325 res = executeSQL(cur,
326 """INSERT INTO trans_beg
327 (timestamp, rpmdb_version, loginuid)
328 VALUES (?, ?, ?)""", (int(time.time()),
329 str(rpmdb_version),
330 yum.misc.getloginuid()))
331 self._tid = cur.lastrowid
332
333 for pkg in using_pkgs:
334 pid = self._ipkg2pid(pkg)
335 self.trans_with_pid(pid)
336
337 for txmbr in txmbrs:
338 pid = self.pkg2pid(txmbr.po)
339 state = self.txmbr2state(txmbr)
340 self.trans_data_pid_beg(pid, state)
341
342 self._commit()
343
344 - def _log_errors(self, errors):
345 cur = self._get_cursor()
346 for error in errors:
347 error = to_unicode(error)
348 executeSQL(cur,
349 """INSERT INTO trans_error
350 (tid, msg) VALUES (?, ?)""", (self._tid, error))
351 self._commit()
352
353 - def log_scriptlet_output(self, data, msg):
354 """ Note that data can be either a real pkg. ... or not. """
355 if msg is None or not hasattr(self, '_tid'):
356 return
357
358 cur = self._get_cursor()
359 for error in msg.split('\n'):
360 error = to_unicode(error)
361 executeSQL(cur,
362 """INSERT INTO trans_script_stdout
363 (tid, line) VALUES (?, ?)""", (self._tid, error))
364 self._commit()
365
366 - def _load_errors(self, tid):
367 cur = self._get_cursor()
368 executeSQL(cur,
369 """SELECT msg FROM trans_error
370 WHERE tid = ?
371 ORDER BY mid ASC""", (tid,))
372 ret = []
373 for row in cur:
374 ret.append(row[0])
375 return ret
376
377 - def _load_output(self, tid):
378 cur = self._get_cursor()
379 executeSQL(cur,
380 """SELECT line FROM trans_script_stdout
381 WHERE tid = ?
382 ORDER BY lid ASC""", (tid,))
383 ret = []
384 for row in cur:
385 ret.append(row[0])
386 return ret
387
388 - def end(self, rpmdb_version, return_code, errors=None):
389 assert return_code or not errors
390 cur = self._get_cursor()
391 res = executeSQL(cur,
392 """INSERT INTO trans_end
393 (tid, timestamp, rpmdb_version, return_code)
394 VALUES (?, ?, ?, ?)""", (self._tid,int(time.time()),
395 str(rpmdb_version),
396 return_code))
397 self._commit()
398 if not return_code:
399
400
401
402 executeSQL(cur,
403 """UPDATE trans_data_pkgs SET done = ?
404 WHERE tid = ?""", ('TRUE', self._tid,))
405 self._commit()
406 if errors is not None:
407 self._log_errors(errors)
408 del self._tid
409
410 - def _old_with_pkgs(self, tid):
411 cur = self._get_cursor()
412 executeSQL(cur,
413 """SELECT name, arch, epoch, version, release, checksum
414 FROM trans_with_pkgs JOIN pkgtups USING(pkgtupid)
415 WHERE tid = ?
416 ORDER BY name ASC, epoch ASC""", (tid,))
417 ret = []
418 for row in cur:
419 obj = YumHistoryPackage(row[0],row[1],row[2],row[3],row[4], row[5])
420 ret.append(obj)
421 return ret
422 - def _old_data_pkgs(self, tid):
423 cur = self._get_cursor()
424 executeSQL(cur,
425 """SELECT name, arch, epoch, version, release,
426 checksum, done, state
427 FROM trans_data_pkgs JOIN pkgtups USING(pkgtupid)
428 WHERE tid = ?
429 ORDER BY name ASC, epoch ASC, state DESC""", (tid,))
430 ret = []
431 for row in cur:
432 obj = YumHistoryPackage(row[0],row[1],row[2],row[3],row[4], row[5])
433 obj.done = row[6] == 'TRUE'
434 obj.state = row[7]
435 obj.state_installed = None
436 if _sttxt2stcode[obj.state] in TS_INSTALL_STATES:
437 obj.state_installed = True
438 if _sttxt2stcode[obj.state] in TS_REMOVE_STATES:
439 obj.state_installed = False
440 ret.append(obj)
441 return ret
442
443 - def old(self, tids=[], limit=None, complete_transactions_only=False):
444 """ Return a list of the last transactions, note that this includes
445 partial transactions (ones without an end transaction). """
446 cur = self._get_cursor()
447 sql = """SELECT tid,
448 trans_beg.timestamp AS beg_ts,
449 trans_beg.rpmdb_version AS beg_rv,
450 trans_end.timestamp AS end_ts,
451 trans_end.rpmdb_version AS end_rv,
452 loginuid, return_code
453 FROM trans_beg JOIN trans_end USING(tid)"""
454
455
456 if not complete_transactions_only:
457 sql = """SELECT tid,
458 trans_beg.timestamp AS beg_ts,
459 trans_beg.rpmdb_version AS beg_rv,
460 NULL, NULL,
461 loginuid, NULL
462 FROM trans_beg"""
463 params = None
464 if tids and len(tids) <= yum.constants.PATTERNS_INDEXED_MAX:
465 params = tids = list(set(tids))
466 sql += " WHERE tid IN (%s)" % ", ".join(['?'] * len(tids))
467 sql += " ORDER BY beg_ts DESC, tid ASC"
468 if limit is not None:
469 sql += " LIMIT " + str(limit)
470 executeSQL(cur, sql, params)
471 ret = []
472 tid2obj = {}
473 for row in cur:
474 if tids and len(tids) > yum.constants.PATTERNS_INDEXED_MAX:
475 if row[0] not in tids:
476 continue
477 obj = YumHistoryTransaction(self, row)
478 tid2obj[row[0]] = obj
479 ret.append(obj)
480
481 sql = """SELECT tid,
482 trans_end.timestamp AS end_ts,
483 trans_end.rpmdb_version AS end_rv,
484 return_code
485 FROM trans_end"""
486 params = tid2obj.keys()
487 if len(params) > yum.constants.PATTERNS_INDEXED_MAX:
488 executeSQL(cur, sql)
489 else:
490 sql += " WHERE tid IN (%s)" % ", ".join(['?'] * len(params))
491 executeSQL(cur, sql, params)
492 for row in cur:
493 if row[0] not in tid2obj:
494 continue
495 tid2obj[row[0]].end_timestamp = row[1]
496 tid2obj[row[0]].end_rpmdbversion = row[2]
497 tid2obj[row[0]].return_code = row[3]
498
499
500 las = None
501 for obj in reversed(ret):
502 cur_rv = obj.beg_rpmdbversion
503 las_rv = None
504 if las is not None:
505 las_rv = las.end_rpmdbversion
506 if las_rv is None or cur_rv is None or (las.tid + 1) != obj.tid:
507 pass
508 elif las_rv != cur_rv:
509 obj.altered_lt_rpmdb = True
510 las.altered_gt_rpmdb = True
511 else:
512 obj.altered_lt_rpmdb = False
513 las.altered_gt_rpmdb = False
514 las = obj
515
516 return ret
517
518 - def last(self, complete_transactions_only=True):
519 """ This is the last full transaction. So any incomplete transactions
520 do not count, by default. """
521 ret = self.old([], 1, complete_transactions_only)
522 if not ret:
523 return None
524 assert len(ret) == 1
525 return ret[0]
526
527 - def _yieldSQLDataList(self, patterns, fields, ignore_case):
528 """Yields all the package data for the given params. """
529
530 cur = self._get_cursor()
531 qsql = _FULL_PARSE_QUERY_BEG
532
533 pat_sqls = []
534 pat_data = []
535 for (pattern, rest) in patterns:
536 for field in fields:
537 if ignore_case:
538 pat_sqls.append("%s LIKE ?%s" % (field, rest))
539 else:
540 pat_sqls.append("%s %s ?" % (field, rest))
541 pat_data.append(pattern)
542 assert pat_sqls
543
544 qsql += " OR ".join(pat_sqls)
545 executeSQL(cur, qsql, pat_data)
546 for x in cur:
547 yield x
548
549 - def search(self, patterns, ignore_case=True):
550 """ Search for history transactions which contain specified
551 packages al. la. "yum list". Returns transaction ids. """
552
553
554 data = _setupHistorySearchSQL(patterns, ignore_case)
555 (need_full, patterns, fields, names) = data
556
557 ret = []
558 pkgtupids = set()
559 for row in self._yieldSQLDataList(patterns, fields, ignore_case):
560 pkgtupids.add(row[0])
561
562 cur = self._get_cursor()
563 sql = """SELECT tid FROM trans_data_pkgs WHERE pkgtupid IN """
564 sql += "(%s)" % ",".join(['?'] * len(pkgtupids))
565 params = list(pkgtupids)
566 tids = set()
567 if len(params) > yum.constants.PATTERNS_INDEXED_MAX:
568 executeSQL(cur, """SELECT tid FROM trans_data_pkgs""")
569 for row in cur:
570 if row[0] in params:
571 tids.add(row[0])
572 return tids
573 if not params:
574 return tids
575 executeSQL(cur, sql, params)
576 for row in cur:
577 tids.add(row[0])
578 return tids
579
580 - def _create_db_file(self):
581 """ Create a new history DB file, populating tables etc. """
582
583 _db_file = '%s/%s-%s.%s' % (self.conf.db_path,
584 'history',
585 time.strftime('%Y-%m-%d'),
586 'sqlite')
587 if self._db_file == _db_file:
588 os.rename(_db_file, _db_file + '.old')
589 self._db_file = _db_file
590
591 if self.conf.writable and not os.path.exists(self._db_file):
592
593
594 fo = os.open(self._db_file, os.O_CREAT, 0600)
595 os.close(fo)
596
597 cur = self._get_cursor()
598 ops = ['''\
599 CREATE TABLE trans_beg (
600 tid INTEGER PRIMARY KEY,
601 timestamp INTEGER NOT NULL, rpmdb_version TEXT NOT NULL,
602 loginuid INTEGER);
603 ''', '''\
604 CREATE TABLE trans_end (
605 tid INTEGER PRIMARY KEY REFERENCES trans_beg,
606 timestamp INTEGER NOT NULL, rpmdb_version TEXT NOT NULL,
607 return_code INTEGER NOT NULL);
608 ''', '''\
609 \
610 CREATE TABLE trans_with_pkgs (
611 tid INTEGER NOT NULL REFERENCES trans_beg,
612 pkgtupid INTEGER NOT NULL REFERENCES pkgtups);
613 ''', '''\
614 \
615 CREATE TABLE trans_error (
616 mid INTEGER PRIMARY KEY,
617 tid INTEGER NOT NULL REFERENCES trans_beg,
618 msg TEXT NOT NULL);
619 ''', '''\
620 CREATE TABLE trans_script_stdout (
621 lid INTEGER PRIMARY KEY,
622 tid INTEGER NOT NULL REFERENCES trans_beg,
623 line TEXT NOT NULL);
624 ''', '''\
625 \
626 CREATE TABLE trans_data_pkgs (
627 tid INTEGER NOT NULL REFERENCES trans_beg,
628 pkgtupid INTEGER NOT NULL REFERENCES pkgtups,
629 done BOOL NOT NULL DEFAULT FALSE, state TEXT NOT NULL);
630 ''', '''\
631 \
632 CREATE TABLE pkgtups (
633 pkgtupid INTEGER PRIMARY KEY, name TEXT NOT NULL, arch TEXT NOT NULL,
634 epoch TEXT NOT NULL, version TEXT NOT NULL, release TEXT NOT NULL,
635 checksum TEXT);
636 ''', '''\
637 CREATE INDEX i_pkgtup_naevr ON pkgtups (name, arch, epoch, version, release);
638 ''']
639 for op in ops:
640 cur.execute(op)
641 self._commit()
642
643
644 _FULL_PARSE_QUERY_BEG = """
645 SELECT pkgtupid,name,epoch,version,release,arch,
646 name || "." || arch AS sql_nameArch,
647 name || "-" || version || "-" || release || "." || arch AS sql_nameVerRelArch,
648 name || "-" || version AS sql_nameVer,
649 name || "-" || version || "-" || release AS sql_nameVerRel,
650 epoch || ":" || name || "-" || version || "-" || release || "." || arch AS sql_envra,
651 name || "-" || epoch || ":" || version || "-" || release || "." || arch AS sql_nevra
652 FROM pkgtups
653 WHERE
654 """
655