1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2009, 2012-2018  Red Hat, Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU Library General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18#
19
20import calendar
21import os
22import time
23
24import libdnf.transaction
25import libdnf.utils
26
27from dnf.i18n import ucd
28from dnf.yum import misc
29from dnf.exceptions import DatabaseError
30
31from .group import GroupPersistor, EnvironmentPersistor, RPMTransaction
32
33
34class RPMTransactionItemWrapper(object):
35    def __init__(self, swdb, item):
36        assert item is not None
37        self._swdb = swdb
38        self._item = item
39
40    def __str__(self):
41        return self._item.getItem().toStr()
42
43    def __lt__(self, other):
44        return self._item < other._item
45
46    def __eq__(self, other):
47        return self._item == other._item
48
49    def __hash__(self):
50        return self._item.__hash__()
51
52    def match(self, pattern):
53        return True
54
55    def is_package(self):
56        return self._item.getRPMItem() is not None
57
58    def is_group(self):
59        return self._item.getCompsGroupItem() is not None
60
61    def is_environment(self):
62        return self._item.getCompsEnvironmentItem() is not None
63
64    def get_group(self):
65        return self._item.getCompsGroupItem()
66
67    def get_environment(self):
68        return self._item.getCompsEnvironmentItem()
69
70    @property
71    def name(self):
72        return self._item.getRPMItem().getName()
73
74    @property
75    def epoch(self):
76        return self._item.getRPMItem().getEpoch()
77
78    @property
79    def version(self):
80        return self._item.getRPMItem().getVersion()
81
82    @property
83    def release(self):
84        return self._item.getRPMItem().getRelease()
85
86    @property
87    def arch(self):
88        return self._item.getRPMItem().getArch()
89
90    @property
91    def evr(self):
92        if self.epoch:
93            return "{}:{}-{}".format(self.epoch, self.version, self.release)
94        return "{}-{}".format(self.version, self.release)
95
96    @property
97    def nevra(self):
98        return self._item.getRPMItem().getNEVRA()
99
100    @property
101    def action(self):
102        return self._item.getAction()
103
104    @action.setter
105    def action(self, value):
106        self._item.setAction(value)
107
108    @property
109    def reason(self):
110        return self._item.getReason()
111
112    @reason.setter
113    def reason(self, value):
114        return self._item.setReason(value)
115
116    @property
117    def action_name(self):
118        try:
119            return self._item.getActionName()
120        except AttributeError:
121            return ""
122
123    @property
124    def action_short(self):
125        try:
126            return self._item.getActionShort()
127        except AttributeError:
128            return ""
129
130    @property
131    def state(self):
132        return self._item.getState()
133
134    @state.setter
135    def state(self, value):
136        self._item.setState(value)
137
138    @property
139    def from_repo(self):
140        return self._item.getRepoid()
141
142    def ui_from_repo(self):
143        if not self._item.getRepoid():
144            return ""
145        return "@" + self._item.getRepoid()
146
147    @property
148    def obsoleting(self):
149        return None
150
151    def get_reason(self):
152        # TODO: get_history_reason
153        return self._swdb.rpm.get_reason(self)
154
155    @property
156    def pkg(self):
157        return self._swdb.rpm._swdb_ti_pkg[self._item]
158
159    @property
160    def files(self):
161        return self.pkg.files
162
163    @property
164    def _active(self):
165        return self.pkg
166
167
168class TransactionWrapper(object):
169
170    altered_lt_rpmdb = False
171    altered_gt_rpmdb = False
172
173    def __init__(self, trans):
174        self._trans = trans
175
176    @property
177    def tid(self):
178        return self._trans.getId()
179
180    @property
181    def cmdline(self):
182        return self._trans.getCmdline()
183
184    @property
185    def releasever(self):
186        return self._trans.getReleasever()
187
188    @property
189    def beg_timestamp(self):
190        return self._trans.getDtBegin()
191
192    @property
193    def end_timestamp(self):
194        return self._trans.getDtEnd()
195
196    @property
197    def beg_rpmdb_version(self):
198        return self._trans.getRpmdbVersionBegin()
199
200    @property
201    def end_rpmdb_version(self):
202        return self._trans.getRpmdbVersionEnd()
203
204    @property
205    def return_code(self):
206        return int(self._trans.getState() != libdnf.transaction.TransactionItemState_DONE)
207
208    @property
209    def loginuid(self):
210        return self._trans.getUserId()
211
212    @property
213    def data(self):
214        return self.packages
215
216    @property
217    def is_output(self):
218        output = self._trans.getConsoleOutput()
219        return bool(output)
220
221    @property
222    def comment(self):
223        return self._trans.getComment()
224
225    def tids(self):
226        return [self._trans.getId()]
227
228    def performed_with(self):
229        return []
230
231    def packages(self):
232        result = self._trans.getItems()
233        return [RPMTransactionItemWrapper(self, i) for i in result]
234
235    def output(self):
236        return [i[1] for i in self._trans.getConsoleOutput()]
237
238    def error(self):
239        return []
240
241    def compare_rpmdbv(self, rpmdbv):
242        self.altered_gt_rpmdb = self._trans.getRpmdbVersionEnd() != rpmdbv
243
244
245class MergedTransactionWrapper(TransactionWrapper):
246
247    def __init__(self, trans):
248        self._trans = libdnf.transaction.MergedTransaction(trans._trans)
249
250    def merge(self, trans):
251        self._trans.merge(trans._trans)
252
253    @property
254    def loginuid(self):
255        return self._trans.listUserIds()
256
257    def tids(self):
258        return self._trans.listIds()
259
260    @property
261    def return_code(self):
262        return [int(i != libdnf.transaction.TransactionItemState_DONE) for i in self._trans.listStates()]
263
264    @property
265    def cmdline(self):
266        return self._trans.listCmdlines()
267
268    @property
269    def releasever(self):
270        return self._trans.listReleasevers()
271
272    @property
273    def comment(self):
274        return self._trans.listComments()
275
276    def output(self):
277        return [i[1] for i in self._trans.getConsoleOutput()]
278
279class SwdbInterface(object):
280
281    def __init__(self, db_dir, releasever=""):
282        # TODO: record all vars
283        # TODO: remove relreasever from options
284        self.releasever = str(releasever)
285        self._rpm = None
286        self._group = None
287        self._env = None
288        self._addon_data = None
289        self._swdb = None
290        self._db_dir = db_dir
291        self._output = []
292
293    def __del__(self):
294        self.close()
295
296    @property
297    def rpm(self):
298        if self._rpm is None:
299            self._rpm = RPMTransaction(self)
300        return self._rpm
301
302    @property
303    def group(self):
304        if self._group is None:
305            self._group = GroupPersistor(self)
306        return self._group
307
308    @property
309    def env(self):
310        if self._env is None:
311            self._env = EnvironmentPersistor(self)
312        return self._env
313
314    @property
315    def dbpath(self):
316        return os.path.join(self._db_dir, libdnf.transaction.Swdb.defaultDatabaseName)
317
318    @property
319    def swdb(self):
320        """ Lazy initialize Swdb object """
321        if not self._swdb:
322            # _db_dir == persistdir which is prepended with installroot already
323            try:
324                self._swdb = libdnf.transaction.Swdb(self.dbpath)
325            except RuntimeError as ex:
326                raise DatabaseError(str(ex))
327            self._swdb.initTransaction()
328            # TODO: vars -> libdnf
329        return self._swdb
330
331    def transform(self, input_dir):
332        transformer = libdnf.transaction.Transformer(input_dir, self.dbpath)
333        transformer.transform()
334
335    def close(self):
336        try:
337            del self._tid
338        except AttributeError:
339            pass
340        self._rpm = None
341        self._group = None
342        self._env = None
343        if self._swdb:
344            self._swdb.closeTransaction()
345            self._swdb.closeDatabase()
346        self._swdb = None
347        self._output = []
348
349    @property
350    def path(self):
351        return self.swdb.getPath()
352
353    def reset_db(self):
354        return self.swdb.resetDatabase()
355
356    # TODO: rename to get_last_transaction?
357    def last(self, complete_transactions_only=True):
358        # TODO: complete_transactions_only
359        t = self.swdb.getLastTransaction()
360        if not t:
361            return None
362        return TransactionWrapper(t)
363
364    # TODO: rename to: list_transactions?
365    def old(self, tids=None, limit=0, complete_transactions_only=False):
366        tids = tids or []
367        tids = [int(i) for i in tids]
368        result = self.swdb.listTransactions()
369        result = [TransactionWrapper(i) for i in result]
370        # TODO: move to libdnf
371        if tids:
372            result = [i for i in result if i.tid in tids]
373
374        # populate altered_lt_rpmdb and altered_gt_rpmdb
375        for i, trans in enumerate(result):
376            if i == 0:
377                continue
378            prev_trans = result[i-1]
379            if trans._trans.getRpmdbVersionBegin() != prev_trans._trans.getRpmdbVersionEnd():
380                trans.altered_lt_rpmdb = True
381                prev_trans.altered_gt_rpmdb = True
382        return result[::-1]
383
384    def get_current(self):
385        return TransactionWrapper(self.swdb.getCurrent())
386
387    def set_reason(self, pkg, reason):
388        """Set reason for package"""
389        rpm_item = self.rpm._pkg_to_swdb_rpm_item(pkg)
390        repoid = self.repo(pkg)
391        action = libdnf.transaction.TransactionItemAction_REASON_CHANGE
392        ti = self.swdb.addItem(rpm_item, repoid, action, reason)
393        ti.setState(libdnf.transaction.TransactionItemState_DONE)
394        return ti
395
396    '''
397    def package(self, pkg):
398        """Get SwdbPackage from package"""
399        return self.swdb.package(str(pkg))
400    '''
401
402    def repo(self, pkg):
403        """Get repository of package"""
404        return self.swdb.getRPMRepo(str(pkg))
405
406    def package_data(self, pkg):
407        """Get package data for package"""
408        # trans item is returned
409        result = self.swdb.getRPMTransactionItem(str(pkg))
410        if result is None:
411            return result
412        result = RPMTransactionItemWrapper(self, result)
413        return result
414
415#    def reason(self, pkg):
416#        """Get reason for package"""
417#        result = self.swdb.resolveRPMTransactionItemReason(pkg.name, pkg.arch, -1)
418#        return result
419
420    # TODO: rename to begin_transaction?
421    def beg(self, rpmdb_version, using_pkgs, tsis, cmdline=None, comment=""):
422        try:
423            self.swdb.initTransaction()
424        except:
425            pass
426
427        tid = self.swdb.beginTransaction(
428            int(calendar.timegm(time.gmtime())),
429            str(rpmdb_version),
430            cmdline or "",
431            int(misc.getloginuid()),
432            comment)
433        self.swdb.setReleasever(self.releasever)
434        self._tid = tid
435
436        return tid
437
438    def pkg_to_swdb_rpm_item(self, po):
439        rpm_item = self.swdb.createRPMItem()
440        rpm_item.setName(po.name)
441        rpm_item.setEpoch(po.epoch or 0)
442        rpm_item.setVersion(po.version)
443        rpm_item.setRelease(po.release)
444        rpm_item.setArch(po.arch)
445        return rpm_item
446
447    def log_scriptlet_output(self, msg):
448        if not hasattr(self, '_tid'):
449            return
450        if not msg:
451            return
452        for line in msg.splitlines():
453            line = ucd(line)
454            # logging directly to database fails if transaction runs in a background process
455            self._output.append((1, line))
456
457    '''
458    def _log_errors(self, errors):
459        for error in errors:
460            error = ucd(error)
461            self.swdb.log_error(self._tid, error)
462    '''
463
464    def end(self, end_rpmdb_version="", return_code=None, errors=None):
465        if not hasattr(self, '_tid'):
466            return  # Failed at beg() time
467
468        if return_code is None:
469            # return_code/state auto-detection
470            return_code = libdnf.transaction.TransactionState_DONE
471            for tsi in self.rpm:
472                if tsi.state == libdnf.transaction.TransactionItemState_ERROR:
473                    return_code = libdnf.transaction.TransactionState_ERROR
474                    break
475
476        for file_descriptor, line in self._output:
477            self.swdb.addConsoleOutputLine(file_descriptor, line)
478        self._output = []
479
480        self.swdb.endTransaction(
481            int(time.time()),
482            str(end_rpmdb_version),
483            return_code,
484        )
485
486        # Closing and cleanup is done in the close() method.
487        # It is important to keep data around after the transaction ends
488        # because it's needed by plugins to report installed packages etc.
489
490    # TODO: ignore_case, more patterns
491    def search(self, patterns, ignore_case=True):
492        """ Search for history transactions which contain specified
493            packages al. la. "yum list". Returns transaction ids. """
494        return self.swdb.searchTransactionsByRPM(patterns)
495
496    def user_installed(self, pkg):
497        """Returns True if package is user installed"""
498        reason = self.swdb.resolveRPMTransactionItemReason(pkg.name, pkg.arch, -1)
499        if reason == libdnf.transaction.TransactionItemReason_USER:
500            return True
501        # if reason is not known, consider a package user-installed
502        # because it was most likely installed via rpm
503        if reason == libdnf.transaction.TransactionItemReason_UNKNOWN:
504            return True
505        return False
506
507    def get_erased_reason(self, pkg, first_trans, rollback):
508        """Get reason of package before transaction being undone. If package
509        is already installed in the system, keep his reason.
510
511        :param pkg: package being installed
512        :param first_trans: id of first transaction being undone
513        :param rollback: True if transaction is performing a rollback"""
514        if rollback:
515            # return the reason at the point of rollback; we're setting that reason
516            result = self.swdb.resolveRPMTransactionItemReason(pkg.name, pkg.arch, first_trans)
517        else:
518            result = self.swdb.resolveRPMTransactionItemReason(pkg.name, pkg.arch, -1)
519
520        # consider unknown reason as user-installed
521        if result == libdnf.transaction.TransactionItemReason_UNKNOWN:
522            result = libdnf.transaction.TransactionItemReason_USER
523        return result
524