]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
Adapt to changes to Package-List field.
[dak.git] / daklib / utils.py
1 #!/usr/bin/env python
2 # vim:set et ts=4 sw=4:
3
4 """Utility functions
5
6 @contact: Debian FTP Master <ftpmaster@debian.org>
7 @copyright: 2000, 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
8 @license: GNU General Public License version 2 or later
9 """
10
11 # This program is free software; you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation; either version 2 of the License, or
14 # (at your option) any later version.
15
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20
21 # You should have received a copy of the GNU General Public License
22 # along with this program; if not, write to the Free Software
23 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
24
25 import commands
26 import email.Header
27 import os
28 import pwd
29 import select
30 import socket
31 import shutil
32 import sys
33 import tempfile
34 import traceback
35 import stat
36 import apt_pkg
37 import time
38 import re
39 import email as modemail
40 import subprocess
41
42 from dbconn import DBConn, get_architecture, get_component, get_suite, \
43                    get_override_type, Keyring, session_wrapper, \
44                    get_active_keyring_paths, get_primary_keyring_path
45 from sqlalchemy import desc
46 from dak_exceptions import *
47 from gpg import SignedFile
48 from textutils import fix_maintainer
49 from regexes import re_html_escaping, html_escaping, re_single_line_field, \
50                     re_multi_line_field, re_srchasver, re_taint_free, \
51                     re_gpg_uid, re_re_mark, re_whitespace_comment, re_issource, \
52                     re_is_orig_source
53
54 from formats import parse_format, validate_changes_format
55 from srcformats import get_format_from_string
56 from collections import defaultdict
57
58 ################################################################################
59
60 default_config = "/etc/dak/dak.conf"     #: default dak config, defines host properties
61 default_apt_config = "/etc/dak/apt.conf" #: default apt config, not normally used
62
63 alias_cache = None        #: Cache for email alias checks
64 key_uid_email_cache = {}  #: Cache for email addresses from gpg key uids
65
66 # (hashname, function, earliest_changes_version)
67 known_hashes = [("sha1", apt_pkg.sha1sum, (1, 8)),
68                 ("sha256", apt_pkg.sha256sum, (1, 8))] #: hashes we accept for entries in .changes/.dsc
69
70 # Monkeypatch commands.getstatusoutput as it may not return the correct exit
71 # code in lenny's Python. This also affects commands.getoutput and
72 # commands.getstatus.
73 def dak_getstatusoutput(cmd):
74     pipe = subprocess.Popen(cmd, shell=True, universal_newlines=True,
75         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
76
77     output = pipe.stdout.read()
78
79     pipe.wait()
80
81     if output[-1:] == '\n':
82         output = output[:-1]
83
84     ret = pipe.wait()
85     if ret is None:
86         ret = 0
87
88     return ret, output
89 commands.getstatusoutput = dak_getstatusoutput
90
91 ################################################################################
92
93 def html_escape(s):
94     """ Escape html chars """
95     return re_html_escaping.sub(lambda x: html_escaping.get(x.group(0)), s)
96
97 ################################################################################
98
99 def open_file(filename, mode='r'):
100     """
101     Open C{file}, return fileobject.
102
103     @type filename: string
104     @param filename: path/filename to open
105
106     @type mode: string
107     @param mode: open mode
108
109     @rtype: fileobject
110     @return: open fileobject
111
112     @raise CantOpenError: If IOError is raised by open, reraise it as CantOpenError.
113
114     """
115     try:
116         f = open(filename, mode)
117     except IOError:
118         raise CantOpenError(filename)
119     return f
120
121 ################################################################################
122
123 def our_raw_input(prompt=""):
124     if prompt:
125         while 1:
126             try:
127                 sys.stdout.write(prompt)
128                 break
129             except IOError:
130                 pass
131     sys.stdout.flush()
132     try:
133         ret = raw_input()
134         return ret
135     except EOFError:
136         sys.stderr.write("\nUser interrupt (^D).\n")
137         raise SystemExit
138
139 ################################################################################
140
141 def extract_component_from_section(section, session=None):
142     component = ""
143
144     if section.find('/') != -1:
145         component = section.split('/')[0]
146
147     # Expand default component
148     if component == "":
149         comp = get_component(section, session)
150         if comp is None:
151             component = "main"
152         else:
153             component = comp.component_name
154
155     return (section, component)
156
157 ################################################################################
158
159 def parse_deb822(armored_contents, signing_rules=0, keyrings=None, session=None):
160     require_signature = True
161     if keyrings == None:
162         keyrings = []
163         require_signature = False
164
165     signed_file = SignedFile(armored_contents, keyrings=keyrings, require_signature=require_signature)
166     contents = signed_file.contents
167
168     error = ""
169     changes = {}
170
171     # Split the lines in the input, keeping the linebreaks.
172     lines = contents.splitlines(True)
173
174     if len(lines) == 0:
175         raise ParseChangesError("[Empty changes file]")
176
177     # Reindex by line number so we can easily verify the format of
178     # .dsc files...
179     index = 0
180     indexed_lines = {}
181     for line in lines:
182         index += 1
183         indexed_lines[index] = line[:-1]
184
185     num_of_lines = len(indexed_lines.keys())
186     index = 0
187     first = -1
188     while index < num_of_lines:
189         index += 1
190         line = indexed_lines[index]
191         if line == "" and signing_rules == 1:
192             if index != num_of_lines:
193                 raise InvalidDscError(index)
194             break
195         slf = re_single_line_field.match(line)
196         if slf:
197             field = slf.groups()[0].lower()
198             changes[field] = slf.groups()[1]
199             first = 1
200             continue
201         if line == " .":
202             changes[field] += '\n'
203             continue
204         mlf = re_multi_line_field.match(line)
205         if mlf:
206             if first == -1:
207                 raise ParseChangesError("'%s'\n [Multi-line field continuing on from nothing?]" % (line))
208             if first == 1 and changes[field] != "":
209                 changes[field] += '\n'
210             first = 0
211             changes[field] += mlf.groups()[0] + '\n'
212             continue
213         error += line
214
215     changes["filecontents"] = armored_contents
216
217     if changes.has_key("source"):
218         # Strip the source version in brackets from the source field,
219         # put it in the "source-version" field instead.
220         srcver = re_srchasver.search(changes["source"])
221         if srcver:
222             changes["source"] = srcver.group(1)
223             changes["source-version"] = srcver.group(2)
224
225     if error:
226         raise ParseChangesError(error)
227
228     return changes
229
230 ################################################################################
231
232 def parse_changes(filename, signing_rules=0, dsc_file=0, keyrings=None):
233     """
234     Parses a changes file and returns a dictionary where each field is a
235     key.  The mandatory first argument is the filename of the .changes
236     file.
237
238     signing_rules is an optional argument:
239
240       - If signing_rules == -1, no signature is required.
241       - If signing_rules == 0 (the default), a signature is required.
242       - If signing_rules == 1, it turns on the same strict format checking
243         as dpkg-source.
244
245     The rules for (signing_rules == 1)-mode are:
246
247       - The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
248         followed by any PGP header data and must end with a blank line.
249
250       - The data section must end with a blank line and must be followed by
251         "-----BEGIN PGP SIGNATURE-----".
252     """
253
254     changes_in = open_file(filename)
255     content = changes_in.read()
256     changes_in.close()
257     try:
258         unicode(content, 'utf-8')
259     except UnicodeError:
260         raise ChangesUnicodeError("Changes file not proper utf-8")
261     changes = parse_deb822(content, signing_rules, keyrings=keyrings)
262
263
264     if not dsc_file:
265         # Finally ensure that everything needed for .changes is there
266         must_keywords = ('Format', 'Date', 'Source', 'Binary', 'Architecture', 'Version',
267                          'Distribution', 'Maintainer', 'Description', 'Changes', 'Files')
268
269         missingfields=[]
270         for keyword in must_keywords:
271             if not changes.has_key(keyword.lower()):
272                 missingfields.append(keyword)
273
274                 if len(missingfields):
275                     raise ParseChangesError("Missing mandantory field(s) in changes file (policy 5.5): %s" % (missingfields))
276
277     return changes
278
279 ################################################################################
280
281 def hash_key(hashname):
282     return '%ssum' % hashname
283
284 ################################################################################
285
286 def create_hash(where, files, hashname, hashfunc):
287     """
288     create_hash extends the passed files dict with the given hash by
289     iterating over all files on disk and passing them to the hashing
290     function given.
291     """
292
293     rejmsg = []
294     for f in files.keys():
295         try:
296             file_handle = open_file(f)
297         except CantOpenError:
298             rejmsg.append("Could not open file %s for checksumming" % (f))
299             continue
300
301         files[f][hash_key(hashname)] = hashfunc(file_handle)
302
303         file_handle.close()
304     return rejmsg
305
306 ################################################################################
307
308 def check_hash(where, files, hashname, hashfunc):
309     """
310     check_hash checks the given hash in the files dict against the actual
311     files on disk.  The hash values need to be present consistently in
312     all file entries.  It does not modify its input in any way.
313     """
314
315     rejmsg = []
316     for f in files.keys():
317         file_handle = None
318         try:
319             try:
320                 file_handle = open_file(f)
321
322                 # Check for the hash entry, to not trigger a KeyError.
323                 if not files[f].has_key(hash_key(hashname)):
324                     rejmsg.append("%s: misses %s checksum in %s" % (f, hashname,
325                         where))
326                     continue
327
328                 # Actually check the hash for correctness.
329                 if hashfunc(file_handle) != files[f][hash_key(hashname)]:
330                     rejmsg.append("%s: %s check failed in %s" % (f, hashname,
331                         where))
332             except CantOpenError:
333                 # TODO: This happens when the file is in the pool.
334                 # warn("Cannot open file %s" % f)
335                 continue
336         finally:
337             if file_handle:
338                 file_handle.close()
339     return rejmsg
340
341 ################################################################################
342
343 def check_size(where, files):
344     """
345     check_size checks the file sizes in the passed files dict against the
346     files on disk.
347     """
348
349     rejmsg = []
350     for f in files.keys():
351         try:
352             entry = os.stat(f)
353         except OSError as exc:
354             if exc.errno == 2:
355                 # TODO: This happens when the file is in the pool.
356                 continue
357             raise
358
359         actual_size = entry[stat.ST_SIZE]
360         size = int(files[f]["size"])
361         if size != actual_size:
362             rejmsg.append("%s: actual file size (%s) does not match size (%s) in %s"
363                    % (f, actual_size, size, where))
364     return rejmsg
365
366 ################################################################################
367
368 def check_dsc_files(dsc_filename, dsc=None, dsc_files=None):
369     """
370     Verify that the files listed in the Files field of the .dsc are
371     those expected given the announced Format.
372
373     @type dsc_filename: string
374     @param dsc_filename: path of .dsc file
375
376     @type dsc: dict
377     @param dsc: the content of the .dsc parsed by C{parse_changes()}
378
379     @type dsc_files: dict
380     @param dsc_files: the file list returned by C{build_file_list()}
381
382     @rtype: list
383     @return: all errors detected
384     """
385     rejmsg = []
386
387     # Parse the file if needed
388     if dsc is None:
389         dsc = parse_changes(dsc_filename, signing_rules=1, dsc_file=1);
390
391     if dsc_files is None:
392         dsc_files = build_file_list(dsc, is_a_dsc=1)
393
394     # Ensure .dsc lists proper set of source files according to the format
395     # announced
396     has = defaultdict(lambda: 0)
397
398     ftype_lookup = (
399         (r'orig.tar.gz',               ('orig_tar_gz', 'orig_tar')),
400         (r'diff.gz',                   ('debian_diff',)),
401         (r'tar.gz',                    ('native_tar_gz', 'native_tar')),
402         (r'debian\.tar\.(gz|bz2|xz)',  ('debian_tar',)),
403         (r'orig\.tar\.(gz|bz2|xz)',    ('orig_tar',)),
404         (r'tar\.(gz|bz2|xz)',          ('native_tar',)),
405         (r'orig-.+\.tar\.(gz|bz2|xz)', ('more_orig_tar',)),
406     )
407
408     for f in dsc_files.keys():
409         m = re_issource.match(f)
410         if not m:
411             rejmsg.append("%s: %s in Files field not recognised as source."
412                           % (dsc_filename, f))
413             continue
414
415         # Populate 'has' dictionary by resolving keys in lookup table
416         matched = False
417         for regex, keys in ftype_lookup:
418             if re.match(regex, m.group(3)):
419                 matched = True
420                 for key in keys:
421                     has[key] += 1
422                 break
423
424         # File does not match anything in lookup table; reject
425         if not matched:
426             reject("%s: unexpected source file '%s'" % (dsc_filename, f))
427
428     # Check for multiple files
429     for file_type in ('orig_tar', 'native_tar', 'debian_tar', 'debian_diff'):
430         if has[file_type] > 1:
431             rejmsg.append("%s: lists multiple %s" % (dsc_filename, file_type))
432
433     # Source format specific tests
434     try:
435         format = get_format_from_string(dsc['format'])
436         rejmsg.extend([
437             '%s: %s' % (dsc_filename, x) for x in format.reject_msgs(has)
438         ])
439
440     except UnknownFormatError:
441         # Not an error here for now
442         pass
443
444     return rejmsg
445
446 ################################################################################
447
448 def check_hash_fields(what, manifest):
449     """
450     check_hash_fields ensures that there are no checksum fields in the
451     given dict that we do not know about.
452     """
453
454     rejmsg = []
455     hashes = map(lambda x: x[0], known_hashes)
456     for field in manifest:
457         if field.startswith("checksums-"):
458             hashname = field.split("-",1)[1]
459             if hashname not in hashes:
460                 rejmsg.append("Unsupported checksum field for %s "\
461                     "in %s" % (hashname, what))
462     return rejmsg
463
464 ################################################################################
465
466 def _ensure_changes_hash(changes, format, version, files, hashname, hashfunc):
467     if format >= version:
468         # The version should contain the specified hash.
469         func = check_hash
470
471         # Import hashes from the changes
472         rejmsg = parse_checksums(".changes", files, changes, hashname)
473         if len(rejmsg) > 0:
474             return rejmsg
475     else:
476         # We need to calculate the hash because it can't possibly
477         # be in the file.
478         func = create_hash
479     return func(".changes", files, hashname, hashfunc)
480
481 # We could add the orig which might be in the pool to the files dict to
482 # access the checksums easily.
483
484 def _ensure_dsc_hash(dsc, dsc_files, hashname, hashfunc):
485     """
486     ensure_dsc_hashes' task is to ensure that each and every *present* hash
487     in the dsc is correct, i.e. identical to the changes file and if necessary
488     the pool.  The latter task is delegated to check_hash.
489     """
490
491     rejmsg = []
492     if not dsc.has_key('Checksums-%s' % (hashname,)):
493         return rejmsg
494     # Import hashes from the dsc
495     parse_checksums(".dsc", dsc_files, dsc, hashname)
496     # And check it...
497     rejmsg.extend(check_hash(".dsc", dsc_files, hashname, hashfunc))
498     return rejmsg
499
500 ################################################################################
501
502 def parse_checksums(where, files, manifest, hashname):
503     rejmsg = []
504     field = 'checksums-%s' % hashname
505     if not field in manifest:
506         return rejmsg
507     for line in manifest[field].split('\n'):
508         if not line:
509             break
510         clist = line.strip().split(' ')
511         if len(clist) == 3:
512             checksum, size, checkfile = clist
513         else:
514             rejmsg.append("Cannot parse checksum line [%s]" % (line))
515             continue
516         if not files.has_key(checkfile):
517         # TODO: check for the file's entry in the original files dict, not
518         # the one modified by (auto)byhand and other weird stuff
519         #    rejmsg.append("%s: not present in files but in checksums-%s in %s" %
520         #        (file, hashname, where))
521             continue
522         if not files[checkfile]["size"] == size:
523             rejmsg.append("%s: size differs for files and checksums-%s entry "\
524                 "in %s" % (checkfile, hashname, where))
525             continue
526         files[checkfile][hash_key(hashname)] = checksum
527     for f in files.keys():
528         if not files[f].has_key(hash_key(hashname)):
529             rejmsg.append("%s: no entry in checksums-%s in %s" % (checkfile,
530                 hashname, where))
531     return rejmsg
532
533 ################################################################################
534
535 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
536
537 def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
538     files = {}
539
540     # Make sure we have a Files: field to parse...
541     if not changes.has_key(field):
542         raise NoFilesFieldError
543
544     # Validate .changes Format: field
545     if not is_a_dsc:
546         validate_changes_format(parse_format(changes['format']), field)
547
548     includes_section = (not is_a_dsc) and field == "files"
549
550     # Parse each entry/line:
551     for i in changes[field].split('\n'):
552         if not i:
553             break
554         s = i.split()
555         section = priority = ""
556         try:
557             if includes_section:
558                 (md5, size, section, priority, name) = s
559             else:
560                 (md5, size, name) = s
561         except ValueError:
562             raise ParseChangesError(i)
563
564         if section == "":
565             section = "-"
566         if priority == "":
567             priority = "-"
568
569         (section, component) = extract_component_from_section(section)
570
571         files[name] = dict(size=size, section=section,
572                            priority=priority, component=component)
573         files[name][hashname] = md5
574
575     return files
576
577 ################################################################################
578
579 # see http://bugs.debian.org/619131
580 def build_package_list(dsc, session = None):
581     if not dsc.has_key("package-list"):
582         return {}
583
584     packages = {}
585
586     for line in dsc["package-list"].split("\n"):
587         if not line:
588             break
589
590         fields = line.split()
591         name = fields[0]
592         package_type = fields[1]
593         (section, component) = extract_component_from_section(fields[2])
594         priority = fields[3]
595
596         # Validate type if we have a session
597         if session and get_override_type(package_type, session) is None:
598             # Maybe just warn and ignore? exit(1) might be a bit hard...
599             utils.fubar("invalid type (%s) in Package-List." % (package_type))
600
601         if name not in packages or packages[name]["type"] == "dsc":
602             packages[name] = dict(priority=priority, section=section, type=package_type, component=component, files=[])
603
604     return packages
605
606 ################################################################################
607
608 def send_mail (message, filename=""):
609     """sendmail wrapper, takes _either_ a message string or a file as arguments"""
610
611     # Check whether we're supposed to be sending mail
612     if Cnf.has_key("Dinstall::Options::No-Mail") and Cnf["Dinstall::Options::No-Mail"]:
613         return
614
615     # If we've been passed a string dump it into a temporary file
616     if message:
617         (fd, filename) = tempfile.mkstemp()
618         os.write (fd, message)
619         os.close (fd)
620
621     if Cnf.has_key("Dinstall::MailWhiteList") and \
622            Cnf["Dinstall::MailWhiteList"] != "":
623         message_in = open_file(filename)
624         message_raw = modemail.message_from_file(message_in)
625         message_in.close();
626
627         whitelist = [];
628         whitelist_in = open_file(Cnf["Dinstall::MailWhiteList"])
629         try:
630             for line in whitelist_in:
631                 if not re_whitespace_comment.match(line):
632                     if re_re_mark.match(line):
633                         whitelist.append(re.compile(re_re_mark.sub("", line.strip(), 1)))
634                     else:
635                         whitelist.append(re.compile(re.escape(line.strip())))
636         finally:
637             whitelist_in.close()
638
639         # Fields to check.
640         fields = ["To", "Bcc", "Cc"]
641         for field in fields:
642             # Check each field
643             value = message_raw.get(field, None)
644             if value != None:
645                 match = [];
646                 for item in value.split(","):
647                     (rfc822_maint, rfc2047_maint, name, email) = fix_maintainer(item.strip())
648                     mail_whitelisted = 0
649                     for wr in whitelist:
650                         if wr.match(email):
651                             mail_whitelisted = 1
652                             break
653                     if not mail_whitelisted:
654                         print "Skipping %s since it's not in %s" % (item, Cnf["Dinstall::MailWhiteList"])
655                         continue
656                     match.append(item)
657
658                 # Doesn't have any mail in whitelist so remove the header
659                 if len(match) == 0:
660                     del message_raw[field]
661                 else:
662                     message_raw.replace_header(field, ', '.join(match))
663
664         # Change message fields in order if we don't have a To header
665         if not message_raw.has_key("To"):
666             fields.reverse()
667             for field in fields:
668                 if message_raw.has_key(field):
669                     message_raw[fields[-1]] = message_raw[field]
670                     del message_raw[field]
671                     break
672             else:
673                 # Clean up any temporary files
674                 # and return, as we removed all recipients.
675                 if message:
676                     os.unlink (filename);
677                 return;
678
679         fd = os.open(filename, os.O_RDWR|os.O_EXCL, 0o700);
680         os.write (fd, message_raw.as_string(True));
681         os.close (fd);
682
683     # Invoke sendmail
684     (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
685     if (result != 0):
686         raise SendmailFailedError(output)
687
688     # Clean up any temporary files
689     if message:
690         os.unlink (filename)
691
692 ################################################################################
693
694 def poolify (source, component):
695     if component:
696         component += '/'
697     if source[:3] == "lib":
698         return component + source[:4] + '/' + source + '/'
699     else:
700         return component + source[:1] + '/' + source + '/'
701
702 ################################################################################
703
704 def move (src, dest, overwrite = 0, perms = 0o664):
705     if os.path.exists(dest) and os.path.isdir(dest):
706         dest_dir = dest
707     else:
708         dest_dir = os.path.dirname(dest)
709     if not os.path.exists(dest_dir):
710         umask = os.umask(00000)
711         os.makedirs(dest_dir, 0o2775)
712         os.umask(umask)
713     #print "Moving %s to %s..." % (src, dest)
714     if os.path.exists(dest) and os.path.isdir(dest):
715         dest += '/' + os.path.basename(src)
716     # Don't overwrite unless forced to
717     if os.path.exists(dest):
718         if not overwrite:
719             fubar("Can't move %s to %s - file already exists." % (src, dest))
720         else:
721             if not os.access(dest, os.W_OK):
722                 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
723     shutil.copy2(src, dest)
724     os.chmod(dest, perms)
725     os.unlink(src)
726
727 def copy (src, dest, overwrite = 0, perms = 0o664):
728     if os.path.exists(dest) and os.path.isdir(dest):
729         dest_dir = dest
730     else:
731         dest_dir = os.path.dirname(dest)
732     if not os.path.exists(dest_dir):
733         umask = os.umask(00000)
734         os.makedirs(dest_dir, 0o2775)
735         os.umask(umask)
736     #print "Copying %s to %s..." % (src, dest)
737     if os.path.exists(dest) and os.path.isdir(dest):
738         dest += '/' + os.path.basename(src)
739     # Don't overwrite unless forced to
740     if os.path.exists(dest):
741         if not overwrite:
742             raise FileExistsError
743         else:
744             if not os.access(dest, os.W_OK):
745                 raise CantOverwriteError
746     shutil.copy2(src, dest)
747     os.chmod(dest, perms)
748
749 ################################################################################
750
751 def where_am_i ():
752     res = socket.getfqdn()
753     database_hostname = Cnf.get("Config::" + res + "::DatabaseHostname")
754     if database_hostname:
755         return database_hostname
756     else:
757         return res
758
759 def which_conf_file ():
760     if os.getenv('DAK_CONFIG'):
761         return os.getenv('DAK_CONFIG')
762
763     res = socket.getfqdn()
764     # In case we allow local config files per user, try if one exists
765     if Cnf.FindB("Config::" + res + "::AllowLocalConfig"):
766         homedir = os.getenv("HOME")
767         confpath = os.path.join(homedir, "/etc/dak.conf")
768         if os.path.exists(confpath):
769             apt_pkg.ReadConfigFileISC(Cnf,default_config)
770
771     # We are still in here, so there is no local config file or we do
772     # not allow local files. Do the normal stuff.
773     if Cnf.get("Config::" + res + "::DakConfig"):
774         return Cnf["Config::" + res + "::DakConfig"]
775
776     return default_config
777
778 def which_apt_conf_file ():
779     res = socket.getfqdn()
780     # In case we allow local config files per user, try if one exists
781     if Cnf.FindB("Config::" + res + "::AllowLocalConfig"):
782         homedir = os.getenv("HOME")
783         confpath = os.path.join(homedir, "/etc/dak.conf")
784         if os.path.exists(confpath):
785             apt_pkg.ReadConfigFileISC(Cnf,default_config)
786
787     if Cnf.get("Config::" + res + "::AptConfig"):
788         return Cnf["Config::" + res + "::AptConfig"]
789     else:
790         return default_apt_config
791
792 def which_alias_file():
793     hostname = socket.getfqdn()
794     aliasfn = '/var/lib/misc/'+hostname+'/forward-alias'
795     if os.path.exists(aliasfn):
796         return aliasfn
797     else:
798         return None
799
800 ################################################################################
801
802 def TemplateSubst(subst_map, filename):
803     """ Perform a substition of template """
804     templatefile = open_file(filename)
805     template = templatefile.read()
806     for k, v in subst_map.iteritems():
807         template = template.replace(k, str(v))
808     templatefile.close()
809     return template
810
811 ################################################################################
812
813 def fubar(msg, exit_code=1):
814     sys.stderr.write("E: %s\n" % (msg))
815     sys.exit(exit_code)
816
817 def warn(msg):
818     sys.stderr.write("W: %s\n" % (msg))
819
820 ################################################################################
821
822 # Returns the user name with a laughable attempt at rfc822 conformancy
823 # (read: removing stray periods).
824 def whoami ():
825     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
826
827 def getusername ():
828     return pwd.getpwuid(os.getuid())[0]
829
830 ################################################################################
831
832 def size_type (c):
833     t  = " B"
834     if c > 10240:
835         c = c / 1024
836         t = " KB"
837     if c > 10240:
838         c = c / 1024
839         t = " MB"
840     return ("%d%s" % (c, t))
841
842 ################################################################################
843
844 def cc_fix_changes (changes):
845     o = changes.get("architecture", "")
846     if o:
847         del changes["architecture"]
848     changes["architecture"] = {}
849     for j in o.split():
850         changes["architecture"][j] = 1
851
852 def changes_compare (a, b):
853     """ Sort by source name, source version, 'have source', and then by filename """
854     try:
855         a_changes = parse_changes(a)
856     except:
857         return -1
858
859     try:
860         b_changes = parse_changes(b)
861     except:
862         return 1
863
864     cc_fix_changes (a_changes)
865     cc_fix_changes (b_changes)
866
867     # Sort by source name
868     a_source = a_changes.get("source")
869     b_source = b_changes.get("source")
870     q = cmp (a_source, b_source)
871     if q:
872         return q
873
874     # Sort by source version
875     a_version = a_changes.get("version", "0")
876     b_version = b_changes.get("version", "0")
877     q = apt_pkg.VersionCompare(a_version, b_version)
878     if q:
879         return q
880
881     # Sort by 'have source'
882     a_has_source = a_changes["architecture"].get("source")
883     b_has_source = b_changes["architecture"].get("source")
884     if a_has_source and not b_has_source:
885         return -1
886     elif b_has_source and not a_has_source:
887         return 1
888
889     # Fall back to sort by filename
890     return cmp(a, b)
891
892 ################################################################################
893
894 def find_next_free (dest, too_many=100):
895     extra = 0
896     orig_dest = dest
897     while os.path.exists(dest) and extra < too_many:
898         dest = orig_dest + '.' + repr(extra)
899         extra += 1
900     if extra >= too_many:
901         raise NoFreeFilenameError
902     return dest
903
904 ################################################################################
905
906 def result_join (original, sep = '\t'):
907     resultlist = []
908     for i in xrange(len(original)):
909         if original[i] == None:
910             resultlist.append("")
911         else:
912             resultlist.append(original[i])
913     return sep.join(resultlist)
914
915 ################################################################################
916
917 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
918     out = ""
919     for line in str.split('\n'):
920         line = line.strip()
921         if line or include_blank_lines:
922             out += "%s%s\n" % (prefix, line)
923     # Strip trailing new line
924     if out:
925         out = out[:-1]
926     return out
927
928 ################################################################################
929
930 def validate_changes_file_arg(filename, require_changes=1):
931     """
932     'filename' is either a .changes or .dak file.  If 'filename' is a
933     .dak file, it's changed to be the corresponding .changes file.  The
934     function then checks if the .changes file a) exists and b) is
935     readable and returns the .changes filename if so.  If there's a
936     problem, the next action depends on the option 'require_changes'
937     argument:
938
939       - If 'require_changes' == -1, errors are ignored and the .changes
940         filename is returned.
941       - If 'require_changes' == 0, a warning is given and 'None' is returned.
942       - If 'require_changes' == 1, a fatal error is raised.
943
944     """
945     error = None
946
947     orig_filename = filename
948     if filename.endswith(".dak"):
949         filename = filename[:-4]+".changes"
950
951     if not filename.endswith(".changes"):
952         error = "invalid file type; not a changes file"
953     else:
954         if not os.access(filename,os.R_OK):
955             if os.path.exists(filename):
956                 error = "permission denied"
957             else:
958                 error = "file not found"
959
960     if error:
961         if require_changes == 1:
962             fubar("%s: %s." % (orig_filename, error))
963         elif require_changes == 0:
964             warn("Skipping %s - %s" % (orig_filename, error))
965             return None
966         else: # We only care about the .dak file
967             return filename
968     else:
969         return filename
970
971 ################################################################################
972
973 def real_arch(arch):
974     return (arch != "source" and arch != "all")
975
976 ################################################################################
977
978 def join_with_commas_and(list):
979     if len(list) == 0: return "nothing"
980     if len(list) == 1: return list[0]
981     return ", ".join(list[:-1]) + " and " + list[-1]
982
983 ################################################################################
984
985 def pp_deps (deps):
986     pp_deps = []
987     for atom in deps:
988         (pkg, version, constraint) = atom
989         if constraint:
990             pp_dep = "%s (%s %s)" % (pkg, constraint, version)
991         else:
992             pp_dep = pkg
993         pp_deps.append(pp_dep)
994     return " |".join(pp_deps)
995
996 ################################################################################
997
998 def get_conf():
999     return Cnf
1000
1001 ################################################################################
1002
1003 def parse_args(Options):
1004     """ Handle -a, -c and -s arguments; returns them as SQL constraints """
1005     # XXX: This should go away and everything which calls it be converted
1006     #      to use SQLA properly.  For now, we'll just fix it not to use
1007     #      the old Pg interface though
1008     session = DBConn().session()
1009     # Process suite
1010     if Options["Suite"]:
1011         suite_ids_list = []
1012         for suitename in split_args(Options["Suite"]):
1013             suite = get_suite(suitename, session=session)
1014             if not suite or suite.suite_id is None:
1015                 warn("suite '%s' not recognised." % (suite and suite.suite_name or suitename))
1016             else:
1017                 suite_ids_list.append(suite.suite_id)
1018         if suite_ids_list:
1019             con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
1020         else:
1021             fubar("No valid suite given.")
1022     else:
1023         con_suites = ""
1024
1025     # Process component
1026     if Options["Component"]:
1027         component_ids_list = []
1028         for componentname in split_args(Options["Component"]):
1029             component = get_component(componentname, session=session)
1030             if component is None:
1031                 warn("component '%s' not recognised." % (componentname))
1032             else:
1033                 component_ids_list.append(component.component_id)
1034         if component_ids_list:
1035             con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
1036         else:
1037             fubar("No valid component given.")
1038     else:
1039         con_components = ""
1040
1041     # Process architecture
1042     con_architectures = ""
1043     check_source = 0
1044     if Options["Architecture"]:
1045         arch_ids_list = []
1046         for archname in split_args(Options["Architecture"]):
1047             if archname == "source":
1048                 check_source = 1
1049             else:
1050                 arch = get_architecture(archname, session=session)
1051                 if arch is None:
1052                     warn("architecture '%s' not recognised." % (archname))
1053                 else:
1054                     arch_ids_list.append(arch.arch_id)
1055         if arch_ids_list:
1056             con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
1057         else:
1058             if not check_source:
1059                 fubar("No valid architecture given.")
1060     else:
1061         check_source = 1
1062
1063     return (con_suites, con_architectures, con_components, check_source)
1064
1065 ################################################################################
1066
1067 # Inspired(tm) by Bryn Keller's print_exc_plus (See
1068 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
1069
1070 def print_exc():
1071     tb = sys.exc_info()[2]
1072     while tb.tb_next:
1073         tb = tb.tb_next
1074     stack = []
1075     frame = tb.tb_frame
1076     while frame:
1077         stack.append(frame)
1078         frame = frame.f_back
1079     stack.reverse()
1080     traceback.print_exc()
1081     for frame in stack:
1082         print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
1083                                              frame.f_code.co_filename,
1084                                              frame.f_lineno)
1085         for key, value in frame.f_locals.items():
1086             print "\t%20s = " % key,
1087             try:
1088                 print value
1089             except:
1090                 print "<unable to print>"
1091
1092 ################################################################################
1093
1094 def try_with_debug(function):
1095     try:
1096         function()
1097     except SystemExit:
1098         raise
1099     except:
1100         print_exc()
1101
1102 ################################################################################
1103
1104 def arch_compare_sw (a, b):
1105     """
1106     Function for use in sorting lists of architectures.
1107
1108     Sorts normally except that 'source' dominates all others.
1109     """
1110
1111     if a == "source" and b == "source":
1112         return 0
1113     elif a == "source":
1114         return -1
1115     elif b == "source":
1116         return 1
1117
1118     return cmp (a, b)
1119
1120 ################################################################################
1121
1122 def split_args (s, dwim=1):
1123     """
1124     Split command line arguments which can be separated by either commas
1125     or whitespace.  If dwim is set, it will complain about string ending
1126     in comma since this usually means someone did 'dak ls -a i386, m68k
1127     foo' or something and the inevitable confusion resulting from 'm68k'
1128     being treated as an argument is undesirable.
1129     """
1130
1131     if s.find(",") == -1:
1132         return s.split()
1133     else:
1134         if s[-1:] == "," and dwim:
1135             fubar("split_args: found trailing comma, spurious space maybe?")
1136         return s.split(",")
1137
1138 ################################################################################
1139
1140 def gpgv_get_status_output(cmd, status_read, status_write):
1141     """
1142     Our very own version of commands.getouputstatus(), hacked to support
1143     gpgv's status fd.
1144     """
1145
1146     cmd = ['/bin/sh', '-c', cmd]
1147     p2cread, p2cwrite = os.pipe()
1148     c2pread, c2pwrite = os.pipe()
1149     errout, errin = os.pipe()
1150     pid = os.fork()
1151     if pid == 0:
1152         # Child
1153         os.close(0)
1154         os.close(1)
1155         os.dup(p2cread)
1156         os.dup(c2pwrite)
1157         os.close(2)
1158         os.dup(errin)
1159         for i in range(3, 256):
1160             if i != status_write:
1161                 try:
1162                     os.close(i)
1163                 except:
1164                     pass
1165         try:
1166             os.execvp(cmd[0], cmd)
1167         finally:
1168             os._exit(1)
1169
1170     # Parent
1171     os.close(p2cread)
1172     os.dup2(c2pread, c2pwrite)
1173     os.dup2(errout, errin)
1174
1175     output = status = ""
1176     while 1:
1177         i, o, e = select.select([c2pwrite, errin, status_read], [], [])
1178         more_data = []
1179         for fd in i:
1180             r = os.read(fd, 8196)
1181             if len(r) > 0:
1182                 more_data.append(fd)
1183                 if fd == c2pwrite or fd == errin:
1184                     output += r
1185                 elif fd == status_read:
1186                     status += r
1187                 else:
1188                     fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
1189         if not more_data:
1190             pid, exit_status = os.waitpid(pid, 0)
1191             try:
1192                 os.close(status_write)
1193                 os.close(status_read)
1194                 os.close(c2pread)
1195                 os.close(c2pwrite)
1196                 os.close(p2cwrite)
1197                 os.close(errin)
1198                 os.close(errout)
1199             except:
1200                 pass
1201             break
1202
1203     return output, status, exit_status
1204
1205 ################################################################################
1206
1207 def process_gpgv_output(status):
1208     # Process the status-fd output
1209     keywords = {}
1210     internal_error = ""
1211     for line in status.split('\n'):
1212         line = line.strip()
1213         if line == "":
1214             continue
1215         split = line.split()
1216         if len(split) < 2:
1217             internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
1218             continue
1219         (gnupg, keyword) = split[:2]
1220         if gnupg != "[GNUPG:]":
1221             internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
1222             continue
1223         args = split[2:]
1224         if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
1225             internal_error += "found duplicate status token ('%s').\n" % (keyword)
1226             continue
1227         else:
1228             keywords[keyword] = args
1229
1230     return (keywords, internal_error)
1231
1232 ################################################################################
1233
1234 def retrieve_key (filename, keyserver=None, keyring=None):
1235     """
1236     Retrieve the key that signed 'filename' from 'keyserver' and
1237     add it to 'keyring'.  Returns nothing on success, or an error message
1238     on error.
1239     """
1240
1241     # Defaults for keyserver and keyring
1242     if not keyserver:
1243         keyserver = Cnf["Dinstall::KeyServer"]
1244     if not keyring:
1245         keyring = get_primary_keyring_path()
1246
1247     # Ensure the filename contains no shell meta-characters or other badness
1248     if not re_taint_free.match(filename):
1249         return "%s: tainted filename" % (filename)
1250
1251     # Invoke gpgv on the file
1252     status_read, status_write = os.pipe()
1253     cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
1254     (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
1255
1256     # Process the status-fd output
1257     (keywords, internal_error) = process_gpgv_output(status)
1258     if internal_error:
1259         return internal_error
1260
1261     if not keywords.has_key("NO_PUBKEY"):
1262         return "didn't find expected NO_PUBKEY in gpgv status-fd output"
1263
1264     fingerprint = keywords["NO_PUBKEY"][0]
1265     # XXX - gpg sucks.  You can't use --secret-keyring=/dev/null as
1266     # it'll try to create a lockfile in /dev.  A better solution might
1267     # be a tempfile or something.
1268     cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
1269           % (Cnf["Dinstall::SigningKeyring"])
1270     cmd += " --keyring %s --keyserver %s --recv-key %s" \
1271            % (keyring, keyserver, fingerprint)
1272     (result, output) = commands.getstatusoutput(cmd)
1273     if (result != 0):
1274         return "'%s' failed with exit code %s" % (cmd, result)
1275
1276     return ""
1277
1278 ################################################################################
1279
1280 def gpg_keyring_args(keyrings=None):
1281     if not keyrings:
1282         keyrings = get_active_keyring_paths()
1283
1284     return " ".join(["--keyring %s" % x for x in keyrings])
1285
1286 ################################################################################
1287 @session_wrapper
1288 def check_signature (sig_filename, data_filename="", keyrings=None, autofetch=None, session=None):
1289     """
1290     Check the signature of a file and return the fingerprint if the
1291     signature is valid or 'None' if it's not.  The first argument is the
1292     filename whose signature should be checked.  The second argument is a
1293     reject function and is called when an error is found.  The reject()
1294     function must allow for two arguments: the first is the error message,
1295     the second is an optional prefix string.  It's possible for reject()
1296     to be called more than once during an invocation of check_signature().
1297     The third argument is optional and is the name of the files the
1298     detached signature applies to.  The fourth argument is optional and is
1299     a *list* of keyrings to use.  'autofetch' can either be None, True or
1300     False.  If None, the default behaviour specified in the config will be
1301     used.
1302     """
1303
1304     rejects = []
1305
1306     # Ensure the filename contains no shell meta-characters or other badness
1307     if not re_taint_free.match(sig_filename):
1308         rejects.append("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
1309         return (None, rejects)
1310
1311     if data_filename and not re_taint_free.match(data_filename):
1312         rejects.append("!!WARNING!! tainted data filename: '%s'." % (data_filename))
1313         return (None, rejects)
1314
1315     if not keyrings:
1316         keyrings = [ x.keyring_name for x in session.query(Keyring).filter(Keyring.active == True).all() ]
1317
1318     # Autofetch the signing key if that's enabled
1319     if autofetch == None:
1320         autofetch = Cnf.get("Dinstall::KeyAutoFetch")
1321     if autofetch:
1322         error_msg = retrieve_key(sig_filename)
1323         if error_msg:
1324             rejects.append(error_msg)
1325             return (None, rejects)
1326
1327     # Build the command line
1328     status_read, status_write = os.pipe()
1329     cmd = "gpgv --status-fd %s %s %s %s" % (
1330         status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
1331
1332     # Invoke gpgv on the file
1333     (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1334
1335     # Process the status-fd output
1336     (keywords, internal_error) = process_gpgv_output(status)
1337
1338     # If we failed to parse the status-fd output, let's just whine and bail now
1339     if internal_error:
1340         rejects.append("internal error while performing signature check on %s." % (sig_filename))
1341         rejects.append(internal_error, "")
1342         rejects.append("Please report the above errors to the Archive maintainers by replying to this mail.", "")
1343         return (None, rejects)
1344
1345     # Now check for obviously bad things in the processed output
1346     if keywords.has_key("KEYREVOKED"):
1347         rejects.append("The key used to sign %s has been revoked." % (sig_filename))
1348     if keywords.has_key("BADSIG"):
1349         rejects.append("bad signature on %s." % (sig_filename))
1350     if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1351         rejects.append("failed to check signature on %s." % (sig_filename))
1352     if keywords.has_key("NO_PUBKEY"):
1353         args = keywords["NO_PUBKEY"]
1354         if len(args) >= 1:
1355             key = args[0]
1356         rejects.append("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1357     if keywords.has_key("BADARMOR"):
1358         rejects.append("ASCII armour of signature was corrupt in %s." % (sig_filename))
1359     if keywords.has_key("NODATA"):
1360         rejects.append("no signature found in %s." % (sig_filename))
1361     if keywords.has_key("EXPKEYSIG"):
1362         args = keywords["EXPKEYSIG"]
1363         if len(args) >= 1:
1364             key = args[0]
1365         rejects.append("Signature made by expired key 0x%s" % (key))
1366     if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1367         args = keywords["KEYEXPIRED"]
1368         expiredate=""
1369         if len(args) >= 1:
1370             timestamp = args[0]
1371             if timestamp.count("T") == 0:
1372                 try:
1373                     expiredate = time.strftime("%Y-%m-%d", time.gmtime(float(timestamp)))
1374                 except ValueError:
1375                     expiredate = "unknown (%s)" % (timestamp)
1376             else:
1377                 expiredate = timestamp
1378         rejects.append("The key used to sign %s has expired on %s" % (sig_filename, expiredate))
1379
1380     if len(rejects) > 0:
1381         return (None, rejects)
1382
1383     # Next check gpgv exited with a zero return code
1384     if exit_status:
1385         rejects.append("gpgv failed while checking %s." % (sig_filename))
1386         if status.strip():
1387             rejects.append(prefix_multi_line_string(status, " [GPG status-fd output:] "))
1388         else:
1389             rejects.append(prefix_multi_line_string(output, " [GPG output:] "))
1390         return (None, rejects)
1391
1392     # Sanity check the good stuff we expect
1393     if not keywords.has_key("VALIDSIG"):
1394         rejects.append("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1395     else:
1396         args = keywords["VALIDSIG"]
1397         if len(args) < 1:
1398             rejects.append("internal error while checking signature on %s." % (sig_filename))
1399         else:
1400             fingerprint = args[0]
1401     if not keywords.has_key("GOODSIG"):
1402         rejects.append("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1403     if not keywords.has_key("SIG_ID"):
1404         rejects.append("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1405
1406     # Finally ensure there's not something we don't recognise
1407     known_keywords = dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1408                           SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1409                           NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="",POLICY_URL="")
1410
1411     for keyword in keywords.keys():
1412         if not known_keywords.has_key(keyword):
1413             rejects.append("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1414
1415     if len(rejects) > 0:
1416         return (None, rejects)
1417     else:
1418         return (fingerprint, [])
1419
1420 ################################################################################
1421
1422 def gpg_get_key_addresses(fingerprint):
1423     """retreive email addresses from gpg key uids for a given fingerprint"""
1424     addresses = key_uid_email_cache.get(fingerprint)
1425     if addresses != None:
1426         return addresses
1427     addresses = set()
1428     cmd = "gpg --no-default-keyring %s --fingerprint %s" \
1429                 % (gpg_keyring_args(), fingerprint)
1430     (result, output) = commands.getstatusoutput(cmd)
1431     if result == 0:
1432         for l in output.split('\n'):
1433             m = re_gpg_uid.match(l)
1434             if m:
1435                 addresses.add(m.group(1))
1436     key_uid_email_cache[fingerprint] = addresses
1437     return addresses
1438
1439 ################################################################################
1440
1441 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
1442
1443 def wrap(paragraph, max_length, prefix=""):
1444     line = ""
1445     s = ""
1446     have_started = 0
1447     words = paragraph.split()
1448
1449     for word in words:
1450         word_size = len(word)
1451         if word_size > max_length:
1452             if have_started:
1453                 s += line + '\n' + prefix
1454             s += word + '\n' + prefix
1455         else:
1456             if have_started:
1457                 new_length = len(line) + word_size + 1
1458                 if new_length > max_length:
1459                     s += line + '\n' + prefix
1460                     line = word
1461                 else:
1462                     line += ' ' + word
1463             else:
1464                 line = word
1465         have_started = 1
1466
1467     if have_started:
1468         s += line
1469
1470     return s
1471
1472 ################################################################################
1473
1474 def clean_symlink (src, dest, root):
1475     """
1476     Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1477     Returns fixed 'src'
1478     """
1479     src = src.replace(root, '', 1)
1480     dest = dest.replace(root, '', 1)
1481     dest = os.path.dirname(dest)
1482     new_src = '../' * len(dest.split('/'))
1483     return new_src + src
1484
1485 ################################################################################
1486
1487 def temp_filename(directory=None, prefix="dak", suffix=""):
1488     """
1489     Return a secure and unique filename by pre-creating it.
1490     If 'directory' is non-null, it will be the directory the file is pre-created in.
1491     If 'prefix' is non-null, the filename will be prefixed with it, default is dak.
1492     If 'suffix' is non-null, the filename will end with it.
1493
1494     Returns a pair (fd, name).
1495     """
1496
1497     return tempfile.mkstemp(suffix, prefix, directory)
1498
1499 ################################################################################
1500
1501 def temp_dirname(parent=None, prefix="dak", suffix=""):
1502     """
1503     Return a secure and unique directory by pre-creating it.
1504     If 'parent' is non-null, it will be the directory the directory is pre-created in.
1505     If 'prefix' is non-null, the filename will be prefixed with it, default is dak.
1506     If 'suffix' is non-null, the filename will end with it.
1507
1508     Returns a pathname to the new directory
1509     """
1510
1511     return tempfile.mkdtemp(suffix, prefix, parent)
1512
1513 ################################################################################
1514
1515 def is_email_alias(email):
1516     """ checks if the user part of the email is listed in the alias file """
1517     global alias_cache
1518     if alias_cache == None:
1519         aliasfn = which_alias_file()
1520         alias_cache = set()
1521         if aliasfn:
1522             for l in open(aliasfn):
1523                 alias_cache.add(l.split(':')[0])
1524     uid = email.split('@')[0]
1525     return uid in alias_cache
1526
1527 ################################################################################
1528
1529 def get_changes_files(from_dir):
1530     """
1531     Takes a directory and lists all .changes files in it (as well as chdir'ing
1532     to the directory; this is due to broken behaviour on the part of p-u/p-a
1533     when you're not in the right place)
1534
1535     Returns a list of filenames
1536     """
1537     try:
1538         # Much of the rest of p-u/p-a depends on being in the right place
1539         os.chdir(from_dir)
1540         changes_files = [x for x in os.listdir(from_dir) if x.endswith('.changes')]
1541     except OSError as e:
1542         fubar("Failed to read list from directory %s (%s)" % (from_dir, e))
1543
1544     return changes_files
1545
1546 ################################################################################
1547
1548 apt_pkg.init()
1549
1550 Cnf = apt_pkg.newConfiguration()
1551 if not os.getenv("DAK_TEST"):
1552     apt_pkg.ReadConfigFileISC(Cnf,default_config)
1553
1554 if which_conf_file() != default_config:
1555     apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1556
1557 ################################################################################
1558
1559 def parse_wnpp_bug_file(file = "/srv/ftp-master.debian.org/scripts/masterfiles/wnpp_rm"):
1560     """
1561     Parses the wnpp bug list available at http://qa.debian.org/data/bts/wnpp_rm
1562     Well, actually it parsed a local copy, but let's document the source
1563     somewhere ;)
1564
1565     returns a dict associating source package name with a list of open wnpp
1566     bugs (Yes, there might be more than one)
1567     """
1568
1569     line = []
1570     try:
1571         f = open(file)
1572         lines = f.readlines()
1573     except IOError as e:
1574         print "Warning:  Couldn't open %s; don't know about WNPP bugs, so won't close any." % file
1575         lines = []
1576     wnpp = {}
1577
1578     for line in lines:
1579         splited_line = line.split(": ", 1)
1580         if len(splited_line) > 1:
1581             wnpp[splited_line[0]] = splited_line[1].split("|")
1582
1583     for source in wnpp.keys():
1584         bugs = []
1585         for wnpp_bug in wnpp[source]:
1586             bug_no = re.search("(\d)+", wnpp_bug).group()
1587             if bug_no:
1588                 bugs.append(bug_no)
1589         wnpp[source] = bugs
1590     return wnpp
1591
1592 ################################################################################
1593
1594 def get_packages_from_ftp(root, suite, component, architecture):
1595     """
1596     Returns an object containing apt_pkg-parseable data collected by
1597     aggregating Packages.gz files gathered for each architecture.
1598
1599     @type root: string
1600     @param root: path to ftp archive root directory
1601
1602     @type suite: string
1603     @param suite: suite to extract files from
1604
1605     @type component: string
1606     @param component: component to extract files from
1607
1608     @type architecture: string
1609     @param architecture: architecture to extract files from
1610
1611     @rtype: TagFile
1612     @return: apt_pkg class containing package data
1613
1614     """
1615     filename = "%s/dists/%s/%s/binary-%s/Packages.gz" % (root, suite, component, architecture)
1616     (fd, temp_file) = temp_filename()
1617     (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_file))
1618     if (result != 0):
1619         fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1620     filename = "%s/dists/%s/%s/debian-installer/binary-%s/Packages.gz" % (root, suite, component, architecture)
1621     if os.path.exists(filename):
1622         (result, output) = commands.getstatusoutput("gunzip -c %s >> %s" % (filename, temp_file))
1623         if (result != 0):
1624             fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1625     packages = open_file(temp_file)
1626     Packages = apt_pkg.ParseTagFile(packages)
1627     os.unlink(temp_file)
1628     return Packages