Blame dnf/yum/misc.py

Packit 6f3914
# misc.py
Packit 6f3914
# Copyright (C) 2012-2016 Red Hat, Inc.
Packit 6f3914
#
Packit 6f3914
# This copyrighted material is made available to anyone wishing to use,
Packit 6f3914
# modify, copy, or redistribute it subject to the terms and conditions of
Packit 6f3914
# the GNU General Public License v.2, or (at your option) any later version.
Packit 6f3914
# This program is distributed in the hope that it will be useful, but WITHOUT
Packit 6f3914
# ANY WARRANTY expressed or implied, including the implied warranties of
Packit 6f3914
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
Packit 6f3914
# Public License for more details.  You should have received a copy of the
Packit 6f3914
# GNU General Public License along with this program; if not, write to the
Packit 6f3914
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
Packit 6f3914
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
Packit 6f3914
# source code or documentation are not subject to the GNU General Public
Packit 6f3914
# License and may only be used or replicated with the express permission of
Packit 6f3914
# Red Hat, Inc.
Packit 6f3914
#
Packit 6f3914
Packit 6f3914
"""
Packit 6f3914
Assorted utility functions for yum.
Packit 6f3914
"""
Packit 6f3914
Packit 6f3914
from __future__ import print_function, absolute_import
Packit 6f3914
from __future__ import unicode_literals
Packit 6f3914
from dnf.exceptions import MiscError
Packit 6f3914
from dnf.pycomp import base64_decodebytes, basestring, unicode
Packit 6f3914
from stat import *
Packit 6f3914
import libdnf.utils
Packit 6f3914
import dnf.const
Packit 6f3914
import dnf.crypto
Packit 6f3914
import dnf.exceptions
Packit 6f3914
import dnf.i18n
Packit 6f3914
import errno
Packit 6f3914
import glob
Packit 6f3914
import hashlib
Packit 6f3914
import io
Packit 6f3914
import os
Packit 6f3914
import os.path
Packit 6f3914
import pwd
Packit 6f3914
import re
Packit 6f3914
import shutil
Packit 6f3914
import tempfile
Packit 6f3914
Packit 6f3914
_available_checksums = set(['md5', 'sha1', 'sha256', 'sha384', 'sha512'])
Packit 6f3914
_default_checksums = ['sha256']
Packit 6f3914
Packit 6f3914
Packit 6f3914
_re_compiled_glob_match = None
Packit 6f3914
def re_glob(s):
Packit 6f3914
    """ Tests if a string is a shell wildcard. """
Packit 6f3914
    global _re_compiled_glob_match
Packit 6f3914
    if _re_compiled_glob_match is None:
Packit 6f3914
        _re_compiled_glob_match = re.compile(r'[*?]|\[.+\]').search
Packit 6f3914
    return _re_compiled_glob_match(s)
Packit 6f3914
Packit 6f3914
_re_compiled_full_match = None
Packit 6f3914
def re_full_search_needed(s):
Packit 6f3914
    """ Tests if a string needs a full nevra match, instead of just name. """
Packit 6f3914
    global _re_compiled_full_match
Packit 6f3914
    if _re_compiled_full_match is None:
Packit 6f3914
        # A glob, or a "." or "-" separator, followed by something (the ".")
Packit 6f3914
        one = re.compile(r'.*([-.*?]|\[.+\]).').match
Packit 6f3914
        # Any epoch, for envra
Packit 6f3914
        two = re.compile('[0-9]+:').match
Packit 6f3914
        _re_compiled_full_match = (one, two)
Packit 6f3914
    for rec in _re_compiled_full_match:
Packit 6f3914
        if rec(s):
Packit 6f3914
            return True
Packit 6f3914
    return False
Packit 6f3914
Packit 6f3914
Packit 6f3914
class Checksums(object):
Packit 6f3914
    """ Generate checksum(s), on given pieces of data. Producing the
Packit 6f3914
        Length and the result(s) when complete. """
Packit 6f3914
Packit 6f3914
    def __init__(self, checksums=None, ignore_missing=False, ignore_none=False):
Packit 6f3914
        if checksums is None:
Packit 6f3914
            checksums = _default_checksums
