]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
Merge remote-tracking branch 'origin/master'
[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 which_conf_file ():
766     if os.getenv('DAK_CONFIG'):
767         return os.getenv('DAK_CONFIG')
768
769     res = socket.getfqdn()
770     # In case we allow local config files per user, try if one exists
771     if Cnf.find_b("Config::" + res + "::AllowLocalConfig"):
772         homedir = os.getenv("HOME")
773         confpath = os.path.join(homedir, "/etc/dak.conf")
774         if os.path.exists(confpath):
775             apt_pkg.read_config_file_isc(Cnf,confpath)
776
777     # We are still in here, so there is no local config file or we do
778     # not allow local files. Do the normal stuff.
779     if Cnf.get("Config::" + res + "::DakConfig"):
780         return Cnf["Config::" + res + "::DakConfig"]
781
782     return default_config
783
784 ################################################################################
785
786 def TemplateSubst(subst_map, filename):
787     """ Perform a substition of template """
788     templatefile = open_file(filename)
789     template = templatefile.read()
790     for k, v in subst_map.iteritems():
791         template = template.replace(k, str(v))
792     templatefile.close()
793     return template
794
795 ################################################################################
796
797 def fubar(msg, exit_code=1):
798     sys.stderr.write("E: %s\n" % (msg))
799     sys.exit(exit_code)
800
801 def warn(msg):
802     sys.stderr.write("W: %s\n" % (msg))
803
804 ################################################################################
805
806 # Returns the user name with a laughable attempt at rfc822 conformancy
807 # (read: removing stray periods).
808 def whoami ():
809     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
810
811 def getusername ():
812     return pwd.getpwuid(os.getuid())[0]
813
814 ################################################################################
815
816 def size_type (c):
817     t  = " B"
818     if c > 10240:
819         c = c / 1024
820         t = " KB"
821     if c > 10240:
822         c = c / 1024
823         t = " MB"
824     return ("%d%s" % (c, t))
825
826 ################################################################################
827
828 def cc_fix_changes (changes):
829     o = changes.get("architecture", "")
830     if o:
831         del changes["architecture"]
832     changes["architecture"] = {}
833     for j in o.split():
834         changes["architecture"][j] = 1
835
836 def changes_compare (a, b):
837     """ Sort by source name, source version, 'have source', and then by filename """
838     try:
839         a_changes = parse_changes(a)
840     except:
841         return -1
842
843     try:
844         b_changes = parse_changes(b)
845     except:
846         return 1
847
848     cc_fix_changes (a_changes)
849     cc_fix_changes (b_changes)
850
851     # Sort by source name
852     a_source = a_changes.get("source")
853     b_source = b_changes.get("source")
854     q = cmp (a_source, b_source)
855     if q:
856         return q
857
858     # Sort by source version
859     a_version = a_changes.get("version", "0")
860     b_version = b_changes.get("version", "0")
861     q = apt_pkg.version_compare(a_version, b_version)
862     if q:
863         return q
864
865     # Sort by 'have source'
866     a_has_source = a_changes["architecture"].get("source")
867     b_has_source = b_changes["architecture"].get("source")
868     if a_has_source and not b_has_source:
869         return -1
870     elif b_has_source and not a_has_source:
871         return 1
872
873     # Fall back to sort by filename
874     return cmp(a, b)
875
876 ################################################################################
877
878 def find_next_free (dest, too_many=100):
879     extra = 0
880     orig_dest = dest
881     while os.path.lexists(dest) and extra < too_many:
882         dest = orig_dest + '.' + repr(extra)
883         extra += 1
884     if extra >= too_many:
885         raise NoFreeFilenameError
886     return dest
887
888 ################################################################################
889
890 def result_join (original, sep = '\t'):
891     resultlist = []
892     for i in xrange(len(original)):
893         if original[i] == None:
894             resultlist.append("")
895         else:
896             resultlist.append(original[i])
897     return sep.join(resultlist)
898
899 ################################################################################
900
901 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
902     out = ""
903     for line in str.split('\n'):
904         line = line.strip()
905         if line or include_blank_lines:
906             out += "%s%s\n" % (prefix, line)
907     # Strip trailing new line
908     if out:
909         out = out[:-1]
910     return out
911
912 ################################################################################
913
914 def validate_changes_file_arg(filename, require_changes=1):
915     """
916     'filename' is either a .changes or .dak file.  If 'filename' is a
917     .dak file, it's changed to be the corresponding .changes file.  The
918     function then checks if the .changes file a) exists and b) is
919     readable and returns the .changes filename if so.  If there's a
920     problem, the next action depends on the option 'require_changes'
921     argument:
922
923       - If 'require_changes' == -1, errors are ignored and the .changes
924         filename is returned.
925       - If 'require_changes' == 0, a warning is given and 'None' is returned.
926       - If 'require_changes' == 1, a fatal error is raised.
927
928     """
929     error = None
930
931     orig_filename = filename
932     if filename.endswith(".dak"):
933         filename = filename[:-4]+".changes"
934
935     if not filename.endswith(".changes"):
936         error = "invalid file type; not a changes file"
937     else:
938         if not os.access(filename,os.R_OK):
939             if os.path.exists(filename):
940                 error = "permission denied"
941             else:
942                 error = "file not found"
943
944     if error:
945         if require_changes == 1:
946             fubar("%s: %s." % (orig_filename, error))
947         elif require_changes == 0:
948             warn("Skipping %s - %s" % (orig_filename, error))
949             return None
950         else: # We only care about the .dak file
951             return filename
952     else:
953         return filename
954
955 ################################################################################
956
957 def real_arch(arch):
958     return (arch != "source" and arch != "all")
959
960 ################################################################################
961
962 def join_with_commas_and(list):
963     if len(list) == 0: return "nothing"
964     if len(list) == 1: return list[0]
965     return ", ".join(list[:-1]) + " and " + list[-1]
966
967 ################################################################################
968
969 def pp_deps (deps):
970     pp_deps = []
971     for atom in deps:
972         (pkg, version, constraint) = atom
973         if constraint:
974             pp_dep = "%s (%s %s)" % (pkg, constraint, version)
975         else:
976             pp_dep = pkg
977         pp_deps.append(pp_dep)
978     return " |".join(pp_deps)
979
980 ################################################################################
981
982 def get_conf():
983     return Cnf
984
985 ################################################################################
986
987 def parse_args(Options):
988     """ Handle -a, -c and -s arguments; returns them as SQL constraints """
989     # XXX: This should go away and everything which calls it be converted
990     #      to use SQLA properly.  For now, we'll just fix it not to use
991     #      the old Pg interface though
992     session = DBConn().session()
993     # Process suite
994     if Options["Suite"]:
995         suite_ids_list = []
996         for suitename in split_args(Options["Suite"]):
997             suite = get_suite(suitename, session=session)
998             if not suite or suite.suite_id is None:
999                 warn("suite '%s' not recognised." % (suite and suite.suite_name or suitename))
1000             else:
1001                 suite_ids_list.append(suite.suite_id)
1002         if suite_ids_list:
1003             con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
1004         else:
1005             fubar("No valid suite given.")
1006     else:
1007         con_suites = ""
1008
1009     # Process component
1010     if Options["Component"]:
1011         component_ids_list = []
1012         for componentname in split_args(Options["Component"]):
1013             component = get_component(componentname, session=session)
1014             if component is None:
1015                 warn("component '%s' not recognised." % (componentname))
1016             else:
1017                 component_ids_list.append(component.component_id)
1018         if component_ids_list:
1019             con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
1020         else:
1021             fubar("No valid component given.")
1022     else:
1023         con_components = ""
1024
1025     # Process architecture
1026     con_architectures = ""
1027     check_source = 0
1028     if Options["Architecture"]:
1029         arch_ids_list = []
1030         for archname in split_args(Options["Architecture"]):
1031             if archname == "source":
1032                 check_source = 1
1033             else:
1034                 arch = get_architecture(archname, session=session)
1035                 if arch is None:
1036                     warn("architecture '%s' not recognised." % (archname))
1037                 else:
1038                     arch_ids_list.append(arch.arch_id)
1039         if arch_ids_list:
1040             con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
1041         else:
1042             if not check_source:
1043                 fubar("No valid architecture given.")
1044     else:
1045         check_source = 1
1046
1047     return (con_suites, con_architectures, con_components, check_source)
1048
1049 ################################################################################
1050
1051 def arch_compare_sw (a, b):
1052     """
1053     Function for use in sorting lists of architectures.
1054
1055     Sorts normally except that 'source' dominates all others.
1056     """
1057
1058     if a == "source" and b == "source":
1059         return 0
1060     elif a == "source":
1061         return -1
1062     elif b == "source":
1063         return 1
1064
1065     return cmp (a, b)
1066
1067 ################################################################################
1068
1069 def split_args (s, dwim=1):
1070     """
1071     Split command line arguments which can be separated by either commas
1072     or whitespace.  If dwim is set, it will complain about string ending
1073     in comma since this usually means someone did 'dak ls -a i386, m68k
1074     foo' or something and the inevitable confusion resulting from 'm68k'
1075     being treated as an argument is undesirable.
1076     """
1077
1078     if s.find(",") == -1:
1079         return s.split()
1080     else:
1081         if s[-1:] == "," and dwim:
1082             fubar("split_args: found trailing comma, spurious space maybe?")
1083         return s.split(",")
1084
1085 ################################################################################
1086
1087 def gpgv_get_status_output(cmd, status_read, status_write):
1088     """
1089     Our very own version of commands.getouputstatus(), hacked to support
1090     gpgv's status fd.
1091     """
1092
1093     cmd = ['/bin/sh', '-c', cmd]
1094     p2cread, p2cwrite = os.pipe()
1095     c2pread, c2pwrite = os.pipe()
1096     errout, errin = os.pipe()
1097     pid = os.fork()
1098     if pid == 0:
1099         # Child
1100         os.close(0)
1101         os.close(1)
1102         os.dup(p2cread)
1103         os.dup(c2pwrite)
1104         os.close(2)
1105         os.dup(errin)
1106         for i in range(3, 256):
1107             if i != status_write:
1108                 try:
1109                     os.close(i)
1110                 except:
1111                     pass
1112         try:
1113             os.execvp(cmd[0], cmd)
1114         finally:
1115             os._exit(1)
1116
1117     # Parent
1118     os.close(p2cread)
1119     os.dup2(c2pread, c2pwrite)
1120     os.dup2(errout, errin)
1121
1122     output = status = ""
1123     while 1:
1124         i, o, e = select.select([c2pwrite, errin, status_read], [], [])
1125         more_data = []
1126         for fd in i:
1127             r = os.read(fd, 8196)
1128             if len(r) > 0:
1129                 more_data.append(fd)
1130                 if fd == c2pwrite or fd == errin:
1131                     output += r
1132                 elif fd == status_read:
1133                     status += r
1134                 else:
1135                     fubar("Unexpected file descriptor [%s] returned from select\n" % (fd))
1136         if not more_data:
1137             pid, exit_status = os.waitpid(pid, 0)
1138             try:
1139                 os.close(status_write)
1140                 os.close(status_read)
1141                 os.close(c2pread)
1142                 os.close(c2pwrite)
1143                 os.close(p2cwrite)
1144                 os.close(errin)
1145                 os.close(errout)
1146             except:
1147                 pass
1148             break
1149
1150     return output, status, exit_status
1151
1152 ################################################################################
1153
1154 def process_gpgv_output(status):
1155     # Process the status-fd output
1156     keywords = {}
1157     internal_error = ""
1158     for line in status.split('\n'):
1159         line = line.strip()
1160         if line == "":
1161             continue
1162         split = line.split()
1163         if len(split) < 2:
1164             internal_error += "gpgv status line is malformed (< 2 atoms) ['%s'].\n" % (line)
1165             continue
1166         (gnupg, keyword) = split[:2]
1167         if gnupg != "[GNUPG:]":
1168             internal_error += "gpgv status line is malformed (incorrect prefix '%s').\n" % (gnupg)
1169             continue
1170         args = split[2:]
1171         if keywords.has_key(keyword) and keyword not in [ "NODATA", "SIGEXPIRED", "KEYEXPIRED" ]:
1172             internal_error += "found duplicate status token ('%s').\n" % (keyword)
1173             continue
1174         else:
1175             keywords[keyword] = args
1176
1177     return (keywords, internal_error)
1178
1179 ################################################################################
1180
1181 def retrieve_key (filename, keyserver=None, keyring=None):
1182     """
1183     Retrieve the key that signed 'filename' from 'keyserver' and
1184     add it to 'keyring'.  Returns nothing on success, or an error message
1185     on error.
1186     """
1187
1188     # Defaults for keyserver and keyring
1189     if not keyserver:
1190         keyserver = Cnf["Dinstall::KeyServer"]
1191     if not keyring:
1192         keyring = get_primary_keyring_path()
1193
1194     # Ensure the filename contains no shell meta-characters or other badness
1195     if not re_taint_free.match(filename):
1196         return "%s: tainted filename" % (filename)
1197
1198     # Invoke gpgv on the file
1199     status_read, status_write = os.pipe()
1200     cmd = "gpgv --status-fd %s --keyring /dev/null %s" % (status_write, filename)
1201     (_, status, _) = gpgv_get_status_output(cmd, status_read, status_write)
1202
1203     # Process the status-fd output
1204     (keywords, internal_error) = process_gpgv_output(status)
1205     if internal_error:
1206         return internal_error
1207
1208     if not keywords.has_key("NO_PUBKEY"):
1209         return "didn't find expected NO_PUBKEY in gpgv status-fd output"
1210
1211     fingerprint = keywords["NO_PUBKEY"][0]
1212     # XXX - gpg sucks.  You can't use --secret-keyring=/dev/null as
1213     # it'll try to create a lockfile in /dev.  A better solution might
1214     # be a tempfile or something.
1215     cmd = "gpg --no-default-keyring --secret-keyring=%s --no-options" \
1216           % (Cnf["Dinstall::SigningKeyring"])
1217     cmd += " --keyring %s --keyserver %s --recv-key %s" \
1218            % (keyring, keyserver, fingerprint)
1219     (result, output) = commands.getstatusoutput(cmd)
1220     if (result != 0):
1221         return "'%s' failed with exit code %s" % (cmd, result)
1222
1223     return ""
1224
1225 ################################################################################
1226
1227 def gpg_keyring_args(keyrings=None):
1228     if not keyrings:
1229         keyrings = get_active_keyring_paths()
1230
1231     return " ".join(["--keyring %s" % x for x in keyrings])
1232
1233 ################################################################################
1234 @session_wrapper
1235 def check_signature (sig_filename, data_filename="", keyrings=None, autofetch=None, session=None):
1236     """
1237     Check the signature of a file and return the fingerprint if the
1238     signature is valid or 'None' if it's not.  The first argument is the
1239     filename whose signature should be checked.  The second argument is a
1240     reject function and is called when an error is found.  The reject()
1241     function must allow for two arguments: the first is the error message,
1242     the second is an optional prefix string.  It's possible for reject()
1243     to be called more than once during an invocation of check_signature().
1244     The third argument is optional and is the name of the files the
1245     detached signature applies to.  The fourth argument is optional and is
1246     a *list* of keyrings to use.  'autofetch' can either be None, True or
1247     False.  If None, the default behaviour specified in the config will be
1248     used.
1249     """
1250
1251     rejects = []
1252
1253     # Ensure the filename contains no shell meta-characters or other badness
1254     if not re_taint_free.match(sig_filename):
1255         rejects.append("!!WARNING!! tainted signature filename: '%s'." % (sig_filename))
1256         return (None, rejects)
1257
1258     if data_filename and not re_taint_free.match(data_filename):
1259         rejects.append("!!WARNING!! tainted data filename: '%s'." % (data_filename))
1260         return (None, rejects)
1261
1262     if not keyrings:
1263         keyrings = [ x.keyring_name for x in session.query(Keyring).filter(Keyring.active == True).all() ]
1264
1265     # Autofetch the signing key if that's enabled
1266     if autofetch == None:
1267         autofetch = Cnf.get("Dinstall::KeyAutoFetch")
1268     if autofetch:
1269         error_msg = retrieve_key(sig_filename)
1270         if error_msg:
1271             rejects.append(error_msg)
1272             return (None, rejects)
1273
1274     # Build the command line
1275     status_read, status_write = os.pipe()
1276     cmd = "gpgv --status-fd %s %s %s %s" % (
1277         status_write, gpg_keyring_args(keyrings), sig_filename, data_filename)
1278
1279     # Invoke gpgv on the file
1280     (output, status, exit_status) = gpgv_get_status_output(cmd, status_read, status_write)
1281
1282     # Process the status-fd output
1283     (keywords, internal_error) = process_gpgv_output(status)
1284
1285     # If we failed to parse the status-fd output, let's just whine and bail now
1286     if internal_error:
1287         rejects.append("internal error while performing signature check on %s." % (sig_filename))
1288         rejects.append(internal_error, "")
1289         rejects.append("Please report the above errors to the Archive maintainers by replying to this mail.", "")
1290         return (None, rejects)
1291
1292     # Now check for obviously bad things in the processed output
1293     if keywords.has_key("KEYREVOKED"):
1294         rejects.append("The key used to sign %s has been revoked." % (sig_filename))
1295     if keywords.has_key("BADSIG"):
1296         rejects.append("bad signature on %s." % (sig_filename))
1297     if keywords.has_key("ERRSIG") and not keywords.has_key("NO_PUBKEY"):
1298         rejects.append("failed to check signature on %s." % (sig_filename))
1299     if keywords.has_key("NO_PUBKEY"):
1300         args = keywords["NO_PUBKEY"]
1301         if len(args) >= 1:
1302             key = args[0]
1303         rejects.append("The key (0x%s) used to sign %s wasn't found in the keyring(s)." % (key, sig_filename))
1304     if keywords.has_key("BADARMOR"):
1305         rejects.append("ASCII armour of signature was corrupt in %s." % (sig_filename))
1306     if keywords.has_key("NODATA"):
1307         rejects.append("no signature found in %s." % (sig_filename))
1308     if keywords.has_key("EXPKEYSIG"):
1309         args = keywords["EXPKEYSIG"]
1310         if len(args) >= 1:
1311             key = args[0]
1312         rejects.append("Signature made by expired key 0x%s" % (key))
1313     if keywords.has_key("KEYEXPIRED") and not keywords.has_key("GOODSIG"):
1314         args = keywords["KEYEXPIRED"]
1315         expiredate=""
1316         if len(args) >= 1:
1317             timestamp = args[0]
1318             if timestamp.count("T") == 0:
1319                 try:
1320                     expiredate = time.strftime("%Y-%m-%d", time.gmtime(float(timestamp)))
1321                 except ValueError:
1322                     expiredate = "unknown (%s)" % (timestamp)
1323             else:
1324                 expiredate = timestamp
1325         rejects.append("The key used to sign %s has expired on %s" % (sig_filename, expiredate))
1326
1327     if len(rejects) > 0:
1328         return (None, rejects)
1329
1330     # Next check gpgv exited with a zero return code
1331     if exit_status:
1332         rejects.append("gpgv failed while checking %s." % (sig_filename))
1333         if status.strip():
1334             rejects.append(prefix_multi_line_string(status, " [GPG status-fd output:] "))
1335         else:
1336             rejects.append(prefix_multi_line_string(output, " [GPG output:] "))
1337         return (None, rejects)
1338
1339     # Sanity check the good stuff we expect
1340     if not keywords.has_key("VALIDSIG"):
1341         rejects.append("signature on %s does not appear to be valid [No VALIDSIG]." % (sig_filename))
1342     else:
1343         args = keywords["VALIDSIG"]
1344         if len(args) < 1:
1345             rejects.append("internal error while checking signature on %s." % (sig_filename))
1346         else:
1347             fingerprint = args[0]
1348     if not keywords.has_key("GOODSIG"):
1349         rejects.append("signature on %s does not appear to be valid [No GOODSIG]." % (sig_filename))
1350     if not keywords.has_key("SIG_ID"):
1351         rejects.append("signature on %s does not appear to be valid [No SIG_ID]." % (sig_filename))
1352
1353     # Finally ensure there's not something we don't recognise
1354     known_keywords = dict(VALIDSIG="",SIG_ID="",GOODSIG="",BADSIG="",ERRSIG="",
1355                           SIGEXPIRED="",KEYREVOKED="",NO_PUBKEY="",BADARMOR="",
1356                           NODATA="",NOTATION_DATA="",NOTATION_NAME="",KEYEXPIRED="",POLICY_URL="")
1357
1358     for keyword in keywords.keys():
1359         if not known_keywords.has_key(keyword):
1360             rejects.append("found unknown status token '%s' from gpgv with args '%r' in %s." % (keyword, keywords[keyword], sig_filename))
1361
1362     if len(rejects) > 0:
1363         return (None, rejects)
1364     else:
1365         return (fingerprint, [])
1366
1367 ################################################################################
1368
1369 def gpg_get_key_addresses(fingerprint):
1370     """retreive email addresses from gpg key uids for a given fingerprint"""
1371     addresses = key_uid_email_cache.get(fingerprint)
1372     if addresses != None:
1373         return addresses
1374     addresses = list()
1375     cmd = "gpg --no-default-keyring %s --fingerprint %s" \
1376                 % (gpg_keyring_args(), fingerprint)
1377     (result, output) = commands.getstatusoutput(cmd)
1378     if result == 0:
1379         for l in output.split('\n'):
1380             m = re_gpg_uid.match(l)
1381             if not m:
1382                 continue
1383             address = m.group(1)
1384             if address.endswith('@debian.org'):
1385                 # prefer @debian.org addresses
1386                 # TODO: maybe not hardcode the domain
1387                 addresses.insert(0, address)
1388             else:
1389                 addresses.append(m.group(1))
1390     key_uid_email_cache[fingerprint] = addresses
1391     return addresses
1392
1393 ################################################################################
1394
1395 def get_logins_from_ldap(fingerprint='*'):
1396     """retrieve login from LDAP linked to a given fingerprint"""
1397
1398     LDAPDn = Cnf['Import-LDAP-Fingerprints::LDAPDn']
1399     LDAPServer = Cnf['Import-LDAP-Fingerprints::LDAPServer']
1400     l = ldap.open(LDAPServer)
1401     l.simple_bind_s('','')
1402     Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
1403                        '(keyfingerprint=%s)' % fingerprint,
1404                        ['uid', 'keyfingerprint'])
1405     login = {}
1406     for elem in Attrs:
1407         login[elem[1]['keyFingerPrint'][0]] = elem[1]['uid'][0]
1408     return login
1409
1410 ################################################################################
1411
1412 def get_users_from_ldap():
1413     """retrieve login and user names from LDAP"""
1414
1415     LDAPDn = Cnf['Import-LDAP-Fingerprints::LDAPDn']
1416     LDAPServer = Cnf['Import-LDAP-Fingerprints::LDAPServer']
1417     l = ldap.open(LDAPServer)
1418     l.simple_bind_s('','')
1419     Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
1420                        '(uid=*)', ['uid', 'cn', 'mn', 'sn'])
1421     users = {}
1422     for elem in Attrs:
1423         elem = elem[1]
1424         name = []
1425         for k in ('cn', 'mn', 'sn'):
1426             try:
1427                 if elem[k][0] != '-':
1428                     name.append(elem[k][0])
1429             except KeyError:
1430                 pass
1431         users[' '.join(name)] = elem['uid'][0]
1432     return users
1433
1434 ################################################################################
1435
1436 def clean_symlink (src, dest, root):
1437     """
1438     Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
1439     Returns fixed 'src'
1440     """
1441     src = src.replace(root, '', 1)
1442     dest = dest.replace(root, '', 1)
1443     dest = os.path.dirname(dest)
1444     new_src = '../' * len(dest.split('/'))
1445     return new_src + src
1446
1447 ################################################################################
1448
1449 def temp_filename(directory=None, prefix="dak", suffix="", mode=None, group=None):
1450     """
1451     Return a secure and unique filename by pre-creating it.
1452
1453     @type directory: str
1454     @param directory: If non-null it will be the directory the file is pre-created in.
1455
1456     @type prefix: str
1457     @param prefix: The filename will be prefixed with this string
1458
1459     @type suffix: str
1460     @param suffix: The filename will end with this string
1461
1462     @type mode: str
1463     @param mode: If set the file will get chmodded to those permissions
1464
1465     @type group: str
1466     @param group: If set the file will get chgrped to the specified group.
1467
1468     @rtype: list
1469     @return: Returns a pair (fd, name)
1470     """
1471
1472     (tfd, tfname) = tempfile.mkstemp(suffix, prefix, directory)
1473     if mode:
1474         os.chmod(tfname, mode)
1475     if group:
1476         gid = grp.getgrnam(group).gr_gid
1477         os.chown(tfname, -1, gid)
1478     return (tfd, tfname)
1479
1480 ################################################################################
1481
1482 def temp_dirname(parent=None, prefix="dak", suffix="", mode=None, group=None):
1483     """
1484     Return a secure and unique directory by pre-creating it.
1485
1486     @type parent: str
1487     @param parent: If non-null it will be the directory the directory is pre-created in.
1488
1489     @type prefix: str
1490     @param prefix: The filename will be prefixed with this string
1491
1492     @type suffix: str
1493     @param suffix: The filename will end with this string
1494
1495     @type mode: str
1496     @param mode: If set the file will get chmodded to those permissions
1497
1498     @type group: str
1499     @param group: If set the file will get chgrped to the specified group.
1500
1501     @rtype: list
1502     @return: Returns a pair (fd, name)
1503
1504     """
1505
1506     tfname = tempfile.mkdtemp(suffix, prefix, parent)
1507     if mode:
1508         os.chmod(tfname, mode)
1509     if group:
1510         gid = grp.getgrnam(group).gr_gid
1511         os.chown(tfname, -1, gid)
1512     return tfname
1513
1514 ################################################################################
1515
1516 def is_email_alias(email):
1517     """ checks if the user part of the email is listed in the alias file """
1518     global alias_cache
1519     if alias_cache == None:
1520         aliasfn = which_alias_file()
1521         alias_cache = set()
1522         if aliasfn:
1523             for l in open(aliasfn):
1524                 alias_cache.add(l.split(':')[0])
1525     uid = email.split('@')[0]
1526     return uid in alias_cache
1527
1528 ################################################################################
1529
1530 def get_changes_files(from_dir):
1531     """
1532     Takes a directory and lists all .changes files in it (as well as chdir'ing
1533     to the directory; this is due to broken behaviour on the part of p-u/p-a
1534     when you're not in the right place)
1535
1536     Returns a list of filenames
1537     """
1538     try:
1539         # Much of the rest of p-u/p-a depends on being in the right place
1540         os.chdir(from_dir)
1541         changes_files = [x for x in os.listdir(from_dir) if x.endswith('.changes')]
1542     except OSError as e:
1543         fubar("Failed to read list from directory %s (%s)" % (from_dir, e))
1544
1545     return changes_files
1546
1547 ################################################################################
1548
1549 Cnf = config.Config().Cnf
1550
1551 ################################################################################
1552
1553 def parse_wnpp_bug_file(file = "/srv/ftp-master.debian.org/scripts/masterfiles/wnpp_rm"):
1554     """
1555     Parses the wnpp bug list available at http://qa.debian.org/data/bts/wnpp_rm
1556     Well, actually it parsed a local copy, but let's document the source
1557     somewhere ;)
1558
1559     returns a dict associating source package name with a list of open wnpp
1560     bugs (Yes, there might be more than one)
1561     """
1562
1563     line = []
1564     try:
1565         f = open(file)
1566         lines = f.readlines()
1567     except IOError as e:
1568         print "Warning:  Couldn't open %s; don't know about WNPP bugs, so won't close any." % file
1569         lines = []
1570     wnpp = {}
1571
1572     for line in lines:
1573         splited_line = line.split(": ", 1)
1574         if len(splited_line) > 1:
1575             wnpp[splited_line[0]] = splited_line[1].split("|")
1576
1577     for source in wnpp.keys():
1578         bugs = []
1579         for wnpp_bug in wnpp[source]:
1580             bug_no = re.search("(\d)+", wnpp_bug).group()
1581             if bug_no:
1582                 bugs.append(bug_no)
1583         wnpp[source] = bugs
1584     return wnpp
1585
1586 ################################################################################
1587
1588 def get_packages_from_ftp(root, suite, component, architecture):
1589     """
1590     Returns an object containing apt_pkg-parseable data collected by
1591     aggregating Packages.gz files gathered for each architecture.
1592
1593     @type root: string
1594     @param root: path to ftp archive root directory
1595
1596     @type suite: string
1597     @param suite: suite to extract files from
1598
1599     @type component: string
1600     @param component: component to extract files from
1601
1602     @type architecture: string
1603     @param architecture: architecture to extract files from
1604
1605     @rtype: TagFile
1606     @return: apt_pkg class containing package data
1607     """
1608     filename = "%s/dists/%s/%s/binary-%s/Packages.gz" % (root, suite, component, architecture)
1609     (fd, temp_file) = temp_filename()
1610     (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_file))
1611     if (result != 0):
1612         fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1613     filename = "%s/dists/%s/%s/debian-installer/binary-%s/Packages.gz" % (root, suite, component, architecture)
1614     if os.path.exists(filename):
1615         (result, output) = commands.getstatusoutput("gunzip -c %s >> %s" % (filename, temp_file))
1616         if (result != 0):
1617             fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1618     packages = open_file(temp_file)
1619     Packages = apt_pkg.TagFile(packages)
1620     os.unlink(temp_file)
1621     return Packages
1622
1623 ################################################################################
1624
1625 def deb_extract_control(fh):
1626     """extract DEBIAN/control from a binary package"""
1627     return apt_inst.DebFile(fh).control.extractdata("control")
1628
1629 ################################################################################
1630
1631 def mail_addresses_for_upload(maintainer, changed_by, fingerprint):
1632     """mail addresses to contact for an upload
1633
1634     @type  maintainer: str
1635     @param maintainer: Maintainer field of the .changes file
1636
1637     @type  changed_by: str
1638     @param changed_by: Changed-By field of the .changes file
1639
1640     @type  fingerprint: str
1641     @param fingerprint: fingerprint of the key used to sign the upload
1642
1643     @rtype:  list of str
1644     @return: list of RFC 2047-encoded mail addresses to contact regarding
1645              this upload
1646     """
1647     addresses = [maintainer]
1648     if changed_by != maintainer:
1649         addresses.append(changed_by)
1650
1651     fpr_addresses = gpg_get_key_addresses(fingerprint)
1652     if len(fpr_addresses) > 0 and fix_maintainer(changed_by)[3] not in fpr_addresses and fix_maintainer(maintainer)[3] not in fpr_addresses:
1653         addresses.append(fpr_addresses[0])
1654
1655     encoded_addresses = [ fix_maintainer(e)[1] for e in addresses ]
1656     return encoded_addresses
1657
1658 ################################################################################
1659
1660 def call_editor(text="", suffix=".txt"):
1661     """run editor and return the result as a string
1662
1663     @type  text: str
1664     @param text: initial text
1665
1666     @type  suffix: str
1667     @param suffix: extension for temporary file
1668
1669     @rtype:  str
1670     @return: string with the edited text
1671     """
1672     editor = os.environ.get('VISUAL', os.environ.get('EDITOR', 'vi'))
1673     tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
1674     try:
1675         print >>tmp, text,
1676         tmp.close()
1677         daklib.daksubprocess.check_call([editor, tmp.name])
1678         return open(tmp.name, 'r').read()
1679     finally:
1680         os.unlink(tmp.name)
1681
1682 ################################################################################
1683
1684 def check_reverse_depends(removals, suite, arches=None, session=None, cruft=False):
1685     dbsuite = get_suite(suite, session)
1686     overridesuite = dbsuite
1687     if dbsuite.overridesuite is not None:
1688         overridesuite = get_suite(dbsuite.overridesuite, session)
1689     dep_problem = 0
1690     p2c = {}
1691     all_broken = {}
1692     if arches:
1693         all_arches = set(arches)
1694     else:
1695         all_arches = set([x.arch_string for x in get_suite_architectures(suite)])
1696     all_arches -= set(["source", "all"])
1697     metakey_d = get_or_set_metadatakey("Depends", session)
1698     metakey_p = get_or_set_metadatakey("Provides", session)
1699     params = {
1700         'suite_id':     dbsuite.suite_id,
1701         'metakey_d_id': metakey_d.key_id,
1702         'metakey_p_id': metakey_p.key_id,
1703     }
1704     for architecture in all_arches | set(['all']):
1705         deps = {}
1706         sources = {}
1707         virtual_packages = {}
1708         params['arch_id'] = get_architecture(architecture, session).arch_id
1709
1710         statement = '''
1711             SELECT b.id, b.package, s.source, c.name as component,
1712                 (SELECT bmd.value FROM binaries_metadata bmd WHERE bmd.bin_id = b.id AND bmd.key_id = :metakey_d_id) AS depends,
1713                 (SELECT bmp.value FROM binaries_metadata bmp WHERE bmp.bin_id = b.id AND bmp.key_id = :metakey_p_id) AS provides
1714                 FROM binaries b
1715                 JOIN bin_associations ba ON b.id = ba.bin AND ba.suite = :suite_id
1716                 JOIN source s ON b.source = s.id
1717                 JOIN files_archive_map af ON b.file = af.file_id
1718                 JOIN component c ON af.component_id = c.id
1719                 WHERE b.architecture = :arch_id'''
1720         query = session.query('id', 'package', 'source', 'component', 'depends', 'provides'). \
1721             from_statement(statement).params(params)
1722         for binary_id, package, source, component, depends, provides in query:
1723             sources[package] = source
1724             p2c[package] = component
1725             if depends is not None:
1726                 deps[package] = depends
1727             # Maintain a counter for each virtual package.  If a
1728             # Provides: exists, set the counter to 0 and count all
1729             # provides by a package not in the list for removal.
1730             # If the counter stays 0 at the end, we know that only
1731             # the to-be-removed packages provided this virtual
1732             # package.
1733             if provides is not None:
1734                 for virtual_pkg in provides.split(","):
1735                     virtual_pkg = virtual_pkg.strip()
1736                     if virtual_pkg == package: continue
1737                     if not virtual_packages.has_key(virtual_pkg):
1738                         virtual_packages[virtual_pkg] = 0
1739                     if package not in removals:
1740                         virtual_packages[virtual_pkg] += 1
1741
1742         # If a virtual package is only provided by the to-be-removed
1743         # packages, treat the virtual package as to-be-removed too.
1744         for virtual_pkg in virtual_packages.keys():
1745             if virtual_packages[virtual_pkg] == 0:
1746                 removals.append(virtual_pkg)
1747
1748         # Check binary dependencies (Depends)
1749         for package in deps.keys():
1750             if package in removals: continue
1751             parsed_dep = []
1752             try:
1753                 parsed_dep += apt_pkg.parse_depends(deps[package])
1754             except ValueError as e:
1755                 print "Error for package %s: %s" % (package, e)
1756             for dep in parsed_dep:
1757                 # Check for partial breakage.  If a package has a ORed
1758                 # dependency, there is only a dependency problem if all
1759                 # packages in the ORed depends will be removed.
1760                 unsat = 0
1761                 for dep_package, _, _ in dep:
1762                     if dep_package in removals:
1763                         unsat += 1
1764                 if unsat == len(dep):
1765                     component = p2c[package]
1766                     source = sources[package]
1767                     if component != "main":
1768                         source = "%s/%s" % (source, component)
1769                     all_broken.setdefault(source, {}).setdefault(package, set()).add(architecture)
1770                     dep_problem = 1
1771
1772     if all_broken:
1773         if cruft:
1774             print "  - broken Depends:"
1775         else:
1776             print "# Broken Depends:"
1777         for source, bindict in sorted(all_broken.items()):
1778             lines = []
1779             for binary, arches in sorted(bindict.items()):
1780                 if arches == all_arches or 'all' in arches:
1781                     lines.append(binary)
1782                 else:
1783                     lines.append('%s [%s]' % (binary, ' '.join(sorted(arches))))
1784             if cruft:
1785                 print '    %s: %s' % (source, lines[0])
1786             else:
1787                 print '%s: %s' % (source, lines[0])
1788             for line in lines[1:]:
1789                 if cruft:
1790                     print '    ' + ' ' * (len(source) + 2) + line
1791                 else:
1792                     print ' ' * (len(source) + 2) + line
1793         if not cruft:
1794             print
1795
1796     # Check source dependencies (Build-Depends and Build-Depends-Indep)
1797     all_broken.clear()
1798     metakey_bd = get_or_set_metadatakey("Build-Depends", session)
1799     metakey_bdi = get_or_set_metadatakey("Build-Depends-Indep", session)
1800     params = {
1801         'suite_id':    dbsuite.suite_id,
1802         'metakey_ids': (metakey_bd.key_id, metakey_bdi.key_id),
1803     }
1804     statement = '''
1805         SELECT s.id, s.source, string_agg(sm.value, ', ') as build_dep
1806            FROM source s
1807            JOIN source_metadata sm ON s.id = sm.src_id
1808            WHERE s.id in
1809                (SELECT source FROM src_associations
1810                    WHERE suite = :suite_id)
1811                AND sm.key_id in :metakey_ids
1812            GROUP BY s.id, s.source'''
1813     query = session.query('id', 'source', 'build_dep').from_statement(statement). \
1814         params(params)
1815     for source_id, source, build_dep in query:
1816         if source in removals: continue
1817         parsed_dep = []
1818         if build_dep is not None:
1819             # Remove [arch] information since we want to see breakage on all arches
1820             build_dep = re_build_dep_arch.sub("", build_dep)
1821             try:
1822                 parsed_dep += apt_pkg.parse_depends(build_dep)
1823             except ValueError as e:
1824                 print "Error for source %s: %s" % (source, e)
1825         for dep in parsed_dep:
1826             unsat = 0
1827             for dep_package, _, _ in dep:
1828                 if dep_package in removals:
1829                     unsat += 1
1830             if unsat == len(dep):
1831                 component, = session.query(Component.component_name) \
1832                     .join(Component.overrides) \
1833                     .filter(Override.suite == overridesuite) \
1834                     .filter(Override.package == re.sub('/(contrib|non-free)$', '', source)) \
1835                     .join(Override.overridetype).filter(OverrideType.overridetype == 'dsc') \
1836                     .first()
1837                 if component != "main":
1838                     source = "%s/%s" % (source, component)
1839                 all_broken.setdefault(source, set()).add(pp_deps(dep))
1840                 dep_problem = 1
1841
1842     if all_broken:
1843         if cruft:
1844             print "  - broken Build-Depends:"
1845         else:
1846             print "# Broken Build-Depends:"
1847         for source, bdeps in sorted(all_broken.items()):
1848             bdeps = sorted(bdeps)
1849             if cruft:
1850                 print '    %s: %s' % (source, bdeps[0])
1851             else:
1852                 print '%s: %s' % (source, bdeps[0])
1853             for bdep in bdeps[1:]:
1854                 if cruft:
1855                     print '    ' + ' ' * (len(source) + 2) + bdep
1856                 else:
1857                     print ' ' * (len(source) + 2) + bdep
1858         if not cruft:
1859             print
1860
1861     return dep_problem