summaryrefslogtreecommitdiffstats
path: root/src/software/test/package.py
blob: 597873085dcbec66c0aff8c79f6bc532e29e912e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#!/usr/bin/python
# -*- Coding:utf-8 -*-
#
# Copyright (C) 2012-2014 Red Hat, Inc.  All rights reserved.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details. #
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
#
# Authors: Michal Minar <miminar@redhat.com>
"""
Abstraction for RPM package for test purposes.
"""

import re
import subprocess

import util

RE_NOT_INSTALLED = re.compile(r'package\s+([^[:space:]]+)\s+is not installed',
        re.IGNORECASE)

class Package(object):
    """
    Element of test package database. It's a container for package
    informations.
    """
    def __init__(self, name, epoch, ver, rel, arch, repoid,
            rpm_path, files=None):
        self._name = name
        if not epoch or (isinstance(epoch, basestring)
                and epoch.lower() == "(none)"):
            epoch = "0"
        self._epoch = int(epoch)
        self._ver = ver
        self._rel = rel
        self._arch = arch
        self._repoid = repoid
        self._rpm_path = rpm_path
        if files is None:
            files = set()
        else:
            files = set(files)
        self._files = files

    def __str__(self):
        return self.get_nevra()

    @property
    def name(self):
        """ :returns: Package name. """
        return self._name
    @property
    def epoch(self):
        """ :returns: Package epoch as an integer. """
        return self._epoch
    @property
    def ver(self):
        """ :returns: Version string of package. """
        return self._ver
    @property
    def rel(self):
        """ :returns: Release string of package. """
        return self._rel
    @property
    def arch(self):
        """ :returns: Architecture string of package. """
        return self._arch
    @property
    def repoid(self):
        """ :returns: Repository id of package, where it is available. """
        return self._repoid
    @property
    def nevra(self):
        """
        :returns: Nevra string of package with epoch part always present.
        """
        return self.get_nevra('ALWAYS')
    @property
    def evra(self):
        """
        :returns: Evra string of package. That's the same as *nevra* without a
            name.
        """
        attrs = ('epoch', 'ver', 'rel', 'arch')
        return util.make_evra(*[getattr(self, '_'+a) for a in attrs])
    @property
    def rpm_path(self):
        """
        :returns: Absolute path to rpm package.
        """
        return self._rpm_path

    @property
    def summary(self):
        """
        :returns: Package summary string.
        """
        return util.check_output(
                ['/bin/rpm', '-q', '--qf', '%{SUMMARY}', '-p',
                    self.rpm_path])

    def get_nevra(self, with_epoch='NOT_ZERO'):
        """
        :param string with_epoch: Says when the epoch part should appear
            in resulting string. There are following possible values:

            * NOT_ZERO - epoch shall be present if it's greater than 0
            * ALWAYS - epoch will be present
            * NEVER - epoch won't be present

        :returns: Package nevra string.
        :rtype: string
        """
        attrs = ('name', 'epoch', 'ver', 'rel', 'arch')
        return util.make_nevra(*[getattr(self, '_'+a) for a in attrs],
                with_epoch=with_epoch)

    def __contains__(self, path):
        return path in self._files
    def __iter__(self):
        for pkg in self._files.__iter__():
            yield pkg
    def __len__(self):
        return len(self._files)
    @property
    def files(self):
        """
        :returns: Set of files and directories installed by this package.
        :rtype: set
        """
        return self._files.copy()

def to_json(_encoder, pkg):
    """
    Converts package object to dictionary which json encoder can handle.

    :param _encoder: Instance of json encoder.
    :param pkg: Package object to convert.
    :type pkg: :py:class:`Package`
    :returns: Dictionary with package attributes.
    :rtype: dictionary
    """
    if not isinstance(pkg, Package):
        raise TypeError("not a Package object")
    return pkg.__dict__

def from_json(json_object):
    """
    Constructs a package object from dictionary loaded from json text.
    Inverse function to :py:func:`to_json`.

    :param dictionary json_object: Deserialized package as a dictionary.
    :returns: Package object.
    :rtype: :py:class:`Package`
    """
    if isinstance(json_object, dict) and '_arch' in json_object:
        kwargs = dict((k[1:],v) for k,v in json_object.items())
        return Package(**kwargs)
    return json_object