Packit 6f3914
        self._sumalgos = []
Packit 6f3914
        self._sumtypes = []
Packit 6f3914
        self._len = 0
Packit 6f3914
Packit 6f3914
        done = set()
Packit 6f3914
        for sumtype in checksums:
Packit 6f3914
            if sumtype == 'sha':
Packit 6f3914
                sumtype = 'sha1'
Packit 6f3914
            if sumtype in done:
Packit 6f3914
                continue
Packit 6f3914
Packit 6f3914
            if sumtype in _available_checksums:
Packit 6f3914
                sumalgo = hashlib.new(sumtype)
Packit 6f3914
            elif ignore_missing:
Packit 6f3914
                continue
Packit 6f3914
            else:
Packit 6f3914
                raise MiscError('Error Checksumming, bad checksum type %s' %
Packit 6f3914
                                sumtype)
Packit 6f3914
            done.add(sumtype)
Packit 6f3914
            self._sumtypes.append(sumtype)
Packit 6f3914
            self._sumalgos.append(sumalgo)
Packit 6f3914
        if not done and not ignore_none:
Packit 6f3914
            raise MiscError('Error Checksumming, no valid checksum type')
Packit 6f3914
Packit 6f3914
    def __len__(self):
Packit 6f3914
        return self._len
Packit 6f3914
Packit 6f3914
    # Note that len(x) is assert limited to INT_MAX, which is 2GB on i686.
Packit 6f3914
    length = property(fget=lambda self: self._len)
Packit 6f3914
Packit 6f3914
    def update(self, data):
Packit 6f3914
        self._len += len(data)
Packit 6f3914
        for sumalgo in self._sumalgos:
Packit 6f3914
            data = data.encode('utf-8') if isinstance(data, unicode) else data
Packit 6f3914
            sumalgo.update(data)
Packit 6f3914
Packit 6f3914
    def read(self, fo, size=2**16):
Packit 6f3914
        data = fo.read(size)
Packit 6f3914
        self.update(data)
Packit 6f3914
        return data
Packit 6f3914
Packit 6f3914
    def hexdigests(self):
Packit 6f3914
        ret = {}
Packit 6f3914
        for sumtype, sumdata in zip(self._sumtypes, self._sumalgos):
Packit 6f3914
            ret[sumtype] = sumdata.hexdigest()
Packit 6f3914
        return ret
Packit 6f3914
Packit 6f3914
    def hexdigest(self, checksum=None):
Packit 6f3914
        if checksum is None:
Packit 6f3914
            if not self._sumtypes:
Packit 6f3914
                return None
Packit 6f3914
            checksum = self._sumtypes[0]
Packit 6f3914
        if checksum == 'sha':
Packit 6f3914
            checksum = 'sha1'
Packit 6f3914
        return self.hexdigests()[checksum]
Packit 6f3914
Packit 6f3914
    def digests(self):
Packit 6f3914
        ret = {}
Packit 6f3914
        for sumtype, sumdata in zip(self._sumtypes, self._sumalgos):
Packit 6f3914
            ret[sumtype] = sumdata.digest()
Packit 6f3914
        return ret
Packit 6f3914
Packit 6f3914
    def digest(self, checksum=None):
Packit 6f3914
        if checksum is None:
Packit 6f3914
            if not self._sumtypes:
Packit 6f3914
                return None
Packit 6f3914
            checksum = self._sumtypes[0]
Packit 6f3914
        if checksum == 'sha':
Packit 6f3914
            checksum = 'sha1'
Packit 6f3914
        return self.digests()[checksum]
Packit 6f3914
Packit 6f3914
def get_default_chksum_type():
Packit 6f3914
    return _default_checksums[0]
Packit 6f3914
Packit 6f3914
def checksum(sumtype, file, CHUNK=2**16, datasize=None):
Packit 6f3914
    """takes filename, hand back Checksum of it
Packit 6f3914
       sumtype = md5 or sha/sha1/sha256/sha512 (note sha == sha1)
Packit 6f3914
       filename = /path/to/file
Packit 6f3914
       CHUNK=65536 by default"""
