summaryrefslogtreecommitdiff
path: root/lib/chef/provider/package/yum-dump.py
blob: 7e86994c649ab71150f7b8f681f5e6da29327f4e (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
296
297
298
299
300
301
302
303
304
305
306
307
#
# Author:: Matthew Kent (<mkent@magoazul.com>)
# 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'

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 --yum-lock-timeout option
    countdown = options.yum_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." % options.yum_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")
  parser.add_option("--yum-lock-timeout",
                    action="callback",  type="int", dest="yum_lock_timeout", default=30,
                    help="Time in seconds to wait for yum process lock")

  (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)