#!/usr/bin/python

""" 
    Program to update an existing bcfg2 Pkgmgr configuration file from a list 
    of directories that contain RPMS.  
 
    Only the epoch, version, release and simplefiles attributes are updated 
    in existing entries.  All other entries and attributes are preserved.   

    This is a total hack until a proper more generalised system for managing
    Pkgmgr configuation files is developed.
"""

__version__ = '0.1'

import sys
import os
import rpm
import optparse
import datetime
import glob
from elementtree.ElementTree import parse, XML, fromstring, tostring

installOnlyPkgs = ['kernel', 'kernel-bigmem', 'kernel-enterprise', 'kernel-smp',
                   'kernel-modules', 'kernel-debug', 'kernel-unsupported',
                   'kernel-source', 'kernel-devel', 'kernel-default',
                   'kernel-largesmp-devel', 'kernel-largesmp', 'kernel-xen', 
                   'gpg-pubkey']

def readRpmHeader(ts, filename):
    """ 
        Read an rpm header from an RPM file. 
    """
    try:
        fd = os.open(filename, os.O_RDONLY)
    except:
        print 'Failed to open RPM file %s' % filename

    h = ts.hdrFromFdno(fd)
    os.close(fd)
    return h

def sortedDictValues(adict):
    """ 
        Sort a dictionary by its keys and return the items in sorted key order. 
    """
    keys = adict.keys()
    keys.sort()
    return map(adict.get, keys)

def cmpRpmHeader(a, b):
    """
        cmp() implemetation suitable for use with sort.
    """
    n1 = str(a.get('name'))
    e1 = str(a.get('epoch'))
    v1 = str(a.get('version'))
    r1 = str(a.get('release'))
    n2 = str(b.get('name'))
    e2 = str(b.get('epoch'))
    v2 = str(b.get('version'))
    r2 = str(b.get('release'))

    return rpm.labelCompare((e1, v1, r1),(e2, v2, r2))

def loadRpms(dirs):
    """
       dirs is a list of directories to search for rpms.

       Builds a multilevel dictionary keyed by the package name and arch. 
       Arch dictionary item is a list, one entry per package instance found.
       
       The list entries are dictionaries.  Keys are 'filename', 'mtime' 'name', 
       'arch', 'epoch', 'version' and 'release'.  
 
       e.g.

       packages = {
       'bcfg2' : { 'noarch' : [ {'filename':'bcfg2-0.9.2-0.0rc1.noarch.rpm', 'mtime':'' 'name':'bcfg2', 
                                 'arch':'noarch', 'epoch':None, 'version':'0.9.2', 'release':'0.0rc1'}
                                {'filename':'bcfg2-0.9.2-0.0rc5.noarch.rpm', 'mtime':'' 'name':'bcfg2', 
                                 'arch':'noarch', 'epoch':None, 'version':'0.9.2', 'release':'0.0rc5'}]},
       'bcfg2-server' { 'noarch' : [ {'filename':'bcfg2-server-0.9.2-0.0rc1.noarch.rpm', 'mtime':'' 'name':'bcfg2-server', 
                                      'arch':'noarch', 'epoch':None, 'version':'0.9.2', 'release':'0.0rc1'}
                                     {'filename':'bcfg2-server-0.9.2-0.0rc5.noarch.rpm', 'mtime':'' 'name':"bcfg2-server', 
                                      'arch':'noarch', 'epoch':None, 'version':'0.9.2', 'release':'0.0rc5'}]},
       }
       
    """
    packages = {}
    ts = rpm.TransactionSet()
    vsflags = 0
    vsflags |= rpm._RPMVSF_NODIGESTS
    vsflags |= rpm._RPMVSF_NOSIGNATURES
    ovsflags = ts.setVSFlags(vsflags)
    for dir in dirs:

        if options.verbose:
            print 'Scanning directory: %s' % dir

        for file in [files for files in os.listdir(dir) 
                           if files.endswith('.rpm')]:

            filename = os.path.join( dir, file )
             
            # Get the mtime of the RPM file.
            file_mtime = datetime.date.fromtimestamp(os.stat(filename).st_mtime)

            # Get the RPM header
            header = readRpmHeader( ts, filename ) 

            # Get what we are interesting in out of the header.
            name = header[rpm.RPMTAG_NAME]
            epoch = header[rpm.RPMTAG_EPOCH]
            version = header[rpm.RPMTAG_VERSION]
            release = header[rpm.RPMTAG_RELEASE]
            subarch = header[rpm.RPMTAG_ARCH]

            if name not in installOnlyPkgs:
                packages.setdefault(name, {}).setdefault(subarch, []).append({'filename':file, \
                                          'mtime':file_mtime, 'name':name, 'arch':subarch, \
                                          'epoch':epoch, 'version':version, 'release':release})
            if options.verbose:
                sys.stdout.write('.')
                sys.stdout.flush()
        if options.verbose:
            sys.stdout.write('\n')

    return packages