Packit 6f3914
Packit 6f3914
    # chunking brazenly lifted from Ryan Tomayko
Packit 6f3914
Packit 6f3914
    if isinstance(file, basestring):
Packit 6f3914
        try:
Packit 6f3914
            with open(file, 'rb', CHUNK) as fo:
Packit 6f3914
                return checksum(sumtype, fo, CHUNK, datasize)
Packit 6f3914
        except (IOError, OSError):
Packit 6f3914
            raise MiscError('Error opening file for checksum: %s' % file)
Packit 6f3914
Packit 6f3914
    try:
Packit 6f3914
        # assumes file is a file-like-object
Packit 6f3914
        data = Checksums([sumtype])
Packit 6f3914
        while data.read(file, CHUNK):
Packit 6f3914
            if datasize is not None and data.length > datasize:
Packit 6f3914
                break
Packit 6f3914
Packit 6f3914
        # This screws up the length, but that shouldn't matter. We only care
Packit 6f3914
        # if this checksum == what we expect.
Packit 6f3914
        if datasize is not None and datasize != data.length:
Packit 6f3914
            return '!%u!%s' % (datasize, data.hexdigest(sumtype))
Packit 6f3914
Packit 6f3914
        return data.hexdigest(sumtype)
Packit 6f3914
    except (IOError, OSError) as e:
Packit 6f3914
        raise MiscError('Error reading file for checksum: %s' % file)
Packit 6f3914
Packit 6f3914
class GenericHolder(object):
Packit 6f3914
    """Generic Holder class used to hold other objects of known types
Packit 6f3914
       It exists purely to be able to do object.somestuff, object.someotherstuff
Packit 6f3914
       or object[key] and pass object to another function that will
Packit 6f3914
       understand it"""
Packit 6f3914
Packit 6f3914
    def __init__(self, iter=None):
Packit 6f3914
        self.__iter = iter
Packit 6f3914
Packit 6f3914
    def __iter__(self):
Packit 6f3914
        if self.__iter is not None:
Packit 6f3914
            return iter(self[self.__iter])
Packit 6f3914
Packit 6f3914
    def __getitem__(self, item):
Packit 6f3914
        if hasattr(self, item):
Packit 6f3914
            return getattr(self, item)
Packit 6f3914
        else:
Packit 6f3914
            raise KeyError(item)
Packit 6f3914
Packit 6f3914
    def all_lists(self):
Packit 6f3914
        """Return a dictionary of all lists."""
Packit 6f3914
        return {key: list_ for key, list_ in vars(self).items()
Packit 6f3914
                if type(list_) is list}
Packit 6f3914
Packit 6f3914
    def merge_lists(self, other):
Packit 6f3914
        """ Concatenate the list attributes from 'other' to ours. """
Packit 6f3914
        for (key, val) in other.all_lists().items():
Packit 6f3914
            vars(self).setdefault(key, []).extend(val)
Packit 6f3914
        return self
Packit 6f3914
Packit 6f3914
def procgpgkey(rawkey):
Packit 6f3914
    '''Convert ASCII-armored GPG key to binary
Packit 6f3914
    '''
Packit 6f3914
Packit 6f3914
    # Normalise newlines
Packit 6f3914
    rawkey = re.sub(b'\r\n?', b'\n', rawkey)
Packit 6f3914
Packit 6f3914
    # Extract block
Packit 6f3914
    block = io.BytesIO()
Packit 6f3914
    inblock = 0
Packit 6f3914
    pastheaders = 0
Packit 6f3914
    for line in rawkey.split(b'\n'):
Packit 6f3914
        if line.startswith(b'-----BEGIN PGP PUBLIC KEY BLOCK-----'):
Packit 6f3914
            inblock = 1
Packit 6f3914
        elif inblock and line.strip() == b'':
Packit 6f3914
            pastheaders = 1
Packit 6f3914
        elif inblock and line.startswith(b'-----END PGP PUBLIC KEY BLOCK-----'):
Packit 6f3914
            # Hit the end of the block, get out
Packit 6f3914
            break
Packit 6f3914
        elif pastheaders and line.startswith(b'='):
Packit 6f3914
            # Hit the CRC line, don't include this and stop