def is_pkg_installed(pkg):
    """
    Check, whether package is installed.
    """
    if not isinstance(pkg, Package):
        match = util.RE_NEVRA.match(pkg)
        if not match:
            match = util.RE_ENVRA.match(pkg)
        if not match:
           return subprocess.call(["rpm", "--quiet", "-q", pkg]) == 0
        cmp_nvra = "%s-%s-%s.%s" % (match.group('name'),
                   match.group('ver'), match.group('rel'), match.group('arch'))
        cmp_epoch = match.group('epoch')
    else:
        cmp_nvra = pkg.get_nevra('NEVER')
        cmp_epoch = pkg.epoch

    cmd = ["/bin/rpm", "-q", "--qf", "%{EPOCH}:%{NVRA}\n", cmp_nvra]
    try:
        out = util.check_output(cmd).splitlines()[0]
        epoch, _ = out.split(':')
        if not epoch or epoch.lower() == "(none)":
            epoch = "0"
        return int(epoch) == int(cmp_epoch)
    except subprocess.CalledProcessError:
        return False

def filter_installed_packages(pkgs, installed=True):
    """
    Filter the package set returning only installed or not installed packages.

    :param list pkgs: Packages to filter. If removed packages are requested
        only Package objects may be present, otherwise package names or nevra
        strings are allowed.
    :param boolean installed: Whether to return installed or uninstalled
        packages.
    :returns: Filtered packages. Nevra strings are returned for any installed
        packages from *pkgs* represented by strings (either by name or nevra),
        package objects will be returned as package objects.
    :rtype: set
    """
    if len(pkgs) < 1:
        return set()
    pkg_strings = []
    pkg_map = {}
    for pkg in pkgs:
        if isinstance(pkg, Package):
            pkg_map[pkg.get_nevra('NEVER')] = pkg
            pkg = pkg.get_nevra('NEVER')
        elif not installed:
            raise TypeError("packages must be objects of Package")
        pkg_strings.append(pkg)
    cmd = [ "/bin/rpm", "-q", "--qf"
          , "%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}\n"]
    cmd.extend(pkg_strings)
    process = subprocess.Popen(cmd,
            stdout=subprocess.PIPE, stderr=util.DEV_NULL)
    out, _ = process.communicate()
    if installed:
        result = set()
        for line in out.splitlines():
            if not util.RE_NEVRA_OPT_EPOCH.match(line):
                continue
            if line in result:
                continue
            result.add(pkg_map.get(line, line))
    else:
        result = set(pkg_strings)
        for match in RE_NOT_INSTALLED.finditer(out):
            if match.group(1) not in pkg_strings:
                raise ValueError('got unexpected package nevra "%s" which'
                        ' should be one of: %s'
                        % (match.group(1), str(pkg_strings)))
            pkg_strings.remove(match.group(1))
            result.remove(result)
        result = set(pkg_map.get(pstr, pstr) for pstr in result)
    return result

def remove_pkgs(pkgs, *args, **kwargs):
    """
    Remove package with rpm command.

    :param pkgs: Either an instance of :py:class:`Package` or package name.
        If it's a name, any version will be removed. Otherwise the exact
        version must be installed for command to be successful.
    :param list args: List of parameters for rpm command.
    :param boolean suppress_stderr: Whether the standard error output of ``rpm``
        command shall be suppressed. Defaults to ``False``.
    """
    suppress_stderr = kwargs.pop('suppress_stderr', False)
    cmd = ["rpm", "--quiet"] + list(args) + ['-e']
    if isinstance(pkgs, (basestring, Package)):
        pkgs = [pkgs]
    pkg_strings = []
    for pkg in pkgs:
        if isinstance(pkg, Package):
            pkg_strings.append(pkg.get_nevra('NEVER'))
        else:
            pkg_strings.append(pkg)
    if len(pkg_strings) > 0:
        cmd.extend(pkg_strings)
        kwargs = {}
        if suppress_stderr:
            kwargs['stderr'] = util.DEV_NULL
        subprocess.call(cmd, **kwargs)

def install_pkgs(pkgs, *args):
    """
    Install a specific package.

    :param pkgs: Package object.
    :type pkgs: :py:class:`Package`
    """
    cmd = ["rpm", "--quiet"] + list(args) + ["-i"]
    if isinstance(pkgs, Package):
        pkgs = [pkgs]
    pkg_paths = []
    for pkg in pkgs:
        if not isinstance(pkg, Package):
            raise TypeError("pkg must be a Package instance")
        pkg_paths.append(pkg.rpm_path)
    if len(pkg_paths) > 0:
        cmd.extend(pkg_paths)
        subprocess.call(cmd)