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