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