# # Author:: Matthew Kent () # Copyright:: Copyright (c) 2009, 2011 Matthew Kent # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # yum-dump.py # Inspired by yumhelper.py by David Lutterkort # # Produce a list of installed, available and re-installable packages using yum # and dump the results to stdout. # # yum-dump invokes yum similarly to the command line interface which makes it # subject to most of the configuration paramaters in yum.conf. yum-dump will # also load yum plugins in the same manor as yum - these can affect the output. # # Can be run as non root, but that won't update the cache. # # Intended to support yum 2.x and 3.x import os import sys import time import yum import re import errno from yum import Errors from optparse import OptionParser from distutils import version YUM_PID_FILE='/var/run/yum.pid' # Seconds to wait for exclusive access to yum LOCK_TIMEOUT = 10 YUM_VER = version.StrictVersion(yum.__version__) YUM_MAJOR = YUM_VER.version[0] if YUM_MAJOR > 3 or YUM_MAJOR < 2: print >> sys.stderr, "yum-dump Error: Can't match supported yum version" \ " (%s)" % yum.__version__ sys.exit(1) # Required for Provides output if YUM_MAJOR == 2: import rpm import rpmUtils.miscutils def setup(yb, options): # Only want our output # if YUM_MAJOR == 3: try: if YUM_VER >= version.StrictVersion("3.2.22"): yb.preconf.errorlevel=0 yb.preconf.debuglevel=0 # initialize the config yb.conf else: yb.doConfigSetup(errorlevel=0, debuglevel=0) except yum.Errors.ConfigError, e: # supresses an ignored exception at exit yb.preconf = None print >> sys.stderr, "yum-dump Config Error: %s" % e return 1 except ValueError, e: yb.preconf = None print >> sys.stderr, "yum-dump Options Error: %s" % e return 1 elif YUM_MAJOR == 2: yb.doConfigSetup() def __log(a,b): pass yb.log = __log yb.errorlog = __log # Give Chef every possible package version, it can decide what to do with them if YUM_MAJOR == 3: yb.conf.showdupesfromrepos = True elif YUM_MAJOR == 2: yb.conf.setConfigOption('showdupesfromrepos', True) # Optionally run only on cached repositories, but non root must use the cache if os.geteuid() != 0: if YUM_MAJOR == 3: yb.conf.cache = True elif YUM_MAJOR == 2: yb.conf.setConfigOption('cache', True) else: if YUM_MAJOR == 3: yb.conf.cache = options.cache elif YUM_MAJOR == 2: yb.conf.setConfigOption('cache', options.cache) # Handle repo toggle via id or glob exactly like yum for opt, repos in options.repo_control: for repo in repos: if opt == '--enablerepo': yb.repos.enableRepo(repo) elif opt == '--disablerepo': yb.repos.disableRepo(repo) return 0 def dump_packages(yb, list, output_provides): packages = {} if YUM_MAJOR == 2: yb.doTsSetup() yb.doRepoSetup() yb.doSackSetup() db = yb.doPackageLists(list) for pkg in db.installed: pkg.type = 'i' packages[str(pkg)] = pkg if YUM_VER >= version.StrictVersion("3.2.21"): for pkg in db.available: pkg.type = 'a' packages[str(pkg)] = pkg # These are both installed and available for pkg in db.reinstall_available: pkg.type = 'r' packages[str(pkg)] = pkg else: # Old style method - no reinstall list for pkg in yb.pkgSack.returnPackages(): if str(pkg) in packages: if packages[str(pkg)].type == "i": packages[str(pkg)].type = 'r' continue pkg.type = 'a' packages[str(pkg)] = pkg unique_packages = packages.values() unique_packages.sort(lambda x, y: cmp(x.name, y.name)) for pkg in unique_packages: if output_provides == "all" or \ (output_provides == "installed" and (pkg.type == "i" or pkg.type == "r")): # yum 2 doesn't have provides_print, implement it ourselves using methods # based on requires gathering in packages.py if YUM_MAJOR == 2: provlist = [] # Installed and available are gathered in different ways if pkg.type == 'i' or pkg.type == 'r': names = pkg.hdr[rpm.RPMTAG_PROVIDENAME] flags = pkg.hdr[rpm.RPMTAG_PROVIDEFLAGS] ver = pkg.hdr[rpm.RPMTAG_PROVIDEVERSION] if names is not None: tmplst = zip(names, flags, ver) for (n, f, v) in tmplst: prov = rpmUtils.miscutils.formatRequire(n, v, f) provlist.append(prov) # This is slow :( elif pkg.type == 'a': for prcoTuple in pkg.returnPrco('provides'): prcostr = pkg.prcoPrintable(prcoTuple) provlist.append(prcostr) provides = provlist else: provides = pkg.provides_print else: provides = "[]" print '%s %s %s %s %s %s %s %s' % ( pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch, provides, pkg.type, pkg.repoid ) return 0 def yum_dump(options): lock_obtained = False yb = yum.YumBase() status = setup(yb, options) if status != 0: return status if options.output_options: print "[option installonlypkgs] %s" % " ".join(yb.conf.installonlypkgs) # Non root can't handle locking on rhel/centos 4 if os.geteuid() != 0: return dump_packages(yb, options.package_list, options.output_provides) # Wrap the collection and output of packages in yum's global lock to prevent # any inconsistencies. try: # Spin up to LOCK_TIMEOUT countdown = LOCK_TIMEOUT while True: try: yb.doLock(YUM_PID_FILE) lock_obtained = True except Errors.LockError, e: time.sleep(1) countdown -= 1 if countdown == 0: print >> sys.stderr, "yum-dump Locking Error! Couldn't obtain an " \ "exclusive yum lock in %d seconds. Giving up." % LOCK_TIMEOUT return 200 else: break return dump_packages(yb, options.package_list, options.output_provides) # Ensure we clear the lock and cleanup any resources finally: try: yb.closeRpmDB() if lock_obtained == True: yb.doUnlock(YUM_PID_FILE) except Errors.LockError, e: print >> sys.stderr, "yum-dump Unlock Error: %s" % e return 200 # Preserve order of enable/disable repo args like yum does def gather_repo_opts(option, opt, value, parser): if getattr(parser.values, option.dest, None) is None: setattr(parser.values, option.dest, []) getattr(parser.values, option.dest).append((opt, value.split(','))) def main(): usage = "Usage: %prog [options]\n" + \ "Output a list of installed, available and re-installable packages via yum" parser = OptionParser(usage=usage) parser.add_option("-C", "--cache", action="store_true", dest="cache", default=False, help="run entirely from cache, don't update cache") parser.add_option("-o", "--options", action="store_true", dest="output_options", default=False, help="output select yum options useful to Chef") parser.add_option("-p", "--installed-provides", action="store_const", const="installed", dest="output_provides", default="none", help="output Provides for installed packages, big/wide output") parser.add_option("-P", "--all-provides", action="store_const", const="all", dest="output_provides", default="none", help="output Provides for all package, slow, big/wide output") parser.add_option("-i", "--installed", action="store_const", const="installed", dest="package_list", default="all", help="output only installed packages") parser.add_option("-a", "--available", action="store_const", const="available", dest="package_list", default="all", help="output only available and re-installable packages") parser.add_option("--enablerepo", action="callback", callback=gather_repo_opts, type="string", dest="repo_control", default=[], help="enable disabled repositories by id or glob") parser.add_option("--disablerepo", action="callback", callback=gather_repo_opts, type="string", dest="repo_control", default=[], help="disable repositories by id or glob") (options, args) = parser.parse_args() try: return yum_dump(options) except yum.Errors.RepoError, e: print >> sys.stderr, "yum-dump Repository Error: %s" % e return 1 except yum.Errors.YumBaseError, e: print >> sys.stderr, "yum-dump General Error: %s" % e return 1 try: status = main() # Suppress a nasty broken pipe error when output is piped to utilities like 'head' except IOError, e: if e.errno == errno.EPIPE: sys.exit(1) else: raise sys.exit(status)