1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2017-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
20
21import libdnf.transaction
22
23import dnf.db.history
24import dnf.transaction
25import dnf.exceptions
26from dnf.i18n import _
27from dnf.util import logger
28
29import rpm
30
31class PersistorBase(object):
32    def __init__(self, history):
33        assert isinstance(history, dnf.db.history.SwdbInterface), str(type(history))
34        self.history = history
35        self._installed = {}
36        self._removed = {}
37        self._upgraded = {}
38
39    def __len__(self):
40        return len(self._installed) + len(self._removed) + len(self._upgraded)
41
42    def clean(self):
43        self._installed = {}
44        self._removed = {}
45        self._upgraded = {}
46
47    def _get_obj_id(self, obj):
48        raise NotImplementedError
49
50    def _add_to_history(self, item, action):
51        ti = self.history.swdb.addItem(item, "", action, libdnf.transaction.TransactionItemReason_USER)
52        ti.setState(libdnf.transaction.TransactionItemState_DONE)
53
54    def install(self, obj):
55        self._installed[self._get_obj_id(obj)] = obj
56        self._add_to_history(obj, libdnf.transaction.TransactionItemAction_INSTALL)
57
58    def remove(self, obj):
59        self._removed[self._get_obj_id(obj)] = obj
60        self._add_to_history(obj, libdnf.transaction.TransactionItemAction_REMOVE)
61
62    def upgrade(self, obj):
63        self._upgraded[self._get_obj_id(obj)] = obj
64        self._add_to_history(obj, libdnf.transaction.TransactionItemAction_UPGRADE)
65
66    def new(self, obj_id, name, translated_name, pkg_types):
67        raise NotImplementedError
68
69    def get(self, obj_id):
70        raise NotImplementedError
71
72    def search_by_pattern(self, pattern):
73        raise NotImplementedError
74
75
76class GroupPersistor(PersistorBase):
77
78    def __iter__(self):
79        items = self.history.swdb.getItems()
80        items = [i for i in items if i.getCompsGroupItem()]
81        return iter(items)
82
83    def _get_obj_id(self, obj):
84        return obj.getGroupId()
85
86    def new(self, obj_id, name, translated_name, pkg_types):
87        swdb_group = self.history.swdb.createCompsGroupItem()
88        swdb_group.setGroupId(obj_id)
89        if name is not None:
90            swdb_group.setName(name)
91        if translated_name is not None:
92            swdb_group.setTranslatedName(translated_name)
93        swdb_group.setPackageTypes(pkg_types)
94        return swdb_group
95
96    def get(self, obj_id):
97        swdb_group = self.history.swdb.getCompsGroupItem(obj_id)
98        if not swdb_group:
99            return None
100        swdb_group = swdb_group.getCompsGroupItem()
101        return swdb_group
102
103    def search_by_pattern(self, pattern):
104        return self.history.swdb.getCompsGroupItemsByPattern(pattern)
105
106    def get_package_groups(self, pkg_name):
107        return self.history.swdb.getPackageCompsGroups(pkg_name)
108
109    def is_removable_pkg(self, pkg_name):
110        # for group removal and autoremove
111        reason = self.history.swdb.resolveRPMTransactionItemReason(pkg_name, "", -2)
112        if reason != libdnf.transaction.TransactionItemReason_GROUP:
113            return False
114
115        # TODO: implement lastTransId == -2 in libdnf
116        package_groups = set(self.get_package_groups(pkg_name))
117        for group_id, group in self._removed.items():
118            for pkg in group.getPackages():
119                if pkg.getName() != pkg_name:
120                    continue
121                if not pkg.getInstalled():
122                    continue
123                package_groups.remove(group_id)
124        for group_id, group in self._installed.items():
125            for pkg in group.getPackages():
126                if pkg.getName() != pkg_name:
127                    continue
128                if not pkg.getInstalled():
129                    continue
130                package_groups.add(group_id)
131        if package_groups:
132            return False
133        return True
134
135
136class EnvironmentPersistor(PersistorBase):
137
138    def __iter__(self):
139        items = self.history.swdb.getItems()
140        items = [i for i in items if i.getCompsEnvironmentItem()]
141        return iter(items)
142
143    def _get_obj_id(self, obj):
144        return obj.getEnvironmentId()
145
146    def new(self, obj_id, name, translated_name, pkg_types):
147        swdb_env = self.history.swdb.createCompsEnvironmentItem()
148        swdb_env.setEnvironmentId(obj_id)
149        if name is not None:
150            swdb_env.setName(name)
151        if translated_name is not None:
152            swdb_env.setTranslatedName(translated_name)
153        swdb_env.setPackageTypes(pkg_types)
154        return swdb_env
155
156    def get(self, obj_id):
157        swdb_env = self.history.swdb.getCompsEnvironmentItem(obj_id)
158        if not swdb_env:
159            return None
160        swdb_env = swdb_env.getCompsEnvironmentItem()
161        return swdb_env
162
163    def search_by_pattern(self, pattern):
164        return self.history.swdb.getCompsEnvironmentItemsByPattern(pattern)
165
166    def get_group_environments(self, group_id):
167        return self.history.swdb.getCompsGroupEnvironments(group_id)
168
169    def is_removable_group(self, group_id):
170        # for environment removal
171        swdb_group = self.history.group.get(group_id)
172        if not swdb_group:
173            return False
174
175        # TODO: implement lastTransId == -2 in libdnf
176        group_environments = set(self.get_group_environments(group_id))
177        for env_id, env in self._removed.items():
178            for group in env.getGroups():
179                if group.getGroupId() != group_id:
180                    continue
181                if not group.getInstalled():
182                    continue
183                group_environments.remove(env_id)
184        for env_id, env in self._installed.items():
185            for group in env.getGroups():
186                if group.getGroupId() != group_id:
187                    continue
188                if not group.getInstalled():
189                    continue
190                group_environments.add(env_id)
191        if group_environments:
192            return False
193        return True
194
195
196class RPMTransaction(object):
197    def __init__(self, history, transaction=None):
198        self.history = history
199        self.transaction = transaction
200        if not self.transaction:
201            try:
202                self.history.swdb.initTransaction()
203            except:
204                pass
205        self._swdb_ti_pkg = {}
206
207    # TODO: close trans if needed
208
209    def __iter__(self):
210        # :api
211        if self.transaction:
212            items = self.transaction.getItems()
213        else:
214            items = self.history.swdb.getItems()
215        items = [dnf.db.history.RPMTransactionItemWrapper(self.history, i) for i in items if i.getRPMItem()]
216        return iter(items)
217
218    def __len__(self):
219        if self.transaction:
220            items = self.transaction.getItems()
221        else:
222            items = self.history.swdb.getItems()
223        items = [dnf.db.history.RPMTransactionItemWrapper(self.history, i) for i in items if i.getRPMItem()]
224        return len(items)
225
226    def _pkg_to_swdb_rpm_item(self, pkg):
227        rpm_item = self.history.swdb.createRPMItem()
228        rpm_item.setName(pkg.name)
229        rpm_item.setEpoch(pkg.epoch or 0)
230        rpm_item.setVersion(pkg.version)
231        rpm_item.setRelease(pkg.release)
232        rpm_item.setArch(pkg.arch)
233        return rpm_item
234
235    def new(self, pkg, action, reason=None, replaced_by=None):
236        rpm_item = self._pkg_to_swdb_rpm_item(pkg)
237        repoid = self.get_repoid(pkg)
238        if reason is None:
239            reason = self.get_reason(pkg)
240        result = self.history.swdb.addItem(rpm_item, repoid, action, reason)
241        if replaced_by:
242            result.addReplacedBy(replaced_by)
243        self._swdb_ti_pkg[result] = pkg
244        return result
245
246    def get_repoid(self, pkg):
247        result = getattr(pkg, "_force_swdb_repoid", None)
248        if result:
249            return result
250        return pkg.reponame
251
252    def get_reason(self, pkg):
253        """Get reason for package"""
254        return self.history.swdb.resolveRPMTransactionItemReason(pkg.name, pkg.arch, -1)
255
256    def get_reason_name(self, pkg):
257        """Get reason for package"""
258        return libdnf.transaction.TransactionItemReasonToString(self.get_reason(pkg))
259
260    def _add_obsoleted(self, obsoleted, replaced_by=None):
261        obsoleted = obsoleted or []
262        for obs in obsoleted:
263            ti = self.new(obs, libdnf.transaction.TransactionItemAction_OBSOLETED)
264            if replaced_by:
265                ti.addReplacedBy(replaced_by)
266
267    def add_downgrade(self, new, old, obsoleted=None):
268        ti_new = self.new(new, libdnf.transaction.TransactionItemAction_DOWNGRADE)
269        ti_old = self.new(old, libdnf.transaction.TransactionItemAction_DOWNGRADED, replaced_by=ti_new)
270        self._add_obsoleted(obsoleted, replaced_by=ti_new)
271
272    def add_erase(self, old, reason=None):
273        self.add_remove(old, reason)
274
275    def add_install(self, new, obsoleted=None, reason=None):
276        if reason is None:
277            reason = libdnf.transaction.TransactionItemReason_USER
278        ti_new = self.new(new, libdnf.transaction.TransactionItemAction_INSTALL, reason)
279        self._add_obsoleted(obsoleted, replaced_by=ti_new)
280
281    def add_reinstall(self, new, old, obsoleted=None):
282        ti_new = self.new(new, libdnf.transaction.TransactionItemAction_REINSTALL)
283        ti_old = self.new(old, libdnf.transaction.TransactionItemAction_REINSTALLED, replaced_by=ti_new)
284        self._add_obsoleted(obsoleted, replaced_by=ti_new)
285
286    def add_remove(self, old, reason=None):
287        reason = reason or libdnf.transaction.TransactionItemReason_USER
288        ti_old = self.new(old, libdnf.transaction.TransactionItemAction_REMOVE, reason)
289
290    def add_upgrade(self, new, old, obsoleted=None):
291        ti_new = self.new(new, libdnf.transaction.TransactionItemAction_UPGRADE)
292        ti_old = self.new(old, libdnf.transaction.TransactionItemAction_UPGRADED, replaced_by=ti_new)
293        self._add_obsoleted(obsoleted, replaced_by=ti_new)
294
295    def _test_fail_safe(self, hdr, pkg):
296        if pkg._from_cmdline:
297            return 0
298        if pkg.repo.module_hotfixes:
299            return 0
300        try:
301            if hdr['modularitylabel'] and not pkg._is_in_active_module():
302                logger.critical(_("No available modular metadata for modular package '{}', "
303                                  "it cannot be installed on the system").format(pkg))
304                return 1
305        except ValueError:
306            return 0
307        return 0
308
309    def _populate_rpm_ts(self, ts):
310        """Populate the RPM transaction set."""
311        modular_problems = 0
312
313        for tsi in self:
314            try:
315                if tsi.action == libdnf.transaction.TransactionItemAction_DOWNGRADE:
316                    hdr = tsi.pkg._header
317                    modular_problems += self._test_fail_safe(hdr, tsi.pkg)
318                    ts.addInstall(hdr, tsi, 'u')
319                elif tsi.action == libdnf.transaction.TransactionItemAction_DOWNGRADED:
320                    ts.addErase(tsi.pkg.idx)
321                elif tsi.action == libdnf.transaction.TransactionItemAction_INSTALL:
322                    hdr = tsi.pkg._header
323                    modular_problems += self._test_fail_safe(hdr, tsi.pkg)
324                    ts.addInstall(hdr, tsi, 'i')
325                elif tsi.action == libdnf.transaction.TransactionItemAction_OBSOLETE:
326                    hdr = tsi.pkg._header
327                    modular_problems += self._test_fail_safe(hdr, tsi.pkg)
328                    ts.addInstall(hdr, tsi, 'u')
329                elif tsi.action == libdnf.transaction.TransactionItemAction_OBSOLETED:
330                    ts.addErase(tsi.pkg.idx)
331                elif tsi.action == libdnf.transaction.TransactionItemAction_REINSTALL:
332                    # note: in rpm 4.12 there should not be set
333                    # rpm.RPMPROB_FILTER_REPLACEPKG to work
334                    hdr = tsi.pkg._header
335                    modular_problems += self._test_fail_safe(hdr, tsi.pkg)
336                    ts.addReinstall(hdr, tsi)
337                elif tsi.action == libdnf.transaction.TransactionItemAction_REINSTALLED:
338                    # Required when multiple packages with the same NEVRA marked as installed
339                    ts.addErase(tsi.pkg.idx)
340                elif tsi.action == libdnf.transaction.TransactionItemAction_REMOVE:
341                    ts.addErase(tsi.pkg.idx)
342                elif tsi.action == libdnf.transaction.TransactionItemAction_UPGRADE:
343                    hdr = tsi.pkg._header
344                    modular_problems += self._test_fail_safe(hdr, tsi.pkg)
345                    ts.addInstall(hdr, tsi, 'u')
346                elif tsi.action == libdnf.transaction.TransactionItemAction_UPGRADED:
347                    ts.addErase(tsi.pkg.idx)
348                elif tsi.action == libdnf.transaction.TransactionItemAction_REASON_CHANGE:
349                    pass
350                else:
351                    raise RuntimeError("TransactionItemAction not handled: %s" % tsi.action)
352            except rpm.error as e:
353                raise dnf.exceptions.Error(_("An rpm exception occurred: %s" % e))
354        if modular_problems:
355            raise dnf.exceptions.Error(_("No available modular metadata for modular package"))
356
357        return ts
358
359    @property
360    def install_set(self):
361        # :api
362        result = set()
363        for tsi in self:
364            if tsi.action in dnf.transaction.FORWARD_ACTIONS:
365                try:
366                    result.add(tsi.pkg)
367                except KeyError:
368                    raise RuntimeError("TransactionItem is has no RPM attached: %s" % tsi)
369        return result
370
371    @property
372    def remove_set(self):
373        # :api
374        result = set()
375        for tsi in self:
376            if tsi.action in dnf.transaction.BACKWARD_ACTIONS + [libdnf.transaction.TransactionItemAction_REINSTALLED]:
377                try:
378                    result.add(tsi.pkg)
379                except KeyError:
380                    raise RuntimeError("TransactionItem is has no RPM attached: %s" % tsi)
381        return result
382
383    def _rpm_limitations(self):
384        """ Ensures all the members can be passed to rpm as they are to perform
385            the transaction.
386        """
387        src_installs = [pkg for pkg in self.install_set if pkg.arch == 'src']
388        if len(src_installs):
389            return _("Will not install a source rpm package (%s).") % \
390                src_installs[0]
391        return None
392
393    def _get_items(self, action):
394        return [tsi for tsi in self if tsi.action == action]
395