]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
daklib/utils.py: lookup gid for group.
[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         os.chown(tfname, -1, group)
1507     return tfname
1508
1509 ################################################################################
1510
1511 def is_email_alias(email):
1512     """ checks if the user part of the email is listed in the alias file """
1513     global alias_cache
1514     if alias_cache == None:
1515         aliasfn = which_alias_file()
1516         alias_cache = set()
1517         if aliasfn:
1518             for l in open(aliasfn):
1519                 alias_cache.add(l.split(':')[0])
1520     uid = email.split('@')[0]
1521     return uid in alias_cache
1522
1523 ################################################################################
1524
1525 def get_changes_files(from_dir):
1526     """
1527     Takes a directory and lists all .changes files in it (as well as chdir'ing
1528     to the directory; this is due to broken behaviour on the part of p-u/p-a
1529     when you're not in the right place)
1530
1531     Returns a list of filenames
1532     """
1533     try:
1534         # Much of the rest of p-u/p-a depends on being in the right place
1535         os.chdir(from_dir)
1536         changes_files = [x for x in os.listdir(from_dir) if x.endswith('.changes')]
1537     except OSError as e:
1538         fubar("Failed to read list from directory %s (%s)" % (from_dir, e))
1539
1540     return changes_files
1541
1542 ################################################################################
1543
1544 apt_pkg.init()
1545
1546 Cnf = apt_pkg.Configuration()
1547 if not os.getenv("DAK_TEST"):
1548     apt_pkg.read_config_file_isc(Cnf,default_config)
1549
1550 if which_conf_file() != default_config:
1551     apt_pkg.read_config_file_isc(Cnf,which_conf_file())
1552
1553 ################################################################################
1554
1555 def parse_wnpp_bug_file(file = "/srv/ftp-master.debian.org/scripts/masterfiles/wnpp_rm"):
1556     """
1557     Parses the wnpp bug list available at http://qa.debian.org/data/bts/wnpp_rm
1558     Well, actually it parsed a local copy, but let's document the source
1559     somewhere ;)
1560
1561     returns a dict associating source package name with a list of open wnpp
1562     bugs (Yes, there might be more than one)
1563     """
1564
1565     line = []
1566     try:
1567         f = open(file)
1568         lines = f.readlines()
1569     except IOError as e:
1570         print "Warning:  Couldn't open %s; don't know about WNPP bugs, so won't close any." % file
1571         lines = []
1572     wnpp = {}
1573
1574     for line in lines:
1575         splited_line = line.split(": ", 1)
1576         if len(splited_line) > 1:
1577             wnpp[splited_line[0]] = splited_line[1].split("|")
1578
1579     for source in wnpp.keys():
1580         bugs = []
1581         for wnpp_bug in wnpp[source]:
1582             bug_no = re.search("(\d)+", wnpp_bug).group()
1583             if bug_no:
1584                 bugs.append(bug_no)
1585         wnpp[source] = bugs
1586     return wnpp
1587
1588 ################################################################################
1589
1590 def get_packages_from_ftp(root, suite, component, architecture):
1591     """
1592     Returns an object containing apt_pkg-parseable data collected by
1593     aggregating Packages.gz files gathered for each architecture.
1594
1595     @type root: string
1596     @param root: path to ftp archive root directory
1597
1598     @type suite: string
1599     @param suite: suite to extract files from
1600
1601     @type component: string
1602     @param component: component to extract files from
1603
1604     @type architecture: string
1605     @param architecture: architecture to extract files from
1606
1607     @rtype: TagFile
1608     @return: apt_pkg class containing package data
1609     """
1610     filename = "%s/dists/%s/%s/binary-%s/Packages.gz" % (root, suite, component, architecture)
1611     (fd, temp_file) = temp_filename()
1612     (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_file))
1613     if (result != 0):
1614         fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1615     filename = "%s/dists/%s/%s/debian-installer/binary-%s/Packages.gz" % (root, suite, component, architecture)
1616     if os.path.exists(filename):
1617         (result, output) = commands.getstatusoutput("gunzip -c %s >> %s" % (filename, temp_file))
1618         if (result != 0):
1619             fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1620     packages = open_file(temp_file)
1621     Packages = apt_pkg.ParseTagFile(packages)
1622     os.unlink(temp_file)
1623     return Packages
1624
1625 ################################################################################
1626
1627 def deb_extract_control(fh):
1628     """extract DEBIAN/control from a binary package"""
1629     return apt_inst.DebFile(fh).control.extractdata("control")
1630
1631 ################################################################################
1632
1633 def mail_addresses_for_upload(maintainer, changed_by, fingerprint):
1634     """mail addresses to contact for an upload
1635
1636     @type  maintainer: str
1637     @param maintainer: Maintainer field of the .changes file
1638
1639     @type  changed_by: str
1640     @param changed_by: Changed-By field of the .changes file
1641
1642     @type  fingerprint: str
1643     @param fingerprint: fingerprint of the key used to sign the upload
1644
1645     @rtype:  list of str
1646     @return: list of RFC 2047-encoded mail addresses to contact regarding
1647              this upload
1648     """
1649     addresses = [maintainer]
1650     if changed_by != maintainer:
1651         addresses.append(changed_by)
1652
1653     fpr_addresses = gpg_get_key_addresses(fingerprint)
1654     if len(fpr_addresses) > 0 and fix_maintainer(changed_by)[3] not in fpr_addresses and fix_maintainer(maintainer)[3] not in fpr_addresses:
1655         addresses.append(fpr_addresses[0])
1656
1657     encoded_addresses = [ fix_maintainer(e)[1] for e in addresses ]
1658     return encoded_addresses
1659
1660 ################################################################################
1661
1662 def call_editor(text="", suffix=".txt"):
1663     """run editor and return the result as a string
1664
1665     @type  text: str
1666     @param text: initial text
1667
1668     @type  suffix: str
1669     @param suffix: extension for temporary file
1670
1671     @rtype:  str
1672     @return: string with the edited text
1673     """
1674     editor = os.environ.get('VISUAL', os.environ.get('EDITOR', 'vi'))
1675     tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
1676     try:
1677         print >>tmp, text,
1678         tmp.close()
1679         subprocess.check_call([editor, tmp.name])
1680         return open(tmp.name, 'r').read()
1681     finally:
1682         os.unlink(tmp.name)
1683
1684 ################################################################################
1685
1686 def check_reverse_depends(removals, suite, arches=None, session=None, cruft=False):
1687     dbsuite = get_suite(suite, session)
1688     overridesuite = dbsuite
1689     if dbsuite.overridesuite is not None:
1690         overridesuite = get_suite(dbsuite.overridesuite, session)
1691     dep_problem = 0
1692     p2c = {}
1693     all_broken = {}
1694     if arches:
1695         all_arches = set(arches)
1696     else:
1697         all_arches = set([x.arch_string for x in get_suite_architectures(suite)])
1698     all_arches -= set(["source", "all"])
1699     metakey_d = get_or_set_metadatakey("Depends", session)
1700     metakey_p = get_or_set_metadatakey("Provides", session)
1701     params = {
1702         'suite_id':     dbsuite.suite_id,
1703         'metakey_d_id': metakey_d.key_id,
1704         'metakey_p_id': metakey_p.key_id,
1705     }
1706     for architecture in all_arches | set(['all']):
1707         deps = {}
1708         sources = {}
1709         virtual_packages = {}
1710         params['arch_id'] = get_architecture(architecture, session).arch_id
1711
1712         statement = '''
1713             SELECT b.id, b.package, s.source, c.name as component,
1714                 (SELECT bmd.value FROM binaries_metadata bmd WHERE bmd.bin_id = b.id AND bmd.key_id = :metakey_d_id) AS depends,
1715                 (SELECT bmp.value FROM binaries_metadata bmp WHERE bmp.bin_id = b.id AND bmp.key_id = :metakey_p_id) AS provides
1716                 FROM binaries b
1717                 JOIN bin_associations ba ON b.id = ba.bin AND ba.suite = :suite_id
1718                 JOIN source s ON b.source = s.id
1719                 JOIN files_archive_map af ON b.file = af.file_id
1720                 JOIN component c ON af.component_id = c.id
1721                 WHERE b.architecture = :arch_id'''
1722         query = session.query('id', 'package', 'source', 'component', 'depends', 'provides'). \
1723             from_statement(statement).params(params)
1724         for binary_id, package, source, component, depends, provides in query:
1725             sources[package] = source
1726             p2c[package] = component
1727             if depends is not None:
1728                 deps[package] = depends
1729             # Maintain a counter for each virtual package.  If a
1730             # Provides: exists, set the counter to 0 and count all
1731             # provides by a package not in the list for removal.
1732             # If the counter stays 0 at the end, we know that only
1733             # the to-be-removed packages provided this virtual
1734             # package.
1735             if provides is not None:
1736                 for virtual_pkg in provides.split(","):
1737                     virtual_pkg = virtual_pkg.strip()
1738                     if virtual_pkg == package: continue
1739                     if not virtual_packages.has_key(virtual_pkg):
1740                         virtual_packages[virtual_pkg] = 0
1741                     if package not in removals:
1742                         virtual_packages[virtual_pkg] += 1
1743
1744         # If a virtual package is only provided by the to-be-removed
1745         # packages, treat the virtual package as to-be-removed too.
1746         for virtual_pkg in virtual_packages.keys():
1747             if virtual_packages[virtual_pkg] == 0:
1748                 removals.append(virtual_pkg)
1749
1750         # Check binary dependencies (Depends)
1751         for package in deps.keys():
1752             if package in removals: continue
1753             parsed_dep = []
1754             try:
1755                 parsed_dep += apt_pkg.ParseDepends(deps[package])
1756             except ValueError as e:
1757                 print "Error for package %s: %s" % (package, e)
1758             for dep in parsed_dep:
1759                 # Check for partial breakage.  If a package has a ORed
1760                 # dependency, there is only a dependency problem if all
1761                 # packages in the ORed depends will be removed.
1762                 unsat = 0
1763                 for dep_package, _, _ in dep:
1764                     if dep_package in removals:
1765                         unsat += 1
1766                 if unsat == len(dep):
1767                     component = p2c[package]
1768                     source = sources[package]
1769                     if component != "main":
1770                         source = "%s/%s" % (source, component)
1771                     all_broken.setdefault(source, {}).setdefault(package, set()).add(architecture)
1772                     dep_problem = 1
1773
1774     if all_broken:
1775         if cruft:
1776             print "  - broken Depends:"
1777         else:
1778             print "# Broken Depends:"
1779         for source, bindict in sorted(all_broken.items()):
1780             lines = []
1781             for binary, arches in sorted(bindict.items()):
1782                 if arches == all_arches or 'all' in arches:
1783                     lines.append(binary)
1784                 else:
1785                     lines.append('%s [%s]' % (binary, ' '.join(sorted(arches))))
1786             if cruft:
1787                 print '    %s: %s' % (source, lines[0])
1788             else:
1789                 print '%s: %s' % (source, lines[0])
1790             for line in lines[1:]:
1791                 if cruft:
1792                     print '    ' + ' ' * (len(source) + 2) + line
1793                 else:
1794                     print ' ' * (len(source) + 2) + line
1795         if not cruft:
1796             print
1797
1798     # Check source dependencies (Build-Depends and Build-Depends-Indep)
1799     all_broken.clear()
1800     metakey_bd = get_or_set_metadatakey("Build-Depends", session)
1801     metakey_bdi = get_or_set_metadatakey("Build-Depends-Indep", session)
1802     params = {
1803         'suite_id':    dbsuite.suite_id,
1804         'metakey_ids': (metakey_bd.key_id, metakey_bdi.key_id),
1805     }
1806     statement = '''
1807         SELECT s.id, s.source, string_agg(sm.value, ', ') as build_dep
1808            FROM source s
1809            JOIN source_metadata sm ON s.id = sm.src_id
1810            WHERE s.id in
1811                (SELECT source FROM src_associations
1812                    WHERE suite = :suite_id)
1813                AND sm.key_id in :metakey_ids
1814            GROUP BY s.id, s.source'''
1815     query = session.query('id', 'source', 'build_dep').from_statement(statement). \
1816         params(params)
1817     for source_id, source, build_dep in query:
1818         if source in removals: continue
1819         parsed_dep = []
1820         if build_dep is not None:
1821             # Remove [arch] information since we want to see breakage on all arches
1822             build_dep = re_build_dep_arch.sub("", build_dep)
1823             try:
1824                 parsed_dep += apt_pkg.ParseDepends(build_dep)
1825             except ValueError as e:
1826                 print "Error for source %s: %s" % (source, e)
1827         for dep in parsed_dep:
1828             unsat = 0
1829             for dep_package, _, _ in dep:
1830                 if dep_package in removals:
1831                     unsat += 1
1832             if unsat == len(dep):
1833                 component, = session.query(Component.component_name) \
1834                     .join(Component.overrides) \
1835                     .filter(Override.suite == overridesuite) \
1836                     .filter(Override.package == source) \
1837                     .join(Override.overridetype).filter(OverrideType.overridetype == 'dsc') \
1838                     .first()
1839                 if component != "main":
1840                     source = "%s/%s" % (source, component)
1841                 all_broken.setdefault(source, set()).add(pp_deps(dep))
1842                 dep_problem = 1
1843
1844     if all_broken:
1845         if cruft:
1846             print "  - broken Build-Depends:"
1847         else:
1848             print "# Broken Build-Depends:"
1849         for source, bdeps in sorted(all_broken.items()):
1850             bdeps = sorted(bdeps)
1851             if cruft:
1852                 print '    %s: %s' % (source, bdeps[0])
1853             else:
1854                 print '%s: %s' % (source, bdeps[0])
1855             for bdep in bdeps[1:]:
1856                 if cruft:
1857                     print '    ' + ' ' * (len(source) + 2) + bdep
1858                 else:
1859                     print ' ' * (len(source) + 2) + bdep
1860         if not cruft:
1861             print
1862
1863     return dep_problem