]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
fix reading $HOME/etc/dak/dak.conf
[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" % (f, hashname, where))
530     return rejmsg
531
532 ################################################################################
533
534 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
535
536 def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
537     files = {}
538
539     # Make sure we have a Files: field to parse...
540     if not changes.has_key(field):
541         raise NoFilesFieldError
542
543     # Validate .changes Format: field
544     if not is_a_dsc:
545         validate_changes_format(parse_format(changes['format']), field)
546
547     includes_section = (not is_a_dsc) and field == "files"
548
549     # Parse each entry/line:
550     for i in changes[field].split('\n'):
551         if not i:
552             break
553         s = i.split()
554         section = priority = ""
555         try:
556             if includes_section:
557                 (md5, size, section, priority, name) = s
558             else:
559                 (md5, size, name) = s
560         except ValueError:
561             raise ParseChangesError(i)
562
563         if section == "":
564             section = "-"
565         if priority == "":
566             priority = "-"
567
568         (section, component) = extract_component_from_section(section)
569
570         files[name] = dict(size=size, section=section,
571                            priority=priority, component=component)
572         files[name][hashname] = md5
573
574     return files
575
576 ################################################################################
577
578 # see http://bugs.debian.org/619131
579 def build_package_list(dsc, session = None):
580     if not dsc.has_key("package-list"):
581         return {}
582
583     packages = {}
584
585     for line in dsc["package-list"].split("\n"):
586         if not line:
587             break
588
589         fields = line.split()
590         name = fields[0]
591         package_type = fields[1]
592         (section, component) = extract_component_from_section(fields[2])
593         priority = fields[3]
594
595         # Validate type if we have a session
596         if session and get_override_type(package_type, session) is None:
597             # Maybe just warn and ignore? exit(1) might be a bit hard...
598             utils.fubar("invalid type (%s) in Package-List." % (package_type))
599
600         if name not in packages or packages[name]["type"] == "dsc":
601             packages[name] = dict(priority=priority, section=section, type=package_type, component=component, files=[])
602
603     return packages
604
605 ################################################################################
606
607 def send_mail (message, filename=""):
608     """sendmail wrapper, takes _either_ a message string or a file as arguments"""
609
610     # Check whether we're supposed to be sending mail
611     if Cnf.has_key("Dinstall::Options::No-Mail") and Cnf["Dinstall::Options::No-Mail"]:
612         return
613
614     # If we've been passed a string dump it into a temporary file
615     if message:
616         (fd, filename) = tempfile.mkstemp()
617         os.write (fd, message)
618         os.close (fd)
619
620     if Cnf.has_key("Dinstall::MailWhiteList") and \
621            Cnf["Dinstall::MailWhiteList"] != "":
622         message_in = open_file(filename)
623         message_raw = modemail.message_from_file(message_in)
624         message_in.close();
625
626         whitelist = [];
627         whitelist_in = open_file(Cnf["Dinstall::MailWhiteList"])
628         try:
629             for line in whitelist_in:
630                 if not re_whitespace_comment.match(line):
631                     if re_re_mark.match(line):
632                         whitelist.append(re.compile(re_re_mark.sub("", line.strip(), 1)))
633                     else:
634                         whitelist.append(re.compile(re.escape(line.strip())))
635         finally:
636             whitelist_in.close()
637
638         # Fields to check.
639         fields = ["To", "Bcc", "Cc"]
640         for field in fields:
641             # Check each field
642             value = message_raw.get(field, None)
643             if value != None:
644                 match = [];
645                 for item in value.split(","):
646                     (rfc822_maint, rfc2047_maint, name, email) = fix_maintainer(item.strip())
647                     mail_whitelisted = 0
648                     for wr in whitelist:
649                         if wr.match(email):
650                             mail_whitelisted = 1
651                             break
652                     if not mail_whitelisted:
653                         print "Skipping %s since it's not in %s" % (item, Cnf["Dinstall::MailWhiteList"])
654                         continue
655                     match.append(item)
656
657                 # Doesn't have any mail in whitelist so remove the header
658                 if len(match) == 0:
659                     del message_raw[field]
660                 else:
661                     message_raw.replace_header(field, ', '.join(match))
662
663         # Change message fields in order if we don't have a To header
664         if not message_raw.has_key("To"):
665             fields.reverse()
666             for field in fields:
667                 if message_raw.has_key(field):
668                     message_raw[fields[-1]] = message_raw[field]
669                     del message_raw[field]
670                     break
671             else:
672                 # Clean up any temporary files
673                 # and return, as we removed all recipients.
674                 if message:
675                     os.unlink (filename);
676                 return;
677
678         fd = os.open(filename, os.O_RDWR|os.O_EXCL, 0o700);
679         os.write (fd, message_raw.as_string(True));
680         os.close (fd);
681
682     # Invoke sendmail
683     (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
684     if (result != 0):
685         raise SendmailFailedError(output)
686
687     # Clean up any temporary files
688     if message:
689         os.unlink (filename)
690
691 ################################################################################
692
693 def poolify (source, component):
694     if component:
695         component += '/'
696     if source[:3] == "lib":
697         return component + source[:4] + '/' + source + '/'
698     else:
699         return component + source[:1] + '/' + source + '/'
700
701 ################################################################################
702
703 def move (src, dest, overwrite = 0, perms = 0o664):
704     if os.path.exists(dest) and os.path.isdir(dest):
705         dest_dir = dest
706     else:
707         dest_dir = os.path.dirname(dest)
708     if not os.path.exists(dest_dir):
709         umask = os.umask(00000)
710         os.makedirs(dest_dir, 0o2775)
711         os.umask(umask)
712     #print "Moving %s to %s..." % (src, dest)
713     if os.path.exists(dest) and os.path.isdir(dest):
714         dest += '/' + os.path.basename(src)
715     # Don't overwrite unless forced to
716     if os.path.exists(dest):
717         if not overwrite:
718             fubar("Can't move %s to %s - file already exists." % (src, dest))
719         else:
720             if not os.access(dest, os.W_OK):
721                 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
722     shutil.copy2(src, dest)
723     os.chmod(dest, perms)
724     os.unlink(src)
725
726 def copy (src, dest, overwrite = 0, perms = 0o664):
727     if os.path.exists(dest) and os.path.isdir(dest):
728         dest_dir = dest
729     else:
730         dest_dir = os.path.dirname(dest)
731     if not os.path.exists(dest_dir):
732         umask = os.umask(00000)
733         os.makedirs(dest_dir, 0o2775)
734         os.umask(umask)
735     #print "Copying %s to %s..." % (src, dest)
736     if os.path.exists(dest) and os.path.isdir(dest):
737         dest += '/' + os.path.basename(src)
738     # Don't overwrite unless forced to
739     if os.path.exists(dest):
740         if not overwrite:
741             raise FileExistsError
742         else:
743             if not os.access(dest, os.W_OK):
744                 raise CantOverwriteError
745     shutil.copy2(src, dest)
746     os.chmod(dest, perms)
747
748 ################################################################################
749
750 def where_am_i ():
751     res = socket.getfqdn()
752     database_hostname = Cnf.get("Config::" + res + "::DatabaseHostname")
753     if database_hostname:
754         return database_hostname
755     else:
756         return res
757
758 def which_conf_file ():
759     if os.getenv('DAK_CONFIG'):
760         return os.getenv('DAK_CONFIG')
761
762     res = socket.getfqdn()
763     # In case we allow local config files per user, try if one exists
764     if Cnf.FindB("Config::" + res + "::AllowLocalConfig"):
765         homedir = os.getenv("HOME")
766         confpath = os.path.join(homedir, "/etc/dak.conf")
767         if os.path.exists(confpath):
768             apt_pkg.ReadConfigFileISC(Cnf,confpath)
769
770     # We are still in here, so there is no local config file or we do
771     # not allow local files. Do the normal stuff.
772     if Cnf.get("Config::" + res + "::DakConfig"):
773         return Cnf["Config::" + res + "::DakConfig"]
774
775     return default_config
776
777 def which_apt_conf_file ():
778     res = socket.getfqdn()
779     # In case we allow local config files per user, try if one exists
780     if Cnf.FindB("Config::" + res + "::AllowLocalConfig"):
781         homedir = os.getenv("HOME")
782         confpath = os.path.join(homedir, "/etc/dak.conf")
783         if os.path.exists(confpath):
784             apt_pkg.ReadConfigFileISC(Cnf,default_config)
785
786     if Cnf.get("Config::" + res + "::AptConfig"):
787         return Cnf["Config::" + res + "::AptConfig"]
788     else:
789         return default_apt_config
790
791 def which_alias_file():
792     hostname = socket.getfqdn()
793     aliasfn = '/var/lib/misc/'+hostname+'/forward-alias'
794     if os.path.exists(aliasfn):
795         return aliasfn
796     else:
797         return None
798
799 ################################################################################
800
801 def TemplateSubst(subst_map, filename):
802     """ Perform a substition of template """
803     templatefile = open_file(filename)
804     template = templatefile.read()
805     for k, v in subst_map.iteritems():
806         template = template.replace(k, str(v))
807     templatefile.close()
808     return template
809
810 ################################################################################
811
812 def fubar(msg, exit_code=1):
813     sys.stderr.write("E: %s\n" % (msg))
814     sys.exit(exit_code)
815
816 def warn(msg):
817     sys.stderr.write("W: %s\n" % (msg))
818
819 ################################################################################
820
821 # Returns the user name with a laughable attempt at rfc822 conformancy
822 # (read: removing stray periods).
823 def whoami ():
824     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
825
826 def getusername ():
827     return pwd.getpwuid(os.getuid())[0]
828
829 ################################################################################
830
831 def size_type (c):
832     t  = " B"
833     if c > 10240:
834         c = c / 1024
835         t = " KB"
836     if c > 10240:
837         c = c / 1024
838         t = " MB"
839     return ("%d%s" % (c, t))
840
841 ################################################################################
842
843 def cc_fix_changes (changes):
844     o = changes.get("architecture", "")
845     if o:
846         del changes["architecture"]
847     changes["architecture"] = {}
848     for j in o.split():
849         changes["architecture"][j] = 1
850
851 def changes_compare (a, b):
852     """ Sort by source name, source version, 'have source', and then by filename """
853     try:
854         a_changes = parse_changes(a)
855     except:
856         return -1
857
858     try:
859         b_changes = parse_changes(b)
860     except:
861         return 1
862
863     cc_fix_changes (a_changes)
864     cc_fix_changes (b_changes)
865
866     # Sort by source name
867     a_source = a_changes.get("source")
868     b_source = b_changes.get("source")
869     q = cmp (a_source, b_source)
870     if q:
871         return q
872
873     # Sort by source version
874     a_version = a_changes.get("version", "0")
875     b_version = b_changes.get("version", "0")
876     q = apt_pkg.VersionCompare(a_version, b_version)
877     if q:
878         return q
879
880     # Sort by 'have source'
881     a_has_source = a_changes["architecture"].get("source")
882     b_has_source = b_changes["architecture"].get("source")
883     if a_has_source and not b_has_source:
884         return -1
885     elif b_has_source and not a_has_source:
886         return 1
887
888     # Fall back to sort by filename
889     return cmp(a, b)
890
891 ################################################################################
892
893 def find_next_free (dest, too_many=100):
894     extra = 0
895     orig_dest = dest
896     while os.path.exists(dest) and extra < too_many:
897         dest = orig_dest + '.' + repr(extra)
898         extra += 1
899     if extra >= too_many:
900         raise NoFreeFilenameError
901     return dest
902
903 ################################################################################
904
905 def result_join (original, sep = '\t'):
906     resultlist = []
907     for i in xrange(len(original)):
908         if original[i] == None:
909             resultlist.append("")
910         else:
911             resultlist.append(original[i])
912     return sep.join(resultlist)
913
914 ################################################################################
915
916 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
917     out = ""
918     for line in str.split('\n'):
919         line = line.strip()
920         if line or include_blank_lines:
921             out += "%s%s\n" % (prefix, line)
922     # Strip trailing new line
923     if out:
924         out = out[:-1]
925     return out
926
927 ################################################################################
928
929 def validate_changes_file_arg(filename, require_changes=1):
930     """
931     'filename' is either a .changes or .dak file.  If 'filename' is a
932     .dak file, it's changed to be the corresponding .changes file.  The
933     function then checks if the .changes file a) exists and b) is
934     readable and returns the .changes filename if so.  If there's a
935     problem, the next action depends on the option 'require_changes'
936     argument:
937
938       - If 'require_changes' == -1, errors are ignored and the .changes
939         filename is returned.
940       - If 'require_changes' == 0, a warning is given and 'None' is returned.
941       - If 'require_changes' == 1, a fatal error is raised.
942
943     """
944     error = None
945
946     orig_filename = filename
947     if filename.endswith(".dak"):
948         filename = filename[:-4]+".changes"
949
950     if not filename.endswith(".changes"):
951         error = "invalid file type; not a changes file"
952     else:
953         if not os.access(filename,os.R_OK):
954             if os.path.exists(filename):
955                 error = "permission denied"
956             else:
957                 error = "file not found"
958
959     if error:
960         if require_changes == 1:
961             fubar("%s: %s." % (orig_filename, error))
962         elif require_changes == 0:
963             warn("Skipping %s - %s" % (orig_filename, error))
964             return None
965         else: # We only care about the .dak file
966             return filename
967     else:
968         return filename
969
970 ################################################################################
971
972 def real_arch(arch):
973     return (arch != "source" and arch != "all")
974
975 ################################################################################
976
977 def join_with_commas_and(list):
978     if len(list) == 0: return "nothing"
979     if len(list) == 1: return list[0]
980     return ", ".join(list[:-1]) + " and " + list[-1]
981
982 ################################################################################
983
984 def pp_deps (deps):
985     pp_deps = []
986     for atom in deps:
987         (pkg, version, constraint) = atom
988         if constraint:
989             pp_dep = "%s (%s %s)" % (pkg, constraint, version)
990         else:
991             pp_dep = pkg
992         pp_deps.append(pp_dep)
993     return " |".join(pp_deps)
994
995 ################################################################################
996
997 def get_conf():
998     return Cnf
999
1000 ################################################################################
1001
1002 def parse_args(Options):
1003     """ Handle -a, -c and -s arguments; returns them as SQL constraints """
1004     # XXX: This should go away and everything which calls it be converted
1005     #      to use SQLA properly.  For now, we'll just fix it not to use
1006     #      the old Pg interface though
1007     session = DBConn().session()
1008     # Process suite
1009     if Options["Suite"]:
1010         suite_ids_list = []
1011         for suitename in split_args(Options["Suite"]):
1012             suite = get_suite(suitename, session=session)
1013             if not suite or suite.suite_id is None:
1014                 warn("suite '%s' not recognised." % (suite and suite.suite_name or suitename))
1015             else:
1016                 suite_ids_list.append(suite.suite_id)
1017         if suite_ids_list:
1018             con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
1019         else:
1020             fubar("No valid suite given.")
1021     else:
1022         con_suites = ""
1023
1024     # Process component
1025     if Options["Component"]:
1026         component_ids_list = []
1027         for componentname in split_args(Options["Component"]):
1028             component = get_component(componentname, session=session)
1029             if component is None:
1030                 warn("component '%s' not recognised." % (componentname))
1031             else:
1032                 component_ids_list.append(component.component_id)
1033         if component_ids_list:
1034             con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
1035         else:
1036             fubar("No valid component given.")
1037     else:
1038         con_components = ""
1039
1040     # Process architecture
1041     con_architectures = ""
1042     check_source = 0
1043     if Options["Architecture"]:
1044         arch_ids_list = []
1045         for archname in split_args(Options["Architecture"]):
1046             if archname == "source":
1047                 check_source = 1
1048             else:
1049                 arch = get_architecture(archname, session=session)
1050                 if arch is None:
1051                     warn("architecture '%s' not recognised." % (archname))
1052                 else:
1053                     arch_ids_list.append(arch.arch_id)
1054         if arch_ids_list:
1055             con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
1056         else:
1057             if not check_source:
1058                 fubar("No valid architecture given.")
1059     else:
1060         check_source = 1
1061
1062     return (con_suites, con_architectures, con_components, check_source)
1063
1064 ################################################################################
1065
1066 # Inspired(tm) by Bryn Keller's print_exc_plus (See
1067 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52215)
1068
1069 def print_exc():
1070     tb = sys.exc_info()[2]
1071     while tb.tb_next:
1072         tb = tb.tb_next
1073     stack = []
1074     frame = tb.tb_frame
1075     while frame:
1076         stack.append(frame)
1077         frame = frame.f_back
1078     stack.reverse()
1079     traceback.print_exc()
1080     for frame in stack:
1081         print "\nFrame %s in %s at line %s" % (frame.f_code.co_name,
1082                                              frame.f_code.co_filename,
1083                                              frame.f_lineno)
1084         for key, value in frame.f_locals.items():
1085             print "\t%20s = " % key,
1086             try:
1087                 print value
1088             except:
1089                 print "<unable to print>"
1090
1091 ################################################################################
1092
1093 def try_with_debug(function):
1094     try:
1095         function()
1096     except SystemExit:
1097         raise
1098     except:
1099         print_exc()
1100
1101 ################################################################################
1102
1103 def arch_compare_sw (a, b):
1104     """
1105     Function for use in sorting lists of architectures.
1106
1107     Sorts normally except that 'source' dominates all others.
1108     """
1109
1110     if a == "source" and b == "source":
1111         return 0
1112     elif a == "source":
1113         return -1
1114     elif b == "source":
1115         return 1
1116
1117     return cmp (a, b)
1118
1119 ################################################################################
1120
1121 def split_args (s, dwim=1):
1122     """
1123     Split command line arguments which can be separated by either commas
1124     or whitespace.  If dwim is set, it will complain about string ending
1125     in comma since this usually means someone did 'dak ls -a i386, m68k
1126     foo' or something and the inevitable confusion resulting from 'm68k'
1127     being treated as an argument is undesirable.
1128     """
1129
1130     if s.find(",") == -1:
1131         return s.split()
1132     else:
1133         if s[-1:] == "," and dwim:
1134             fubar("split_args: found trailing comma, spurious space maybe?")
1135         return s.split(",")
1136
1137 ################################################################################
1138
1139 def gpgv_get_status_output(cmd, status_read, status_write):
1140     """
1141     Our very own version of commands.getouputstatus(), hacked to support
1142     gpgv's status fd.
1143     """
1144
1145     cmd = ['/bin/sh', '-c', cmd]
1146     p2cread, p2cwrite = os.pipe()
1147     c2pread, c2pwrite = os.pipe()
1148     errout, errin = os.pipe()
1149     pid = os.fork()
1150     if pid == 0:
1151         # Child
1152         os.close(0)
1153         os.close(1)
1154         os.dup(p2cread)
1155         os.dup(c2pwrite)
1156         os.close(2)
1157         os.dup(errin)
1158         for i in range(3, 256):
1159             if i != status_write:
1160                 try:
1161                     os.close(i)
1162                 except:
1163                     pass
1164         try:
1165             os.execvp(cmd[0], cmd)
1166         finally:
1167             os._exit(1)
1168
1169     # Parent
1170     os.close(p2cread)
1171     os.dup2(c2pread, c2pwrite)
1172     os.dup2(errout, errin)
1173
1174     output = status = ""
1175     while 1:
1176         i, o, e = select.select([c2pwrite, errin, status_read], [], [])
1177         more_data = []
1178         for fd in i:
1179             r = os.read(fd, 8196)
1180             if len(r) > 0:
1181                 more_data.append(fd)
1182                 if fd == c2pwrite or fd == errin:
1183                     output += r
1184                 elif fd == status_read:
1185                     status += r
1186                 else:
1187                     fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
1188         if not more_data:
1189             pid, exit_status = os.waitpid(pid, 0)
1190             try:
1191                 os.close(status_write)
1192                 os.close(status_read)
1193                 os.close(c2pread)
1194                 os.close(c2pwrite)
1195                 os.close(p2cwrite)
1196                 os.close(errin)
1197                 os.close(errout)
1198             except:
1199                 pass
1200             break
1201
1202     return output, status, exit_status
1203
1204 ################################################################################
1205
1206 def process_gpgv_output(status):
1207     # Process the status-fd output
1208     keywords = {}
1209     internal_error = ""
1210     for line in status.split('\n'):
1211         line = line.strip()
1212         if line == "":
1213             continue
1214         split = line.split()
1215         if len(split) < 2:
1216             internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
1217             continue
1218         (gnupg, keyword) = split[:2]
1219         if gnupg != "[GNUPG:]":
1220             internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
1221             continue
1222         args = split[2:]
1223         if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
1224             internal_error += "found duplicate status token ('%s').\n" % (keyword)
1225             continue
1226         else:
1227             keywords[keyword] = args
1228
1229     return (keywords, internal_error)
1230
1231 ################################################################################
1232
1233 def retrieve_key (filename, keyserver=None, keyring=None):
1234     """
1235     Retrieve the key that signed 'filename' from 'keyserver' and
1236     add it to 'keyring'.  Returns nothing on success, or an error message
1237     on error.
1238     """
1239
1240     # Defaults for keyserver and keyring
1241     if not keyserver:
1242         keyserver = Cnf["Dinstall::KeyServer"]
1243     if not keyring:
1244         keyring = get_primary_keyring_path()
1245
1246     # Ensure the filename contains no shell meta-characters or other badness
1247     if not re_taint_free.match(filename):
1248         return "%s: tainted filename" % (filename)
1249
1250     # Invoke gpgv on the file
1251     status_read, status_write = os.pipe()
1252     cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
1253     (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
1254
1255     # Process the status-fd output
1256     (keywords, internal_error) = process_gpgv_output(status)
1257     if internal_error:
1258         return internal_error
1259
1260     if not keywords.has_key("NO_PUBKEY"):
1261         return "didn't find expected NO_PUBKEY in gpgv status-fd output"
1262
1263     fingerprint = keywords["NO_PUBKEY"][0]
1264     # XXX - gpg sucks.  You can't use --secret-keyring=/dev/null as
1265     # it'll try to create a lockfile in /dev.  A better solution might
1266     # be a tempfile or something.
1267     cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
1268           % (Cnf["Dinstall::SigningKeyring"])
1269     cmd += " --keyring %s --keyserver %s --recv-key %s" \
1270            % (keyring, keyserver, fingerprint)
1271     (result, output) = commands.getstatusoutput(cmd)
1272     if (result != 0):
1273         return "'%s' failed with exit code %s" % (cmd, result)
1274
1275     return ""
1276
1277 ################################################################################
1278
1279 def gpg_keyring_args(keyrings=None):
1280     if not keyrings:
1281         keyrings = get_active_keyring_paths()
1282
1283     return " ".join(["--keyring %s" % x for x in keyrings])
1284
1285 ################################################################################
1286 @session_wrapper
1287 def check_signature (sig_filename, data_filename="", keyrings=None, autofetch=None, session=None):
1288     """
1289     Check the signature of a file and return the fingerprint if the
1290     signature is valid or 'None' if it's not.  The first argument is the
1291     filename whose signature should be checked.  The second argument is a
1292     reject function and is called when an error is found.  The reject()
1293     function must allow for two arguments: the first is the error message,
1294     the second is an optional prefix string.  It's possible for reject()
1295     to be called more than once during an invocation of check_signature().
1296     The third argument is optional and is the name of the files the
1297     detached signature applies to.  The fourth argument is optional and is
1298     a *list* of keyrings to use.  'autofetch' can either be None, True or
1299     False.  If None, the default behaviour specified in the config will be
1300     used.
1301     """
1302
1303     rejects = []
1304
1305     # Ensure the filename contains no shell meta-characters or other badness
1306     if not re_taint_free.match(sig_filename):
1307         rejects.append("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
1308         return (None, rejects)
1309
1310     if data_filename and not re_taint_free.match(data_filename):
1311         rejects.append("!!WARNING!! tainted data filename: '%s'." % (data_filename))
1312         return (None, rejects)
1313
1314     if not keyrings:
1315         keyrings = [ x.keyring_name for x in session.query(Keyring).filter(Keyring.active == True).all() ]
1316
1317     # Autofetch the signing key if that's enabled
1318     if autofetch == None:
1319         autofetch = Cnf.get("Dinstall::KeyAutoFetch")
1320     if autofetch:
1321         error_msg = retrieve_key(sig_filename)
1322         if error_msg:
1323             rejects.append(error_msg)
1324             return (None, rejects)
1325
1326     # Build the command line
1327     status_read, status_write = os.pipe()
1328     cmd = "gpgv --status-fd %s %s %s %s" % (
1329         status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
1330
1331     # Invoke gpgv on the file
1332     (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1333
1334     # Process the status-fd output
1335     (keywords, internal_error) = process_gpgv_output(status)
1336
1337     # If we failed to parse the status-fd output, let's just whine and bail now
1338     if internal_error:
1339         rejects.append("internal error while performing signature check on %s." % (sig_filename))
1340         rejects.append(internal_error, "")
1341         rejects.append("Please report the above errors to the Archive maintainers by replying to this mail.", "")
1342         return (None, rejects)
1343
1344     # Now check for obviously bad things in the processed output
1345     if keywords.has_key("KEYREVOKED"):
1346         rejects.append("The key used to sign %s has been revoked." % (sig_filename))
1347     if keywords.has_key("BADSIG"):
1348         rejects.append("bad signature on %s." % (sig_filename))
1349     if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1350         rejects.append("failed to check signature on %s." % (sig_filename))
1351     if keywords.has_key("NO_PUBKEY"):
1352         args = keywords["NO_PUBKEY"]
1353         if len(args) >= 1:
1354             key = args[0]
1355         rejects.append("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1356     if keywords.has_key("BADARMOR"):
1357         rejects.append("ASCII armour of signature was corrupt in %s." % (sig_filename))
1358     if keywords.has_key("NODATA"):
1359         rejects.append("no signature found in %s." % (sig_filename))
1360     if keywords.has_key("EXPKEYSIG"):
1361         args = keywords["EXPKEYSIG"]
1362         if len(args) >= 1:
1363             key = args[0]
1364         rejects.append("Signature made by expired key 0x%s" % (key))
1365     if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1366         args = keywords["KEYEXPIRED"]
1367         expiredate=""
1368         if len(args) >= 1:
1369             timestamp = args[0]
1370             if timestamp.count("T") == 0:
1371                 try:
1372                     expiredate = time.strftime("%Y-%m-%d", time.gmtime(float(timestamp)))
1373                 except ValueError:
1374                     expiredate = "unknown (%s)" % (timestamp)
1375             else:
1376                 expiredate = timestamp
1377         rejects.append("The key used to sign %s has expired on %s" % (sig_filename, expiredate))
1378
1379     if len(rejects) > 0:
1380         return (None, rejects)
1381
1382     # Next check gpgv exited with a zero return code
1383     if exit_status:
1384         rejects.append("gpgv failed while checking %s." % (sig_filename))
1385         if status.strip():
1386             rejects.append(prefix_multi_line_string(status, " [GPG status-fd output:] "))
1387         else:
1388             rejects.append(prefix_multi_line_string(output, " [GPG output:] "))
1389         return (None, rejects)
1390
1391     # Sanity check the good stuff we expect
1392     if not keywords.has_key("VALIDSIG"):
1393         rejects.append("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1394     else:
1395         args = keywords["VALIDSIG"]
1396         if len(args) < 1:
1397             rejects.append("internal error while checking signature on %s." % (sig_filename))
1398         else:
1399             fingerprint = args[0]
1400     if not keywords.has_key("GOODSIG"):
1401         rejects.append("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1402     if not keywords.has_key("SIG_ID"):
1403         rejects.append("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1404
1405     # Finally ensure there's not something we don't recognise
1406     known_keywords = dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1407                           SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1408                           NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="",POLICY_URL="")
1409
1410     for keyword in keywords.keys():
1411         if not known_keywords.has_key(keyword):
1412             rejects.append("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1413
1414     if len(rejects) > 0:
1415         return (None, rejects)
1416     else:
1417         return (fingerprint, [])
1418
1419 ################################################################################
1420
1421 def gpg_get_key_addresses(fingerprint):
1422     """retreive email addresses from gpg key uids for a given fingerprint"""
1423     addresses = key_uid_email_cache.get(fingerprint)
1424     if addresses != None:
1425         return addresses
1426     addresses = set()
1427     cmd = "gpg --no-default-keyring %s --fingerprint %s" \
1428                 % (gpg_keyring_args(), fingerprint)
1429     (result, output) = commands.getstatusoutput(cmd)
1430     if result == 0:
1431         for l in output.split('\n'):
1432             m = re_gpg_uid.match(l)
1433             if m:
1434                 addresses.add(m.group(1))
1435     key_uid_email_cache[fingerprint] = addresses
1436     return addresses
1437
1438 ################################################################################
1439
1440 # Inspired(tm) by http://www.zopelabs.com/cookbook/1022242603
1441
1442 def wrap(paragraph, max_length, prefix=""):
1443     line = ""
1444     s = ""
1445     have_started = 0
1446     words = paragraph.split()
1447
1448     for word in words:
1449         word_size = len(word)
1450         if word_size > max_length:
1451             if have_started:
1452                 s += line + '\n' + prefix
1453             s += word + '\n' + prefix
1454         else:
1455             if have_started:
1456                 new_length = len(line) + word_size + 1
1457                 if new_length > max_length:
1458                     s += line + '\n' + prefix
1459                     line = word
1460                 else:
1461                     line += ' ' + word
1462             else:
1463                 line = word
1464         have_started = 1
1465
1466     if have_started:
1467         s += line
1468
1469     return s
1470
1471 ################################################################################
1472
1473 def clean_symlink (src, dest, root):
1474     """
1475     Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1476     Returns fixed 'src'
1477     """
1478     src = src.replace(root, '', 1)
1479     dest = dest.replace(root, '', 1)
1480     dest = os.path.dirname(dest)
1481     new_src = '../' * len(dest.split('/'))
1482     return new_src + src
1483
1484 ################################################################################
1485
1486 def temp_filename(directory=None, prefix="dak", suffix=""):
1487     """
1488     Return a secure and unique filename by pre-creating it.
1489     If 'directory' is non-null, it will be the directory the file is pre-created in.
1490     If 'prefix' is non-null, the filename will be prefixed with it, default is dak.
1491     If 'suffix' is non-null, the filename will end with it.
1492
1493     Returns a pair (fd, name).
1494     """
1495
1496     return tempfile.mkstemp(suffix, prefix, directory)
1497
1498 ################################################################################
1499
1500 def temp_dirname(parent=None, prefix="dak", suffix=""):
1501     """
1502     Return a secure and unique directory by pre-creating it.
1503     If 'parent' is non-null, it will be the directory the directory is pre-created in.
1504     If 'prefix' is non-null, the filename will be prefixed with it, default is dak.
1505     If 'suffix' is non-null, the filename will end with it.
1506
1507     Returns a pathname to the new directory
1508     """
1509
1510     return tempfile.mkdtemp(suffix, prefix, parent)
1511
1512 ################################################################################
1513
1514 def is_email_alias(email):
1515     """ checks if the user part of the email is listed in the alias file """
1516     global alias_cache
1517     if alias_cache == None:
1518         aliasfn = which_alias_file()
1519         alias_cache = set()
1520         if aliasfn:
1521             for l in open(aliasfn):
1522                 alias_cache.add(l.split(':')[0])
1523     uid = email.split('@')[0]
1524     return uid in alias_cache
1525
1526 ################################################################################
1527
1528 def get_changes_files(from_dir):
1529     """
1530     Takes a directory and lists all .changes files in it (as well as chdir'ing
1531     to the directory; this is due to broken behaviour on the part of p-u/p-a
1532     when you're not in the right place)
1533
1534     Returns a list of filenames
1535     """
1536     try:
1537         # Much of the rest of p-u/p-a depends on being in the right place
1538         os.chdir(from_dir)
1539         changes_files = [x for x in os.listdir(from_dir) if x.endswith('.changes')]
1540     except OSError as e:
1541         fubar("Failed to read list from directory %s (%s)" % (from_dir, e))
1542
1543     return changes_files
1544
1545 ################################################################################
1546
1547 apt_pkg.init()
1548
1549 Cnf = apt_pkg.newConfiguration()
1550 if not os.getenv("DAK_TEST"):
1551     apt_pkg.ReadConfigFileISC(Cnf,default_config)
1552
1553 if which_conf_file() != default_config:
1554     apt_pkg.ReadConfigFileISC(Cnf,which_conf_file())
1555
1556 ################################################################################
1557
1558 def parse_wnpp_bug_file(file = "/srv/ftp-master.debian.org/scripts/masterfiles/wnpp_rm"):
1559     """
1560     Parses the wnpp bug list available at http://qa.debian.org/data/bts/wnpp_rm
1561     Well, actually it parsed a local copy, but let's document the source
1562     somewhere ;)
1563
1564     returns a dict associating source package name with a list of open wnpp
1565     bugs (Yes, there might be more than one)
1566     """
1567
1568     line = []
1569     try:
1570         f = open(file)
1571         lines = f.readlines()
1572     except IOError as e:
1573         print "Warning:  Couldn't open %s; don't know about WNPP bugs, so won't close any." % file
1574         lines = []
1575     wnpp = {}
1576
1577     for line in lines:
1578         splited_line = line.split(": ", 1)
1579         if len(splited_line) > 1:
1580             wnpp[splited_line[0]] = splited_line[1].split("|")
1581
1582     for source in wnpp.keys():
1583         bugs = []
1584         for wnpp_bug in wnpp[source]:
1585             bug_no = re.search("(\d)+", wnpp_bug).group()
1586             if bug_no:
1587                 bugs.append(bug_no)
1588         wnpp[source] = bugs
1589     return wnpp
1590
1591 ################################################################################
1592
1593 def get_packages_from_ftp(root, suite, component, architecture):
1594     """
1595     Returns an object containing apt_pkg-parseable data collected by
1596     aggregating Packages.gz files gathered for each architecture.
1597
1598     @type root: string
1599     @param root: path to ftp archive root directory
1600
1601     @type suite: string
1602     @param suite: suite to extract files from
1603
1604     @type component: string
1605     @param component: component to extract files from
1606
1607     @type architecture: string
1608     @param architecture: architecture to extract files from
1609
1610     @rtype: TagFile
1611     @return: apt_pkg class containing package data
1612
1613     """
1614     filename = "%s/dists/%s/%s/binary-%s/Packages.gz" % (root, suite, component, architecture)
1615     (fd, temp_file) = temp_filename()
1616     (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_file))
1617     if (result != 0):
1618         fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1619     filename = "%s/dists/%s/%s/debian-installer/binary-%s/Packages.gz" % (root, suite, component, architecture)
1620     if os.path.exists(filename):
1621         (result, output) = commands.getstatusoutput("gunzip -c %s >> %s" % (filename, temp_file))
1622         if (result != 0):
1623             fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1624     packages = open_file(temp_file)
1625     Packages = apt_pkg.ParseTagFile(packages)
1626     os.unlink(temp_file)
1627     return Packages