Packit 6f3914
            break
Packit 6f3914
        elif pastheaders:
Packit 6f3914
            block.write(line + b'\n')
Packit 6f3914
Packit 6f3914
    # Decode and return
Packit 6f3914
    return base64_decodebytes(block.getvalue())
Packit 6f3914
Packit 6f3914
Packit 6f3914
def keyInstalled(ts, keyid, timestamp):
Packit 6f3914
    '''
Packit 6f3914
    Return if the GPG key described by the given keyid and timestamp are
Packit 6f3914
    installed in the rpmdb.
Packit 6f3914
Packit 6f3914
    The keyid and timestamp should both be passed as integers.
Packit 6f3914
    The ts is an rpm transaction set object
Packit 6f3914
Packit 6f3914
    Return values:
Packit 6f3914
        - -1      key is not installed
Packit 6f3914
        - 0       key with matching ID and timestamp is installed
Packit 6f3914
        - 1       key with matching ID is installed but has an older timestamp
Packit 6f3914
        - 2       key with matching ID is installed but has a newer timestamp
Packit 6f3914
Packit 6f3914
    No effort is made to handle duplicates. The first matching keyid is used to
Packit 6f3914
    calculate the return result.
Packit 6f3914
    '''
Packit 6f3914
    # Search
Packit 6f3914
    for hdr in ts.dbMatch('name', 'gpg-pubkey'):
Packit 6f3914
        if hdr['version'] == keyid:
Packit 6f3914
            installedts = int(hdr['release'], 16)
Packit 6f3914
            if installedts == timestamp:
Packit 6f3914
                return 0
Packit 6f3914
            elif installedts < timestamp:
Packit 6f3914
                return 1
Packit 6f3914
            else:
Packit 6f3914
                return 2
Packit 6f3914
Packit 6f3914
    return -1
Packit 6f3914
Packit 6f3914
Packit 6f3914
def import_key_to_pubring(rawkey, keyid, gpgdir=None, make_ro_copy=True):
Packit 6f3914
    if not os.path.exists(gpgdir):
Packit 6f3914
        os.makedirs(gpgdir)
Packit 6f3914
Packit 6f3914
    with dnf.crypto.pubring_dir(gpgdir), dnf.crypto.Context() as ctx:
Packit 6f3914
        # import the key
Packit 6f3914
        with open(os.path.join(gpgdir, 'gpg.conf'), 'wb') as fp:
Packit 6f3914
            fp.write(b'')
Packit 6f3914
        ctx.op_import(rawkey)
Packit 6f3914
Packit 6f3914
        if make_ro_copy:
Packit 6f3914
Packit 6f3914
            rodir = gpgdir + '-ro'
Packit 6f3914
            if not os.path.exists(rodir):
Packit 6f3914
                os.makedirs(rodir, mode=0o755)
Packit 6f3914
                for f in glob.glob(gpgdir + '/*'):
Packit 6f3914
                    basename = os.path.basename(f)
Packit 6f3914
                    ro_f = rodir + '/' + basename
Packit 6f3914
                    shutil.copy(f, ro_f)
Packit 6f3914
                    os.chmod(ro_f, 0o755)
Packit 6f3914
                # yes it is this stupid, why do you ask?
Packit 6f3914
                opts = """lock-never
Packit 6f3914
    no-auto-check-trustdb
Packit 6f3914
    trust-model direct
Packit 6f3914
    no-expensive-trust-checks
Packit 6f3914
    no-permission-warning
Packit 6f3914
    preserve-permissions
Packit 6f3914
    """
Packit 6f3914
                with open(os.path.join(rodir, 'gpg.conf'), 'w', 0o755) as fp:
Packit 6f3914
                    fp.write(opts)
Packit 6f3914
Packit 6f3914
Packit 6f3914
        return True
Packit 6f3914
Packit 6f3914
Packit 6f3914
def getCacheDir():
Packit 6f3914
    """return a path to a valid and safe cachedir - only used when not running
Packit 6f3914
       as root or when --tempcache is set"""
Packit 6f3914
Packit 6f3914
    uid = os.geteuid()
Packit 6f3914
    try:
Packit 6f3914
        usertup = pwd.getpwuid(uid)
Packit 6f3914
        username = dnf.i18n.ucd(usertup[0])
Packit 6f3914
        prefix = '%s-%s-' % (dnf.const.PREFIX, username)
Packit 6f3914
    except KeyError:
Packit 6f3914
        prefix = '%s-%s-' % (dnf.const.PREFIX, uid)
Packit 6f3914
Packit 6f3914
    # check for /var/tmp/prefix-* -
Packit 6f3914
    dirpath = '%s/%s*' % (dnf.const.TMPDIR, prefix)
Packit 6f3914
    cachedirs = sorted(glob.glob(dirpath))
Packit 6f3914
    for thisdir in cachedirs:
Packit 6f3914
        stats = os.lstat(thisdir)
Packit 6f3914
        if S_ISDIR(stats[0]) and S_IMODE(stats[0]) == 448 and stats[4] == uid:
Packit 6f3914
            return thisdir
Packit 6f3914
Packit 6f3914
    # make the dir (tempfile.mkdtemp())
Packit 6f3914
    cachedir = tempfile.mkdtemp(prefix=prefix, dir=dnf.const.TMPDIR)
Packit 6f3914
    return cachedir
Packit 6f3914
Packit 6f3914
def seq_max_split(seq, max_entries):
Packit 6f3914
    """ Given a seq, split into a list of lists of length max_entries each. """
Packit 6f3914
    ret = []
Packit 6f3914
    num = len(seq)
Packit 6f3914
    seq = list(seq) # Trying to use a set/etc. here is bad
Packit 6f3914
    beg = 0
Packit 6f3914
    while num > max_entries:
Packit 6f3914
        end = beg + max_entries
Packit 6f3914
        ret.append(seq[beg:end])
Packit 6f3914
        beg += max_entries
Packit 6f3914
        num -= max_entries
Packit 6f3914
    ret.append(seq[beg:])
Packit 6f3914
    return ret
Packit 6f3914
Packit 6f3914
def unlink_f(filename):
Packit 6f3914
    """ Call os.unlink, but don't die if the file isn't there. This is the main
Packit 6f3914
        difference between "rm -f" and plain "rm". """
Packit 6f3914
    try:
Packit 6f3914
        os.unlink(filename)
Packit 6f3914
    except OSError as e:
Packit 6f3914
        if e.errno != errno.ENOENT:
Packit 6f3914
            raise
Packit 6f3914
Packit 6f3914
def stat_f(filename, ignore_EACCES=False):
Packit 6f3914
    """ Call os.stat(), but don't die if the file isn't there. Returns None. """
Packit 6f3914
    try:
Packit 6f3914
        return os.stat(filename)
Packit 6f3914
    except OSError as e:
Packit 6f3914
        if e.errno in (errno.ENOENT, errno.ENOTDIR):
Packit 6f3914
            return None
Packit 6f3914
        if ignore_EACCES and e.errno == errno.EACCES:
Packit 6f3914
            return None
Packit 6f3914
        raise
Packit 6f3914
Packit 6f3914
def _getloginuid():
Packit 6f3914
    """ Get the audit-uid/login-uid, if available. os.getuid() is returned
Packit 6f3914
        instead if there was a problem. Note that no caching is done here. """
Packit 6f3914
    #  We might normally call audit.audit_getloginuid(), except that requires
Packit 6f3914
    # importing all of the audit module. And it doesn't work anyway: BZ 518721
Packit 6f3914
    try:
Packit 6f3914
        with open("/proc/self/loginuid") as fo:
Packit 6f3914
            data = fo.read()
Packit 6f3914
            return int(data)
Packit 6f3914
    except (IOError, ValueError):
Packit 6f3914
        return os.getuid()
Packit 6f3914
Packit 6f3914
_cached_getloginuid = None
Packit 6f3914
def getloginuid():
Packit 6f3914
    """ Get the audit-uid/login-uid, if available. os.getuid() is returned
Packit 6f3914
        instead if there was a problem. The value is cached, so you don't
Packit 6f3914
        have to save it. """
Packit 6f3914
    global _cached_getloginuid
