> I think I've asked before, but can't seem to find that email. > > Does anyone have a script to update an existing directory with newer > rpms from a given folder? > > For example, I have an RPM folder with customizations, and I want to > update the RPMS from a directory that has update 1 plus newer erratas? > Does anyone have such a script, before I try to write one? > Try this. I can't claim ownership or authorship of it - someone on this list wrote it, but I've been using it for the past two months. It's a life-saver. Klaus
#!/usr/bin/python # # update_release.py 2.0 - merge rpm updates and third party rpms # into a RedHat distro and check for # dependencies/conflicts problems # Copyright (C) 2003 - Zouhir Hafidi (Zouhir.Hafidi@xxxxxxxxxxxxxxxxxxx) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Usage (some examples): # - get help: # update_release.py -h # - run in test mode and use current dir as a base dir # update_release.py -n # update_release.py -n -v # - run in test mode and use /export/install/redhat/8.0 as a base dir # update_release.py -n -d /export/install/redhat/8.0 # update_release.py -n -v -d /export/install/redhat/8.0 # - use current dir as a base dir. The installation dir and updates dir # are respectively by default updated_release/RedHat/RPMS/ and updates/ # update_release.py # update_release.py -v # - use /export/install/redhat/8.0 as a base dir. The installation dir and # updates dir are respectively by default updated_release/RedHat/RPMS/ and updates/ # update_release.py -d /export/install/redhat/8.0 # update_release.py -v -d /export/install/redhat/8.0 # - use /export/install/redhat/8.0 as a base dir and only check for # dependencies/conflicts under updated_release/RedHat/RPMS # update_release.py -c -d /export/install/redhat/8.0 # update_release.py -c -v -d /export/install/redhat/8.0 # - use /export/install/redhat/8.0 as a base dir and only check for # dependencies/conflicts under some/relative/dir # update_release.py -c -d /export/install/redhat/8.0 -i some/relative/dir # update_release.py -c -v -d /export/install/redhat/8.0 -i some/relative/dir import os import sys import getopt import commands import rpm import string import shutil def usage(): print "Usage: %s [-h] | [[-n] [-v] [-d dir] [-i dir] [-u dir] [-b branch] [-c] [-H]]" % sys.argv[0] print " -h|--help print this message" print " -n|--dryrun dry run mode (show a trace of what would be done)" print " -v|--verbose print more information when running" print " -d dir|--basedir=dir base dir (current dir by default)" print " -i dir|--installdir=dir installation dir (updated_release/RedHat/RPMS/ by default)" print " -u dir|--updatesdir=dir updates dir (updates/ by default)" print " -b branch|--branch=vers RedHat version (i.e. Fedora, RedHat)" print " -c|--checkonly check for dependencies/conflicts in the installation dir" print " -H|--nohdlist don't regenerate hdlists" # Description: # construct a dependency string (this function comes from anaconda) # Argument(s): # - rpm name # - rpm version # - dependency flags # Return value(s): # - dependency string def formatRequire (name, version, flags): string = name if flags: if flags & (rpm.RPMSENSE_LESS | rpm.RPMSENSE_GREATER | rpm.RPMSENSE_EQUAL): string = string + " " if flags & rpm.RPMSENSE_LESS: string = string + "<" if flags & rpm.RPMSENSE_GREATER: string = string + ">" if flags & rpm.RPMSENSE_EQUAL: string = string + "=" string = string + " %s" % version return string # Description: # construct a dictionary containing the headers of a set of rpms. # If a same rpm exists with different versions, then take into # account only the recent one. Source rpms are not processed. # Argument(s): # - a list of rpms filenames # Return value(s): # - a dictionary of the form: # {(rpm_name, rpm_arch):(rpm_header, rpm_filename), ...} # - a list containing source rpms and old rpms def getRpmsHeaders(rpms): headers = {} bad_rpms = [] ts = rpm.TransactionSet("/", ~(rpm._RPMVSF_NOSIGNATURES)) ts.closeDB() for rpm_file in rpms: fdno = os.open(rpm_file, os.O_RDONLY) header = ts.hdrFromFdno(fdno) os.close(fdno) if string.find(rpm_file, "debuginfo") >= 0: bad_rpms.append(rpm_file) if verbose_flag: print " skip %s which is a debug package" % os.path.basename(rpm_file) elif header[rpm.RPMTAG_SOURCEPACKAGE]: bad_rpms.append(rpm_file) if verbose_flag: print " skip %s which is a source package" % os.path.basename(rpm_file) else: key = (header[rpm.RPMTAG_NAME], header[rpm.RPMTAG_ARCH]) if headers.has_key(key): cmp = rpm.versionCompare(headers[key][0], header) if not cmp in [-1, 1]: print " rpm.versionCompare() returned error code %d while comparing %s and %s" % (cmp, os.path.basename(headers[key][1]), os.path.basename(rpm_file)) sys.exit(cmp) if cmp == -1: bad_rpms.append(headers[key][1]) if verbose_flag: print " use %s instead of %s" % (os.path.basename(rpm_file), os.path.basename(headers[key][1])) headers[key] = (header, rpm_file) else: bad_rpms.append(rpm_file) if verbose_flag: print " skip %s which is older than %s" % (os.path.basename(rpm_file), os.path.basename(headers[key][1])) else: headers[key] = (header, rpm_file) return (headers, bad_rpms) # Description: # delete unwanted entries from a list of rpms filenames # Argument(s): # - list of rpms filenames # - list of entries to delete # Return value(s): # None def skipRpms(rpms_list, skip_list): for item in skip_list: del rpms_list[rpms_list.index(item)] # Description: # check if a given set of rpms put together doesn't have any # dependencies nor conflicts problems # Argument(s): # - a dictionary of the form: # {(rpm_name, rpm_arch):(rpm_header, rpm_filename), ...} # Return value(s): # - a list of errors if any def checkDepsAndConflicts(headers): ts = rpm.TransactionSet("/", ~(rpm._RPMVSF_NOSIGNATURES)) ts.closeDB() for key in headers.keys(): ts.addInstall(headers[key][0], key, 'i') return ts.check() def main(): # Processing command line arguments try: opts, args = getopt.getopt(sys.argv[1:], 'hnvd:i:u:b:cHG', \ ["help", "dryrun", "verbose", "basedir=", "installdir=", \ "updatesdir=", "branch=", "checkonly", "nohdlist", "genhdlist"]) except getopt.GetoptError: usage() sys.exit(1) if args != []: usage() sys.exit(1) global verbose_flag verbose_flag = 0 dryrun_flag = 0 base_dir = os.getcwd() # updated_release_dir = "updated_release/RedHat/RPMS" updated_release_dir = "Fedora/RPMS" updates_dir = "updates" branch = "Fedora" checkonly_flag = 0 nohdlist_flag = 0 genhdlist_flag = 0 for o, a in opts: if o in ("-h", "--help"): usage() sys.exit() if o in ("-n", "--dryrun"): dryrun_flag = 1 if o in ("-v", "--verbose"): verbose_flag = 1 if o in ("-d", "--basedir"): base_dir = a if o in ("-i", "--installdir"): updated_release_dir = a if o in ("-u", "--updatesdir"): updates_dir = a if o in ("-b", "--branch"): branch = a if o in ("-c", "--checkonly"): checkonly_flag = 1 if o in ("-H", "--nohdlist"): nohdlist_flag = 1 if o in ("-G", "--genhdlist"): genhdlist_flag = 1 # Some initializations updated_release_dir = os.path.join(base_dir, updated_release_dir) updates_dir = os.path.join(base_dir, updates_dir) old_rpms_dir = os.path.join(base_dir, "old_rpms") regenerate_hdlist = 0 if dryrun_flag: verbose_flag = 1 # The updated release dir must exist if not os.path.isdir(updated_release_dir): print "can't access %s, nothing to do" % updated_release_dir sys.exit(2) if verbose_flag: print "processing rpms under %s ..." % updated_release_dir # Get all rpm names under updated_release_dir exit_status, output = commands.getstatusoutput("find %s -name '*\.rpm' | grep -v '.src.rpm'" % updated_release_dir) if exit_status != 0: sys.exit(3) updated_release_rpms = output.split() # Get headers of all rpms under updated_release_dir updated_release_headers, bad_rpms = getRpmsHeaders(updated_release_rpms) if bad_rpms != []: print " ERROR: the following rpms are either source rpms or older rpms. Please remove them:" for rpm_file in bad_rpms: print " %s" % rpm_file sys.exit(4) # Before merging, check if the current updated release dir contains # any dependencies or conflicts problems and exit if so errors = checkDepsAndConflicts(updated_release_headers) if errors: print "ERROR: the installation tree already contains conflicts and/or dependencies problems" for ((name, version, release), (reqname, reqversion), \ flags, suggest, sense) in errors: if sense==rpm.RPMDEP_SENSE_REQUIRES: print " depcheck: package %s needs %s" % ( name, formatRequire(reqname, reqversion, flags)) elif sense==rpm.RPMDEP_SENSE_CONFLICTS: print " depcheck: package %s conflicts with %s" % (name, reqname) sys.exit(4) if verbose_flag: print "done" if checkonly_flag: if errors: print "dependencies/conflicts problems exist in %s" % updated_release_dir else: print "no dependencies/conflicts problems in %s" % updated_release_dir sys.exit() # The updates dir must exist if not os.path.isdir(updates_dir): print "can't access %s, nothing to do" % updates_dir sys.exit(2) # If necessary, create the dir to put old rpms into if not os.path.isdir(old_rpms_dir): os.mkdir(old_rpms_dir) if verbose_flag: print "processing rpms under %s ..." % updates_dir # Get all rpm names under updates_dir exit_status, output = commands.getstatusoutput("find %s -follow -name '*\.rpm' | grep -v '.src.rpm'" % updates_dir) if exit_status != 0: sys.exit(3) updates_rpms = output.split() # Get headers of all rpms under updates_dir updates_headers, bad_rpms = getRpmsHeaders(updates_rpms) # Ignore unwanted rpms skipRpms(updates_rpms, bad_rpms) if bad_rpms != []: print " WARNING: the following rpms are either source rpms or older rpms. I will ignore them:" for rpm_file in bad_rpms: print " %s" % rpm_file if verbose_flag: print "done" # Take the entries in the updates_headers dictionary and merge them # with the entries in the updated_release_headers dictionary: # - for every entry in the updates_headers dictionary # - if a corresponding entry with an old version exists # in the updated_release_headers dictionary then drop it # in the old_rpms_headers dictionary and replace it with # the new one # - if there is no corresponding entry with an old version # then simply add the new one in the updated_release_headers # dictionary old_rpms_headers = {} for key in updates_headers.keys(): if updated_release_headers.has_key(key): cmp = rpm.versionCompare(updated_release_headers[key][0], updates_headers[key][0]) if cmp == 1: if verbose_flag: print "skip %s which is older than %s" % (os.path.basename(updates_headers[key][1]), os.path.basename(updated_release_headers[key][1])) elif cmp == 0: if verbose_flag: print "skip %s which is the same as %s" % (os.path.basename(updates_headers[key][1]), os.path.basename(updated_release_headers[key][1])) elif cmp == -1: if verbose_flag: print "exchange %s with %s" % (os.path.basename(updated_release_headers[key][1]), os.path.basename(updates_headers[key][1])) old_rpms_headers[key] = updated_release_headers[key] updated_release_headers[key] = updates_headers[key] else: print " rpm.versionCompare() returned error code %d while comparing %s and %s" % (cmp, os.path.basename(updated_release_headers[key][1]), os.path.basename(updates_headers[key][1])) sys.exit(cmp) else: updated_release_headers[key] = updates_headers[key] if verbose_flag: print "add %s" % os.path.basename(updates_headers[key][1]) # At this point, the merging process is done and the updated_release_headers # dictionary should be up to date. Before applying the changes to the updated # release dir, we must make sure that there's no dependencies nor conflicts # problems. If so, we do a "backtracking" on the headers which make problems # until a state without any dependencies/conflicts is reached. while 1: deps_and_conflicts = 0 errors = checkDepsAndConflicts(updated_release_headers) if errors: # Construct a list containing the names of the rpms that # make problems. If a same rpm exists for different # architectures, then all of them will not be used. # NOTE: we don't know how to discard only problematic rpms # since the architecture is not reported by the check() # method (is there a fix ?) if verbose_flag: print "merging will result in the following conflicts and/or dependencies problems:" name_list = [] for ((name, version, release), (reqname, reqversion), \ flags, suggest, sense) in errors: if sense==rpm.RPMDEP_SENSE_REQUIRES: if verbose_flag: print " depcheck: package %s needs %s" % ( name, formatRequire(reqname, reqversion, flags)) deps_and_conflicts = 1 if not name in name_list: name_list.append(name) elif sense==rpm.RPMDEP_SENSE_CONFLICTS: if verbose_flag: print " depcheck: package %s conflicts with %s" % (name, reqname) deps_and_conflicts = 1 if not name in name_list: name_list.append(name) # Discard entries with dependencies/conflicts problems in # the updated_release_headers dictionary if verbose_flag: print "backtracking:" for (name, arch) in updated_release_headers.keys(): if name in name_list : if os.path.dirname(updated_release_headers[(name, arch)][1]) == updated_release_dir: print "CRITICAL ERROR: attempt to discard %s" % updated_release_headers[(name, arch)][1] sys.exit(4) if verbose_flag: print " discard %s" % updated_release_headers[(name, arch)][1] del updated_release_headers[(name, arch)] # Put back those entries which have been replaced by recent # entries which resulted in dependencies/conflicts problems for (name, arch) in old_rpms_headers.keys(): if name in name_list : if verbose_flag: print " reuse %s" % old_rpms_headers[(name, arch)][1] updated_release_headers[(name, arch)] = old_rpms_headers[(name, arch)] del old_rpms_headers[(name, arch)] # The infinite loop *should* be broken after some iterations if deps_and_conflicts == 0: break # At this point, the updated_release_headers dictionary contains # entries with the rpms filenames that should be in the updated # release dir so that we have no dependencies/conflicts problems. # The old_rpms_headers dictionary contains entries with the rpms # filenames that should be moved from the updated release dir to # the old rpms dir. Just reflect these changes on the directories # themselves for key in updated_release_headers.keys(): if os.path.dirname(updated_release_headers[key][1]) == updated_release_dir: continue regenerate_hdlist = 1 if not dryrun_flag: # os.link(updated_release_headers[key][1], \ shutil.copy2(updated_release_headers[key][1], \ os.path.join(updated_release_dir, os.path.basename(updated_release_headers[key][1]))) else: print "should", if key in old_rpms_headers.keys(): print "update %s with %s" % (os.path.basename(old_rpms_headers[key][1]), \ os.path.basename(updated_release_headers[key][1])) if not dryrun_flag: os.rename(old_rpms_headers[key][1], \ os.path.join(old_rpms_dir, os.path.basename(old_rpms_headers[key][1]))) else: print "add %s" % os.path.basename(updated_release_headers[key][1]) # Regenerate hdlists if necessary if genhdlist_flag or ((not nohdlist_flag) and regenerate_hdlist): if not dryrun_flag: os.system("/usr/lib/anaconda-runtime/genhdlist --withnumbers --productpath %s %s/../.." % (branch, updated_release_dir) ) os.system("PYTHONPATH=/usr/lib/anaconda /usr/lib/anaconda-runtime/pkgorder %s/../.. i386 %s > pkgorder" % (updated_release_dir, branch) ) os.system("/usr/lib/anaconda-runtime/genhdlist --withnumbers --productpath %s --fileorder pkgorder %s/../.." % (branch, updated_release_dir) ) else: if verbose_flag: print "should", if verbose_flag: print "regenerate hdlist" if __name__ == "__main__": main()