def str_evra(instance):
    """
        Convert evra dict entries to a string.
    """
    if instance.get('epoch', '*') == '*' or instance.get('epoch', '*') == None: 
        return '%s-%s.%s' % (instance.get('version', '*'), instance.get('release', '*'), 
                             instance.get('arch', '*')) 
    else: 
        return '%s:%s-%s.%s' % (instance.get('epoch', '*'), instance.get('version', '*'), 
                                instance.get('release', '*'), instance.get('arch', '*')) 

def updatepkg(pkg):
    """
    """
    global package_dict
    name = pkg.get('name')
    if name not in installOnlyPkgs:
        for inst in [inst for inst in pkg if inst.tag == 'Instance']:
            arch = inst.get('arch')
            if package_dict.has_key(name):
                if package_dict[name].has_key(arch):
                    package_dict[name][arch].sort(cmpRpmHeader)
                    latest = package_dict[name][arch][-1]
                    if cmpRpmHeader(inst, latest) == -1:
                        if options.verbose:
                            print 'Found newer version of package %s' % name
                            print '    Updating %s to %s' % (str_evra(inst), str_evra(latest))
                        if latest['epoch'] != None:
                            inst.attrib['epoch'] = str(latest['epoch'])
                        inst.attrib['version'] = latest['version']
                        inst.attrib['release'] = latest['release']
                        if inst.get('simplefile', False):
                            inst.attrib['simplefile'] = latest['filename']

def main():
    global package_dict
    if options.verbose:
        print 'Loading Pkgmgr config file %s.' % (options.configfile)

    tree = parse(options.configfile)
    config = tree.getroot()
 
    if options.verbose:
        print 'Loading package headers'

    package_dict = loadRpms(search_dirs)

    if options.verbose:
        print 'Processing package headers'

    for pkg in config.getiterator('Package'):
        updatepkg(pkg)

    output.write(tostring(config))

if __name__ == "__main__":

    p = optparse.OptionParser()

    p.add_option('--configfile', '-c', action='store', \
                                   type='string', \
                                   help='Existing Pkgmgr configuration  file name.')

    p.add_option('--rpmdirs', '-d', action='store', 
                                   default='.', \
                                   type='string', \
                                   help='Comma separated list of directories to scan for RPMS. Wilcards are permitted. (Default: .)')
    
    p.add_option('--outfile', '-o', action='store', \
                                   type='string', \
                                   help='Output file name or new Pkgrmgr file.')
                                  
    p.add_option('--verbose', '-v', action='store_true', \
                                    help='Enable verbose output.')

    options, arguments = p.parse_args()

    if not options.configfile:
        print "An existing Pkgmgr configuration file must be specified with the -c option."
        sys.exit()

    # Set up list of directories to search
    search_dirs = []
    for d in options.rpmdirs.split(','):
        search_dirs += glob.glob(d) 

    if options.verbose:
        print 'The following directories will be scanned:'
        for d in search_dirs:
            print '    %s' % d
    
    if options.outfile:
        output = file(options.outfile, "w")
    else:
        output = sys.stdout

    package_dict = {}

    main()



syntax highlighted by Code2HTML, v. 0.9.1