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