import repository from arizona
[raven.git] / lib / ravenlib / package / rpm.py
1 #! /usr/bin/env python
2
3 """
4 <Program Name>
5    storkrpm.py
6
7 <Started>
8    April 27, 2004
9
10 <Author>
11    Programmed by Justin Cappos.  Refactored by Jeffry Johnston.
12
13 <Purpose>
14    Implements RPM functionality.
15 """
16
17 import os
18 import re
19 import sys
20 import subprocess
21 import ravenlib.listutil
22 import ravenlib.typecheck
23 import transaction
24
25 from ravenlib.package.exception import *
26
27 def splitlines(x):
28     x = x.split("\n")
29     if (len(x)>0) and (x[-1]==""):
30        # splitting the output of a subprocess communicate often leaves us with
31        # a dangling empty string
32        x = x[:-1]
33     return x
34
35 def initialize():
36    """
37    <Purpose>
38       Initializes the package manager.  
39       This must be called before can_satisfy.
40
41    <Arguments>
42       None.
43
44    <Exceptions>
45       None.
46
47    <Side Effects>
48       None.
49
50    <Returns>
51       Returns a list of dependencies that the package manager itself
52       satisfies.
53    """
54    # build list of dependencies internally satisfied by rpm
55    args = ["rpm", "--showrc"]
56    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
57    (stdout_data, stderr_data) = sub.communicate()
58    rpm_sats = []
59    for output in splitlines(stdout_data):
60       output = output.strip()
61       if output.startswith('rpmlib('):
62          rpm_sats.append(output)
63
64    return rpm_sats
65
66
67
68
69
70 def is_package_understood(filename):
71    """
72    <Purpose>
73       Given a filename, checks whether it is a valid RPM package.
74
75    <Arguments>
76       filename:
77               Filename of the rpm to check.
78
79    <Exceptions>
80       TypeError:
81               If a type mismatch or parameter error is detected.
82
83    <Side Effects>
84       None.
85
86    <Returns>
87       True if the package is valid and understood, False otherwise.
88    """
89    # check params
90    ravenlib.typecheck.simple(filename, "filename", str, "storkrpm.is_package_understood")
91    
92    # does the file exist?
93    if not os.path.isfile(filename):
94       return False
95    
96    # if we can get package information, then the rpm was understood
97    return get_package_info(filename) != None
98
99
100
101
102
103 def __understood_packages(filename_list):
104    """
105    <Purpose>
106       Given a string list of package filenames, returns a string list of 
107       package filenames that are understood by RPM.
108
109    <Arguments>
110       filename_list:
111               String list of package filenames.
112
113    <Exceptions>
114       TypeError:
115               If a type mismatch or parameter error is detected.
116
117    <Side Effects>
118       None.
119
120    <Returns>
121       String list of RPM package filenames.
122    """
123    understood = []
124    for filename in filename_list:
125       if is_package_understood(filename):
126          understood.append(filename)
127    return understood      
128
129
130
131
132
133 def get_packages_provide(filename_list):
134    """
135    <Purpose>
136       Given a string list of package filenames, returns a string list of
137       dependencies that those packages can provide.
138
139    <Arguments>
140       filename_list:
141               String list of package filenames.
142
143    <Exceptions>
144       TypeError:
145               If a type mismatch or parameter error is detected.
146
147    <Side Effects>
148       None.
149
150    <Returns>
151       String list of dependencies provided by the given package files.
152    """
153    # check params
154    ravenlib.typecheck.stringlist(filename_list, "filename_list", "storkrpm.get_packages_provide")
155
156    filename_list = __understood_packages(filename_list)
157
158    if len(filename_list) < 1:
159       return []
160
161    args = ["rpm", "-qp", "--provides"] + filename_list
162    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
163    (stdout_data, stderr_data) = sub.communicate()
164    retlist = []
165    for output in splitlines(stdout_data):
166       if not (output.startswith('error:') or output.endswith('Is a directory')):
167          retlist.append(output.strip())
168
169    return retlist
170
171
172
173
174
175 def get_packages_require(filename_list):
176    """
177    <Purpose>
178       Given a string list of package filenames, returns a string list of 
179       dependencies that those packages require.
180
181    <Arguments>
182       filename_list:
183               String list of package filenames.
184
185    <Exceptions>
186       TypeError:
187               If a type mismatch or parameter error is detected.
188
189    <Side Effects>
190       None.
191
192    <Returns>
193       String list of dependencies required by the given package files.
194    """
195    # check params
196    ravenlib.typecheck.stringlist(filename_list, "filename_list", "storkrpm.get_packages_require")
197
198    filename_list = __understood_packages(filename_list)   
199
200    if len(filename_list) < 1:
201       return []
202
203    args = ["rpm", "-qp", "--requires"] + filename_list
204    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
205    (stdout_data, stderr_data) = sub.communicate()
206    retlist = []
207    for output in splitlines(stdout_data):
208       if not (output.startswith('error:') or output.endswith('Is a directory')):
209          retlist.append(output.strip())
210
211    return retlist
212
213
214
215
216
217 def get_packages_files(filename_list):
218    """
219    <Purpose>
220       Given a string list of package filenames, returns a string list of
221       files that are installed by those packages.
222
223    <Arguments>
224       filename_list:
225               String list of package filenames.
226
227    <Exceptions>
228       TypeError:
229               If a type mismatch or parameter error is detected.
230
231    <Side Effects>
232       None.
233
234    <Returns>
235       String list of files in the given package files.
236    """
237    # check params
238    ravenlib.typecheck.stringlist(filename_list, "filename_list", "storkrpm.get_packages_files")
239
240    if len(filename_list) < 1:
241       return []
242
243    filename_list = __understood_packages(filename_list)   
244
245    args = ["rpm", "-qpl"] + filename_list
246    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
247    (stdout_data, stderr_data) = sub.communicate()
248    retlist = []
249    for output in splitlines(stdout_data):
250       if not (output.startswith('error:') or output.endswith('Is a directory')):
251          retlist.append(output.strip())
252
253    # rpm prints a special message when an rpm has no files, correct this
254    if retlist == ['(contains no files)']:
255       retlist = []
256
257    return retlist
258
259
260
261
262
263 def get_package_info(filename):
264    """
265    <Purpose>
266       Given a package filename, returns a string list of package
267       information of the form:
268         [NAME, VERSION, RELEASE, SIZE]
269
270    <Arguments>
271       filename:
272               Package filename.
273
274    <Exceptions>
275       TypeError:
276               If a type mismatch or parameter error is detected.
277
278    <Side Effects>
279       None.
280
281    <Returns>
282       String list containing package information, or None on error.
283    """
284    # check params
285    ravenlib.typecheck.simple(filename, "filename", str, "storkrpm.get_package_info")
286
287    args = ["rpm", "-qp", "--qf", '%{NAME}|%{EPOCH}:%{VERSION}|%{RELEASE}|%{SIZE}', filename]
288    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
289    (out, err) = sub.communicate()
290    out = out.split("\n")
291    err = err.split("\n")
292
293    #ravenlib.report.debug("get_package_info: rpm (" + " ".join(args) + "):" + "\n".join(out))
294
295    if len(out) < 1:
296       return None
297
298    if sub.returncode != 0:
299       return None
300
301    output = out[0].split("|")
302    if len(output) != 4:
303       return None
304
305    # if there was no epoch, then get rid of the (none): prefix
306    if output[1].startswith("(none)"):
307       output[1] = output[1][7:]
308
309    return output
310
311
312
313
314
315 def get_installed_versions(package_list):
316    """
317    <Purpose>
318       Given a package list, returns a list containing the name and version
319       of each package if installed, 
320    
321    <Arguments>
322       package_list:
323               List of strings containing the names of the packages to get 
324               version information for.
325
326    <Exceptions>
327       TypeError:
328               If a type mismatch or parameter error is detected.
329
330    <Side Effects>
331       None.
332
333    <Returns>
334       String list containing each package name and version (in the format
335       "name = version-release"). Packages that are not installed are not
336       listed. If a package has more than one installed version, then multiple
337       results may be returned for that package.
338    """
339    # check params
340    ravenlib.typecheck.stringlist(package_list, "package_list", "storkrpm.get_installed_versions")
341
342    if len(package_list) < 1:
343       return []
344
345    version_list = []
346
347    args = ["rpm", "-q", "--qf", "%{name} = %{epoch}:%{version}-%{release}\n"] + package_list
348    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
349    (stdout_data, stderr_data) = sub.communicate()
350
351    # for each package there will be one line of output, either with the
352    # formatted info we want, or ending with "not installed".
353    for output in splitlines(stdout_data):
354       output = output.strip()
355       if not output.endswith("not installed"):
356          # if there isn't an epoch, remove the "(none):" text
357          output = output.replace(" = (none):", " = ")
358          version_list.append(output)
359
360    return version_list
361
362
363
364 def get_installedpackages_requiring(dep_list):
365    """
366    <Purpose>
367       Given a string list of dependencies, returns a string list of
368       installed packages that require those package dependencies.
369
370    <Arguments>
371       dep_list:
372               String list of package dependencies.
373
374    <Exceptions>
375       TypeError:
376               If a type mismatch or parameter error is detected.
377
378    <Side Effects>
379       None.
380
381    <Returns>
382       String list of installed packages that require the given dependencies.
383    """
384    # check params
385    ravenlib.typecheck.stringlist(dep_list, "dep_list", "storkrpm.get_installedpackages_fulfilling")
386
387    if not dep_list:
388       return []
389
390    args = ["rpm", "-q", "--whatrequires"] + dep_list
391    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
392    (stdout_data, stderr_data) = sub.communicate()
393    retlist = []
394    for line in splitlines(stdout_data):
395       # no package provides...
396       if not line.startswith('no package '):
397          retlist.append(line.rstrip())
398
399    return retlist
400
401
402
403
404 def get_installedpackages_fulfilling(dep_list):
405    """
406    <Purpose>
407       Given a string list of dependencies, returns a string list of
408       installed packages that fulfill those package dependencies.
409
410    <Arguments>
411       dep_list:
412               String list of package dependencies.
413
414    <Exceptions>
415       TypeError:
416               If a type mismatch or parameter error is detected.
417
418    <Side Effects>
419       None.
420
421    <Returns>
422       String list of installed packages that meet the given dependencies.
423    """
424    # check params
425    ravenlib.typecheck.stringlist(dep_list, "dep_list", "storkrpm.get_installedpackages_fulfilling")
426
427    if not dep_list:
428       return []
429
430    args = ["rpm", "-q", "--whatprovides"] + dep_list
431    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
432    (stdout_data, stderr_data) = sub.communicate()
433    retlist = []
434    for line in splitlines(stdout_data):
435       # no package provides...
436       if not line.startswith('no package '):
437          retlist.append(line.rstrip())
438
439    return retlist
440
441
442
443
444 def get_installedpackages():
445    """
446    <Purpose>
447       Return a list of all packages installed.
448
449    <Arguments>
450
451    <Exceptions>
452
453    <Side Effects>
454       None.
455
456    <Returns>
457       String list of installed packages that are installed.
458    """
459
460    args = ["rpm", "-q", "-a"]
461    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
462    (stdout_data, stderr_data) = sub.communicate()
463    retlist = []
464    for line in splitlines(stdout_data):
465       # no package provides...
466       if not line.startswith('no package '):
467          retlist.append(line.rstrip())
468
469    return retlist
470
471
472
473
474 def get_installedpackages_provide(package_list):
475    """
476    <Purpose>
477       Given a string list of installed package names, returns a string
478       list of all dependencies fulfilled by those packages.
479
480    <Arguments>
481       package_list:
482               String list of installed package names.
483
484    <Exceptions>
485       IOError:
486               If asked to report on a package that is not installed.
487       TypeError:
488               If a type mismatch or parameter error is detected.
489
490    <Side Effects>
491       None.
492
493    <Returns>
494       String list of all dependencies fulfilled by the given packages.
495       Dependencies will either be in the form:
496         name=version-release (possible spaces around `=')
497       Or:
498         name  
499    """
500    # check params
501    ravenlib.typecheck.stringlist(package_list, "package_list", "storkrpm.get_installedpackages_provide")
502
503    if len(package_list) < 1:
504       return []
505
506    args = ["rpm", "-q", "--provides"] + package_list
507    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
508    (stdout_data, stderr_data) = sub.communicate()
509
510    if stdout_data.split("\n")[0].endswith('is not installed'):
511       raise IOError, "Inconsistent RPM database!"
512
513    retlist = []
514    for output in splitlines(stdout_data):
515       retlist.append(output.strip())
516
517    return retlist
518
519
520
521
522
523 def get_installedpackages_requires(package_list):
524    """
525    <Purpose>
526       Given a string list of installed package names, returns a string
527       list of all dependencies required by those packages.
528
529    <Arguments>
530       package_list:
531               String list of installed package names.
532
533    <Exceptions>
534       IOError:
535               If asked to report on a package that is not installed.
536       TypeError:
537               If a type mismatch or parameter error is detected.
538
539    <Side Effects>
540       None.
541
542    <Returns>
543       String list of all dependencies required by the given packages.
544       Dependencies will either be in the form:
545         name=version-release (possible spaces around `=')
546       Or:
547         name  
548    """
549    # check params
550    ravenlib.typecheck.stringlist(package_list, "package_list", "storkrpm.get_installedpackages_requires")
551
552    if len(package_list) < 1:
553       return []
554
555    args = ["rpm", "-q", "--requires"] + package_list
556    sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
557    (stdout_data, stderr_data) = sub.communicate()
558
559    if stdout_data.split("\n")[0].endswith('is not installed'):
560       raise IOError, "Inconsistent RPM database!"
561
562    retlist = []
563    for output in splitlines(stdout_data):
564       retlist.append(output.strip())
565
566    return retlist
567
568
569
570
571
572 def check_install_status(filename_list):
573    """
574    <Purpose>
575       Given a list of filenames, see if they were installed.
576
577    <Arguments>
578       filename_list:
579               String list of filenames representing packages to check.
580
581    <Returns>
582       (installed_list, not_installed_list), where installed_list is a list of
583       the filenames that were installed.
584    """
585    ravenlib.typecheck.stringlist(filename_list, "filename_list", "storkrpm.check_install_status")
586
587    installed_list = []
588    not_installed_list = []
589   
590    for filename in filename_list:
591
592      # get the package name from the rpm file
593      package_names = []
594
595      args = ["rpm", "-qp", filename]
596      sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
597      (stdout_data, stderr_data) = sub.communicate()
598
599      for output in splitlines(stdout_data):
600         output = output.strip()
601         if output.startswith("error:") or output.startswith("warning:"):
602            pass
603         else:
604            package_names.append(output)
605
606      # can package_names have more than one item? example?
607
608      # use get_installed_versions to see if the packages was installed. If so,
609      # then add it to installed_list. else, add it to not_installed_list
610      if get_installed_versions(package_names):
611         installed_list.append(filename)
612      else:
613         not_installed_list.append(filename)
614
615    return (installed_list, not_installed_list)
616
617
618
619
620 def execute(trans_list):
621    """
622    <Purpose>
623       Installs packages with the given filenames.
624
625    <Arguments>
626       filename_list:
627               String list of filenames representing packages to install.
628
629    <Exceptions>
630       TypeError:
631               If a type mismatch or parameter error is detected.
632
633    <Side Effects>
634       None.
635
636    <Returns>
637       None.
638    """
639    # check params
640    ravenlib.typecheck.simple(trans_list, "trans_list", list, "storkrpm.execute")
641
642    if len(trans_list) < 1:
643        return
644
645    # get a list of the RPMs that we're going to install or upgrade
646    filename_list = \
647        transaction.tl_get_filename_list(trans_list, transaction.INSTALL) + \
648        transaction.tl_get_filename_list(trans_list, transaction.UPGRADE)
649
650    if len(filename_list) >= 1:
651        # install (or upgrade) packages
652        args = ["rpm", "--upgrade", "--force", "-i"] + filename_list
653        sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
654        (out, err) = sub.communicate()
655        out = out.split("\n")
656        err = err.split("\n")
657
658        if (sub.returncode == 0):
659           for trans in trans_list:
660               transaction.trans_set_status(trans, "success", "installed")
661
662        if (sub.returncode != 0):
663           (installed_list, not_installed_list) = check_install_status(filename_list)
664
665           # hmmm... it seems we can fail an install due to something like the
666           # postinstall script failing, but the rpm database still thinks the
667           # package is installed and thus we do not report any error.
668
669           # rather than failing silently, let's at least display the stderr
670           if len(not_installed_list) == 0:
671               sys.stderr.write("".join(err).strip() + "\n")
672
673           for fn in installed_list:
674               transaction.tl_set_status_by_filename(trans_list, fn, "success", "installed")
675
676           for fn in not_installed_list:
677               transaction.tl_set_status_by_filename(trans_list, fn, "failure", "rpm install error")
678
679           if len(not_installed_list) > 0:
680               raise StorkInstallFailureException()
681
682    packname_list = \
683        transaction.tl_get_packname_list(trans_list, transaction.REMOVE)
684
685    if len(packname_list) > 0:
686        # remove packages
687        if False:   # FIXME nodeps
688            args = ["rpm", "-e", "--nodeps"] + packname_list
689        else:
690            args = ["rpm", "-e"] + packname_list
691
692        sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
693        (out, err) = sub.communicate()
694        out = out.split("\n")
695        err = err.split("\n")
696
697        if (sub.returncode == 0):
698           for trans in trans_list:
699               transaction.trans_set_status(trans, "success", "removed")
700
701        if (sub.returncode != 0):
702           # generate lists of removed and failed packages
703           failcount = 0
704           for pack in get_installed_versions(packname_list):
705               if pack == None:
706                   failcount = failcount + 1
707                   transaction.tl_set_status_by_packname(trans_list, pack, "failure", "rpm remove error")
708               else:
709                   transaction.tl_set_status_by_packname(trans_list, pack, "success", "removed")
710
711           if (failcount > 0):
712               raise StorkRemoveFailureException()
713