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