import repository from arizona
[raven.git] / lib / ravenlib / package / tar.py
1 #! /usr/bin/env python
2 """
3 <Program Name>
4    storktar.py
5
6 <Author>
7    Programmed by Byung Suk, refactored by Jeffry Johnston.
8    Additional tar formats and special files support by Jeffry Johnston.
9
10 <Purpose>
11    Install/remove:
12      tar file (.tar)
13      gzipped tar file (.tar.gz, .tar.gzip, .tgz)
14      bzipped tar file (.tar.bz2, .tar.bzip2, .tbz2, .tbz)
15      compressed tar file (.tar.Z, .taz)
16
17 <Details>
18    Installing a package creates a PACKAGENAME.packinfo file in the
19    tarpackinfo directory, containing:
20       name              Name of the package
21       version           Package version (default is 0.0.0)
22       release           Package release (default is 0)
23       files             Tab separated list of files in the tarball, for
24                         file removal on uninstall
25
26    Packages may contain the following special files (files with these
27    names are omitted from the files list above):
28       autorun.sh        Run it when the package is installed
29 """
30
31 import os
32 import pwd
33 import re
34 import subprocess
35 import ravenlib.report
36 import ravenlib.typecheck
37 import transaction
38
39 from ravenlib.package.exception import *
40
41 # import flags for os.stat()
42 from stat import *
43
44 TARPACKINFO = "/usr/local/stork/tar_packinfo"
45
46 DEFAULT_VERSION = "0.0.0"
47 DEFAULT_RELEASE = "0"
48 RECOGNIZED_EXTENSIONS = [".tar", \
49                          ".tar.gz", ".tar.gzip", ".tgz", \
50                          ".tar.bz2", ".tar.bzip2", ".tbz2", ".tbz", \
51                          ".tar.Z", ".taz"]
52 INSTALL_FLAGS = ["", "z", "j", "Z"]
53
54
55
56 tarpackinfo_path = None
57 def __init_packinfo():
58    """ Returns True on error, False on success """
59    global tarpackinfo_path
60
61    if tarpackinfo_path == None:
62       # get tar package info path
63       tarpackinfo_path = TARPACKINFO
64
65       # create tar package info path if it doesn't exist
66       if not os.path.exists(tarpackinfo_path):
67          try:
68             os.makedirs(tarpackinfo_path)
69          except OSError:
70             ravenlib.report.error("storktar: Can't create tar package info path `" + tarpackinfo_path + "'")
71
72    return False
73
74
75
76
77
78 def initialize():
79    """
80    <Purpose>
81       Initializes the package manager.
82
83    <Arguments>
84       None.
85
86    <Exceptions>
87       None.
88
89    <Side Effects>
90       None.
91
92    <Returns>
93       Returns a list of dependencies that the package manager itself
94       satisfies (empty list for tar), or None on error.
95    """
96    return []
97
98
99
100
101
102 def is_package_understood(filename):
103    """
104    <Purpose>
105       Given a filename, checks whether it is a valid Tar package.
106
107    <Arguments>
108       filename:
109               Tar filename to check.
110
111    <Exceptions>
112       TypeError:
113               If a type mismatch or parameter error is detected.
114
115    <Side Effects>
116       None.
117
118    <Returns>
119       True if the package is valid and understood, False otherwise.
120    """
121    # check the arguments
122    ravenlib.typecheck.simple(filename, "filename", str, "storktar.is_package_understood")
123
124    # run command to check if the file is valid (returns None, if not)
125    # It apparently now returns false instead of none
126    flag = __determine_package_type(filename)
127
128    return flag != False
129
130
131
132
133
134 def __understood_packages(filename_list):
135    """
136    <Purpose>
137       Given a string list of package filenames, returns a string list of
138       package filenames that are understood by tar.
139
140    <Arguments>
141       filename_list:
142               String list of package filenames.
143
144    <Exceptions>
145       TypeError:
146               If a type mismatch or parameter error is detected.
147
148    <Side Effects>
149       None.
150
151    <Returns>
152       String list of tar package filenames.
153    """
154    understood = []
155    for filename in filename_list:
156       if is_package_understood(filename):
157          understood.append(filename)
158    return understood
159
160
161
162
163
164 def get_packages_provide(filename_list):
165    """
166    <Purpose>
167       Given a string list of package filenames, returns a string list of
168       dependencies that those packages can provide.
169
170    <Arguments>
171       filename_list:
172               String list of package filenames.
173
174    <Exceptions>
175       TypeError:
176               If a type mismatch or parameter error is detected.
177
178    <Side Effects>
179       None.
180
181    <Returns>
182       String list of dependencies provided by the given package files.
183    """
184    # check params
185    ravenlib.typecheck.stringlist(filename_list, "filename_list", "storktar.get_packages_provide")
186
187    if len(filename_list) < 1:
188       return []
189
190    # filter out packages we don't understand
191    filename_list = __understood_packages(filename_list)
192
193    # build a list of provided dependencies
194    deplist = []
195    for filename in filename_list:
196       info = _get_package_info(filename)
197       if not info:
198          continue
199       deplist.append(info['name'] + " = " + info['version'] + "-" + info['release'])
200
201    return deplist
202
203
204
205
206
207 def get_packages_require(filename_list):
208    """
209    <Purpose>
210       Given a string list of package filenames, returns a string list of
211       dependencies that those packages require.  For now, tar doesn't
212       implement nested dependencies, so always returns [].
213
214    <Arguments>
215       filename_list:
216               String list of package filenames.
217
218    <Exceptions>
219       TypeError:
220               If a type mismatch or parameter error is detected.
221
222    <Side Effects>
223       None.
224
225    <Returns>
226       String list of dependencies required by the given package files.
227    """
228    # check params
229    ravenlib.typecheck.stringlist(filename_list, "filename_list", "storktar.get_packages_require")
230
231    return []
232
233
234 def get_packages_files(tarball_filename_list):
235     file_list = []
236     for tarball_filename in tarball_filename_list:
237         if is_package_understood(tarball_filename):
238             info = _get_package_info(tarball_filename)
239             if info:
240                 for fn in info.get("files_nohomedir", []):
241                     file_list.append(fn)
242     return file_list
243
244 def get_package_info(filename):
245     result = _get_package_info(filename)
246     if not result:
247         return None
248
249     return [result['name'], result['version'], result['release'], result['size']]
250
251 def _get_package_info(filename):
252    """
253    <Purpose>
254       Given a package filename, returns a string list of package
255       information of the form:
256         [NAME, VERSION, RELEASE, SIZE, FILENAME_LIST]
257
258    <Arguments>
259       filename:
260               Package filename.
261
262    <Exceptions>
263       TypeError:
264               If a type mismatch or parameter error is detected.
265
266    <Side Effects>
267       None.
268
269    <Returns>
270       String list containing package information, or None on error.
271    """
272    # check params
273    ravenlib.typecheck.simple(filename, "filename", str, "storktar._get_package_info")
274
275    if not is_package_understood(filename):
276       return None
277
278    tarinfo = __get_tar_info(filename)
279    if not tarinfo:
280       return None
281
282    (out, err, status) = (tarinfo[4], tarinfo[5], tarinfo[6])
283
284    if status != 0:
285       return None
286
287    # name
288    name = os.path.basename(filename)
289
290    # remove .tar extensions
291    for ext in RECOGNIZED_EXTENSIONS:
292       if name.endswith(ext):
293          name = name[0:-len(ext)]
294          break
295
296    # version, release
297    fields = name.split("-")
298    if len(fields) == 2:
299       name = fields[0]
300       version = fields[1]
301       release = DEFAULT_RELEASE
302    elif len(fields) == 3:
303       name = fields[0]
304       version = fields[1]
305       release = fields[2]
306    else:
307       version = DEFAULT_VERSION
308       release = DEFAULT_RELEASE
309
310    homedir = __get_homedir()
311
312    # size
313    size = 0
314    file_list = []
315    file_list_nohomedir = []
316    for line in out:
317       # the format of a line of tar output is:
318       # <permissions> <username> <size> <date> <time> <filename>
319       tmp = line.strip("\n").split()
320       if len(tmp) < 6:
321          continue
322       file_list.append(os.path.join(homedir, tmp[5]))
323       file_list_nohomedir.append(tmp[5])
324       size += int(tmp[2])
325    size = str(size)
326
327    dict = {}
328    dict['name'] = name
329    dict['version'] = version
330    dict['release'] = release
331    dict['size'] = size
332    dict['homedir'] = homedir
333    dict['files'] = file_list
334    dict['files_nohomedir'] = file_list_nohomedir
335
336    return dict
337
338
339
340
341 def get_installed_versions(package_list):
342    """
343    <Purpose>
344       Given a package list, returns a list containing the name
345       if installed,
346
347    <Arguments>
348       package_list:
349          List of strings containing the names of the packages to get
350          version information for.
351
352    <Exceptions>
353       TypeError:
354          If a type mismatch or parameter error is detected.
355          Or if package info directory isn't specified.
356
357    <Side Effects>
358       None.
359
360    <Returns>
361       String list containing each package name and version (in the format
362       "name = version-release"). Packages that are not installed are not
363       listed. If a package has more than one installed version, then multiple
364       results may be returned for that package.
365    """
366    # check the arguments
367    ravenlib.typecheck.stringlist(package_list, "package_list", "storktar.get_installed_versions")
368
369    # nothing given
370    if len(package_list) == 0:
371       return []
372
373    # a list which holds the info about each package
374    installed_packages = []
375
376    for package in package_list:
377       info = __read_packinfo(package)
378       if info != None:
379          # installed, format info
380          installed_packages.append(info['name'] + " = " + info['version'] + "-" + info['release'])
381
382    return installed_packages
383
384
385
386
387
388 def get_installedpackages_fulfilling(dep_list):
389    """
390    <Purpose>
391       Given a string list of dependencies, returns a string list of 
392       installed packages that fulfill those package dependencies.
393
394    <Arguments>
395       dep_list:
396          String list of package dependencies.
397
398    <Exceptions>
399       None.
400
401    <Side Effects>
402       None.
403
404    <Returns>
405       String list of installed packages that meet the given dependencies.
406    """
407    # check params
408    ravenlib.typecheck.stringlist(dep_list, "dep_list", "storktar.get_installedpackages_fulfilling")
409
410    if len(dep_list) < 1:
411       return []
412
413    # TODO need to find file dependencies
414    retlist = []
415    for package in dep_list:
416       info = __read_packinfo(package)
417       if info == None:
418          continue
419       retlist.append(info['name'] + "-" + info['version'] + "-" + info['release'])
420
421    return retlist
422
423
424 def get_installedpackages_requiring(deplist):
425    """
426    <Purpose>
427       Return a list of all installed packages.
428
429    <Arguments>
430
431    <Exceptions>
432       None.
433
434    <Side Effects>
435       None.
436
437    <Returns>
438       String list of installed packages
439    """
440
441    # TODO finish me
442
443    return []
444
445
446    
447 def get_installedpackages():
448    """
449    <Purpose>
450       Return a list of all installed packages.
451
452    <Arguments>
453
454    <Exceptions>
455       None.
456
457    <Side Effects>
458       None.
459
460    <Returns>
461       String list of installed packages
462    """
463
464    # TODO finish me
465
466    return []
467
468
469
470 def get_installedpackages_provide(package_list):
471    """
472    <Purpose>
473       Given a string list of installed package names, returns a string 
474       list of all dependencies fulfilled by those packages. 
475
476    <Arguments>
477       package_list:
478          String list of installed package names.
479
480    <Exceptions>
481       None.
482
483    <Side Effects>
484       None.
485
486    <Returns>
487       String list of all dependencies fulfilled by the given packages.
488       Dependencies will either be in the form:
489         name=version-release (possible spaces around `=')
490       Or:
491         name
492    """
493    # check params
494    ravenlib.typecheck.stringlist(package_list, "package_list", "storktar.get_installedpackages_provide")
495
496    if len(package_list) < 1:
497       return []
498
499    retlist = []
500    for package in package_list:
501       info = __read_packinfo(package)
502       if info == None:
503          continue
504       retlist.append(info['name'] + " = " + info['version'] + "-" + info['release'])
505       
506    return retlist
507
508
509
510
511
512 def get_installedpackages_requires(package_list):
513    """
514    <Purpose>
515       Return a list of all dependencies required
516
517    <Arguments>
518
519    <Exceptions>
520       None.
521
522    <Side Effects>
523       None.
524
525    <Returns>
526       String list of installed packages
527    """
528
529    # TODO finish me
530
531    return []
532
533
534
535
536
537 def execute(trans_list):
538     """
539     <Purpose>
540        Installs packages with the given filenames.
541
542     <Arguments>
543        filename_list:
544                String list of filenames representing packages to install.
545
546     <Exceptions>
547        TypeError:
548           If a type mismatch or parameter error is detected.
549
550     <Side Effects>
551        None.
552
553     <Returns>
554        None.
555     """
556     # check the arguments
557     ravenlib.typecheck.simple(trans_list, "trans_list", list, "storktar.install")
558
559     if len(trans_list) < 1:
560        return
561
562     for trans in trans_list:
563         if (trans["op"] == transaction.INSTALL) or (trans["op"] == transaction.UPGRADE):
564             filename = trans["filename"]
565
566             # get package information from the file
567             packinfo = _get_package_info(filename)
568             if not packinfo:
569                 transaction.trans_set_status(trans, "failure", "tar packinfo error")
570                 raise StorkInstallFailureException(trans)
571
572             flag = __determine_package_type(filename)
573
574             homedir = __get_homedir()
575
576             # 1) install the package
577             args = ["tar", "-C", homedir+"/", "-P" + flag + "xvf", filename]
578             sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
579             (out, err) = sub.communicate()
580             out = out.split("\n")
581             err = err.split("\n")
582
583             if sub.returncode != 0:
584                 transaction.trans_set_status(trans, "failure", "un-tar error")
585                 raise StorkInstallFailureException(trans)
586
587             installed_files = []
588             for line in out:
589                 line = line.strip("\n")
590                 pathname = os.path.join(homedir, line)
591                 installed_files.append(pathname)
592
593             # We now know what files were installed. The set should be the same as
594             # packinfo['files'], but just in case it isn't, we'll update the packinfo
595             packinfo['files'] = installed_files
596
597             # 2) create packinfo file
598             try:
599                 __write_packinfo(packinfo)
600             except IOError:
601                 transaction.trans_set_status(trans, "failure", "tar create packinfo error")
602                 raise StorkInstallFailureException(trans)
603
604             # 3) run the autorun script
605             for fn in installed_files:
606                  if fn.endswith("autorun.sh"):
607                      autorun_dir = os.path.split(fn)[0]
608                      status = os.system("cd " + autorun_dir + "; chmod +x autorun.sh; ./autorun.sh 1>/dev/null")
609
610             transaction.trans_set_status(trans, "success", "installed")
611
612         elif (trans["op"] == transaction.REMOVE):
613             transaction.trans_set_status(trans, "failure", "unsupported", "removal of tar packages is not supported")
614             raise StorkUnsupportedException(trans)
615
616         else:
617             transaction.trans_set_status(trans, "failure", "unknown transaction op")
618             raise StorkException(trans)
619
620 glo_package_cache = {}
621
622 def __get_tar_info(filename):
623    """
624    <Purpose>
625       Determines the type of a tar package (z=gz, j=bzip2, ...) and reads the
626       file table from the tarball. Results are cached so that multiple calls
627       to this func do not need to process the tarball multiple times.
628
629    <Arguments>
630       filename:
631          name of file to get info
632          depend upon them. This likely makes no difference for tar packages.
633
634    <Side Effects>
635       None.
636
637    <Returns>
638       None if the file is not valid
639       otherwise, a tuple, (dev, ino, mtime, typeflag, stdout, stderr, status)
640          dev, ino, and mtime are used internally
641          typeflag = "j" or "z"
642          status = status from tar operation (probably 0)
643          stdout, stderr = output streams containing tar file table
644    """
645    global glo_package_cache
646
647    try:
648       stat = os.stat(filename)
649    except IOError:
650       return None
651    except OSError:
652       return None
653
654    # a simple caching scheme: if we've already determined the package type
655    # of this file before, then return previous results.
656    cached_result = glo_package_cache.get(filename, None)
657    if cached_result:
658       if (cached_result[0] == stat[ST_DEV]) and \
659          (cached_result[1] == stat[ST_INO]) and \
660          (cached_result[2] == stat[ST_MTIME]):
661          return cached_result
662
663    """ Returns the appropriate compression flag for invoking tar """
664    ravenlib.report.debug("__get_tar_info " + str(filename))
665    for flag in INSTALL_FLAGS:
666       args = ["tar", "-" + flag, "-tvf", filename]
667       sub = subprocess.Popen(args, stderr = subprocess.PIPE, stdout = subprocess.PIPE)
668       (out, err) = sub.communicate()
669       out = out.split("\n")
670       err = err.split("\n")
671
672       #ravenlib.report.debug("__get_tar_info: tar (" + " ".join(args)+ "):" + "\n".join(out))
673
674       if sub.returncode == 0 and len(out) > 0:
675          result = (stat[ST_DEV], stat[ST_INO], stat[ST_MTIME], \
676                    flag, out, err, sub.returncode)
677          glo_package_cache[filename] = result
678          ravenlib.report.debug("  returning type " + str(flag))
679          return result
680
681    ravenlib.report.debug("__get_tar_info: returning error")
682    return None
683
684 def __determine_package_type(filename):
685    info = __get_tar_info(filename)
686    if not info:
687       return False
688
689    # return the typeflag
690    return info[3]
691
692 def __write_packinfo(packinfo):
693     __init_packinfo()
694     packinfo['packinfoname'] = tarpackinfo_path + "/" + packinfo['name'] + ".packinfo"
695     infofile = open(packinfo['packinfoname'], 'w')
696     infofile.write(packinfo['name'] + "\n")
697     infofile.write(packinfo['version'] + "\n")
698     infofile.write(packinfo['release'] + "\n")
699     infofile.write("\t".join(packinfo['files']) + "\n")
700     infofile.close()
701
702
703 def __read_packinfo(packagename):
704    __init_packinfo()
705    filename = tarpackinfo_path + "/" + packagename + ".packinfo"
706
707    # see if package info exists (i.e. is package installed?)
708    if not os.path.isfile(filename):
709        return None
710
711    try:
712        lines = file(filename, "r").readlines()
713    except IOError:
714        return None
715
716    # check basic data integrity
717    if len(lines) < 4:
718       return None
719
720    packinfo = {}
721    packinfo['packinfoname'] = filename
722    packinfo['name'] = lines[0].strip("\n")
723    packinfo['version'] = lines[1].strip("\n")
724    packinfo['release'] = lines[2].strip("\n")
725    packinfo['files'] = lines[3].strip("\n").split("\t")
726
727    return packinfo
728
729 def __get_homedir():
730    try:
731       # use the home directory specified in the root user's password entry
732       pwent = pwd.getpwuid(0)
733       homedir = pwent.pw_dir
734    except:
735       # if the above fails for any reason, then we'll fall back to using HOME
736       homedir = None
737
738    # try getting the homedir from the 'HOME' environment variable
739    if (not homedir) or (not os.path.exists(homedir)):
740       homedir = os.environ.get("HOME", None)
741
742    return homedir
743
744
745
746