Packit 6f3914
    if _cached_getloginuid is None:
Packit 6f3914
        _cached_getloginuid = _getloginuid()
Packit 6f3914
    return _cached_getloginuid
Packit 6f3914
Packit 6f3914
def decompress(filename, dest=None, fn_only=False, check_timestamps=False):
Packit 6f3914
    """take a filename and decompress it into the same relative location.
Packit 6f3914
       if the file is not compressed just return the file"""
Packit 6f3914
Packit 6f3914
    ztype = None
Packit 6f3914
    out = filename  # If the file is not compressed, it returns the same file
Packit 6f3914
Packit 6f3914
    dot_pos = filename.rfind('.')
Packit 6f3914
    if dot_pos > 0:
Packit 6f3914
        ext = filename[dot_pos:]
Packit 6f3914
        if ext in ('.zck', '.xz', '.bz2', '.gz'):
Packit 6f3914
            ztype = ext
Packit 6f3914
            out = dest if dest else filename[:dot_pos]
Packit 6f3914
Packit 6f3914
    if ztype and not fn_only:
Packit 6f3914
        if check_timestamps:
Packit 6f3914
            fi = stat_f(filename)
Packit 6f3914
            fo = stat_f(out)
Packit 6f3914
            if fi and fo and fo.st_mtime == fi.st_mtime:
Packit 6f3914
                return out
Packit 6f3914
Packit 6f3914
        try:
Packit 6f3914
            libdnf.utils.decompress(filename, out, 0o644, ztype)
Packit 6f3914
        except RuntimeError as e:
Packit 6f3914
            raise dnf.exceptions.MiscError(str(e))
Packit 6f3914
Packit 6f3914
        if check_timestamps and fi:
Packit 6f3914
            os.utime(out, (fi.st_mtime, fi.st_mtime))
Packit 6f3914
Packit 6f3914
    return out
Packit 6f3914
Packit 6f3914
def calculate_repo_gen_dest(filename, generated_name):
Packit 6f3914
    dest = os.path.dirname(filename)
Packit 6f3914
    dest += '/gen'
Packit 6f3914
    if not os.path.exists(dest):
Packit 6f3914
        os.makedirs(dest, mode=0o755)
Packit 6f3914
    return dest + '/' + generated_name
Packit 6f3914
Packit 6f3914
def repo_gen_decompress(filename, generated_name, cached=False):
Packit 6f3914
    """ This is a wrapper around decompress, where we work out a cached
Packit 6f3914
        generated name, and use check_timestamps. filename _must_ be from
Packit 6f3914
        a repo. and generated_name is the type of the file. """
Packit 6f3914
Packit 6f3914
    dest = calculate_repo_gen_dest(filename, generated_name)
Packit 6f3914
    return decompress(filename, dest=dest, check_timestamps=True, fn_only=cached)
Packit 6f3914
Packit 6f3914
def read_in_items_from_dot_dir(thisglob, line_as_list=True):
Packit 6f3914
    """ Takes a glob of a dir (like /etc/foo.d/\\*.foo) returns a list of all
Packit 6f3914
       the lines in all the files matching that glob, ignores comments and blank
Packit 6f3914
       lines, optional paramater 'line_as_list tells whether to treat each line
Packit 6f3914
       as a space or comma-separated list, defaults to True.
Packit 6f3914
    """
Packit 6f3914
    results = []
Packit 6f3914
    for fname in glob.glob(thisglob):
Packit 6f3914
        with open(fname) as f:
Packit 6f3914
            for line in f:
Packit 6f3914
                if re.match(r'\s*(#|$)', line):
Packit 6f3914
                    continue
Packit 6f3914
                line = line.rstrip() # no more trailing \n's
Packit 6f3914
                line = line.lstrip() # be nice
Packit 6f3914
                if not line:
Packit 6f3914
                    continue
Packit 6f3914
                if line_as_list:
Packit 6f3914
                    line = line.replace('\n', ' ')
Packit 6f3914
                    line = line.replace(',', ' ')
Packit 6f3914
                    results.extend(line.split())
Packit 6f3914
                    continue
Packit 6f3914
                results.append(line)
Packit 6f3914
    return results