]> git.decadent.org.uk Git - dak.git/blob - daklib/utils.py
6e7fc06992fb3ad23ee136371fc47bff59d33e79
[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 codecs
27 import datetime
28 import email.Header
29 import os
30 import pwd
31 import grp
32 import select
33 import socket
34 import shutil
35 import sys
36 import tempfile
37 import traceback
38 import stat
39 import apt_inst
40 import apt_pkg
41 import time
42 import re
43 import email as modemail
44 import subprocess
45 import ldap
46 import errno
47
48 import daklib.config as config
49 import daklib.daksubprocess
50 from dbconn import DBConn, get_architecture, get_component, get_suite, \
51                    get_override_type, Keyring, session_wrapper, \
52                    get_active_keyring_paths, \
53                    get_suite_architectures, get_or_set_metadatakey, DBSource, \
54                    Component, Override, OverrideType
55 from sqlalchemy import desc
56 from dak_exceptions import *
57 from gpg import SignedFile
58 from textutils import fix_maintainer
59 from regexes import re_html_escaping, html_escaping, re_single_line_field, \
60                     re_multi_line_field, re_srchasver, re_taint_free, \
61                     re_re_mark, re_whitespace_comment, re_issource, \
62                     re_build_dep_arch, re_parse_maintainer
63
64 from formats import parse_format, validate_changes_format
65 from srcformats import get_format_from_string
66 from collections import defaultdict
67
68 ################################################################################
69
70 default_config = "/etc/dak/dak.conf"     #: default dak config, defines host properties
71
72 alias_cache = None        #: Cache for email alias checks
73 key_uid_email_cache = {}  #: Cache for email addresses from gpg key uids
74
75 # Monkeypatch commands.getstatusoutput as it may not return the correct exit
76 # code in lenny's Python. This also affects commands.getoutput and
77 # commands.getstatus.
78 def dak_getstatusoutput(cmd):
79     pipe = daklib.daksubprocess.Popen(cmd, shell=True, universal_newlines=True,
80         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
81
82     output = pipe.stdout.read()
83
84     pipe.wait()
85
86     if output[-1:] == '\n':
87         output = output[:-1]
88
89     ret = pipe.wait()
90     if ret is None:
91         ret = 0
92
93     return ret, output
94 commands.getstatusoutput = dak_getstatusoutput
95
96 ################################################################################
97
98 def html_escape(s):
99     """ Escape html chars """
100     return re_html_escaping.sub(lambda x: html_escaping.get(x.group(0)), s)
101
102 ################################################################################
103
104 def open_file(filename, mode='r'):
105     """
106     Open C{file}, return fileobject.
107
108     @type filename: string
109     @param filename: path/filename to open
110
111     @type mode: string
112     @param mode: open mode
113
114     @rtype: fileobject
115     @return: open fileobject
116
117     @raise CantOpenError: If IOError is raised by open, reraise it as CantOpenError.
118
119     """
120     try:
121         f = open(filename, mode)
122     except IOError:
123         raise CantOpenError(filename)
124     return f
125
126 ################################################################################
127
128 def our_raw_input(prompt=""):
129     if prompt:
130         while 1:
131             try:
132                 sys.stdout.write(prompt)
133                 break
134             except IOError:
135                 pass
136     sys.stdout.flush()
137     try:
138         ret = raw_input()
139         return ret
140     except EOFError:
141         sys.stderr.write("\nUser interrupt (^D).\n")
142         raise SystemExit
143
144 ################################################################################
145
146 def extract_component_from_section(section, session=None):
147     component = ""
148
149     if section.find('/') != -1:
150         component = section.split('/')[0]
151
152     # Expand default component
153     if component == "":
154         component = "main"
155
156     return (section, component)
157
158 ################################################################################
159
160 def parse_deb822(armored_contents, signing_rules=0, keyrings=None, session=None):
161     require_signature = True
162     if keyrings == None:
163         keyrings = []
164         require_signature = False
165
166     signed_file = SignedFile(armored_contents, keyrings=keyrings, require_signature=require_signature)
167     contents = signed_file.contents
168
169     error = ""
170     changes = {}
171
172     # Split the lines in the input, keeping the linebreaks.
173     lines = contents.splitlines(True)
174
175     if len(lines) == 0:
176         raise ParseChangesError("[Empty changes file]")
177
178     # Reindex by line number so we can easily verify the format of
179     # .dsc files...
180     index = 0
181     indexed_lines = {}
182     for line in lines:
183         index += 1
184         indexed_lines[index] = line[:-1]
185
186     num_of_lines = len(indexed_lines.keys())
187     index = 0
188     first = -1
189     while index < num_of_lines:
190         index += 1
191         line = indexed_lines[index]
192         if line == "" and signing_rules == 1:
193             if index != num_of_lines:
194                 raise InvalidDscError(index)
195             break
196         slf = re_single_line_field.match(line)
197         if slf:
198             field = slf.groups()[0].lower()
199             changes[field] = slf.groups()[1]
200             first = 1
201             continue
202         if line == " .":
203             changes[field] += '\n'
204             continue
205         mlf = re_multi_line_field.match(line)
206         if mlf:
207             if first == -1:
208                 raise ParseChangesError("'%s'\n [Multi-line field continuing on from nothing?]" % (line))
209             if first == 1 and changes[field] != "":
210                 changes[field] += '\n'
211             first = 0
212             changes[field] += mlf.groups()[0] + '\n'
213             continue
214         error += line
215
216     changes["filecontents"] = armored_contents
217
218     if changes.has_key("source"):
219         # Strip the source version in brackets from the source field,
220         # put it in the "source-version" field instead.
221         srcver = re_srchasver.search(changes["source"])
222         if srcver:
223             changes["source"] = srcver.group(1)
224             changes["source-version"] = srcver.group(2)
225
226     if error:
227         raise ParseChangesError(error)
228
229     return changes
230
231 ################################################################################
232
233 def parse_changes(filename, signing_rules=0, dsc_file=0, keyrings=None):
234     """
235     Parses a changes file and returns a dictionary where each field is a
236     key.  The mandatory first argument is the filename of the .changes
237     file.
238
239     signing_rules is an optional argument:
240
241       - If signing_rules == -1, no signature is required.
242       - If signing_rules == 0 (the default), a signature is required.
243       - If signing_rules == 1, it turns on the same strict format checking
244         as dpkg-source.
245
246     The rules for (signing_rules == 1)-mode are:
247
248       - The PGP header consists of "-----BEGIN PGP SIGNED MESSAGE-----"
249         followed by any PGP header data and must end with a blank line.
250
251       - The data section must end with a blank line and must be followed by
252         "-----BEGIN PGP SIGNATURE-----".
253     """
254
255     with open_file(filename) as changes_in:
256         content = changes_in.read()
257     try:
258         unicode(content, 'utf-8')
259     except UnicodeError:
260         raise ChangesUnicodeError("Changes file not proper utf-8")
261     changes = parse_deb822(content, signing_rules, keyrings=keyrings)
262
263
264     if not dsc_file:
265         # Finally ensure that everything needed for .changes is there
266         must_keywords = ('Format', 'Date', 'Source', 'Binary', 'Architecture', 'Version',
267                          'Distribution', 'Maintainer', 'Description', 'Changes', 'Files')
268
269         missingfields=[]
270         for keyword in must_keywords:
271             if not changes.has_key(keyword.lower()):
272                 missingfields.append(keyword)
273
274                 if len(missingfields):
275                     raise ParseChangesError("Missing mandatory field(s) in changes file (policy 5.5): %s" % (missingfields))
276
277     return changes
278
279 ################################################################################
280
281 def hash_key(hashname):
282     return '%ssum' % hashname
283
284 ################################################################################
285
286 def check_dsc_files(dsc_filename, dsc, dsc_files):
287     """
288     Verify that the files listed in the Files field of the .dsc are
289     those expected given the announced Format.
290
291     @type dsc_filename: string
292     @param dsc_filename: path of .dsc file
293
294     @type dsc: dict
295     @param dsc: the content of the .dsc parsed by C{parse_changes()}
296
297     @type dsc_files: dict
298     @param dsc_files: the file list returned by C{build_file_list()}
299
300     @rtype: list
301     @return: all errors detected
302     """
303     rejmsg = []
304
305     # Ensure .dsc lists proper set of source files according to the format
306     # announced
307     has = defaultdict(lambda: 0)
308
309     ftype_lookup = (
310         (r'orig\.tar\.gz',             ('orig_tar_gz', 'orig_tar')),
311         (r'diff\.gz',                  ('debian_diff',)),
312         (r'tar\.gz',                   ('native_tar_gz', 'native_tar')),
313         (r'debian\.tar\.(gz|bz2|xz)',  ('debian_tar',)),
314         (r'orig\.tar\.(gz|bz2|xz)\.asc', ('orig_tar_sig',)),
315         (r'orig\.tar\.(gz|bz2|xz)',    ('orig_tar',)),
316         (r'tar\.(gz|bz2|xz)',          ('native_tar',)),
317         (r'orig-.+\.tar\.(gz|bz2|xz)\.asc', ('more_orig_tar_sig',)),
318         (r'orig-.+\.tar\.(gz|bz2|xz)', ('more_orig_tar',)),
319     )
320
321     for f in dsc_files:
322         m = re_issource.match(f)
323         if not m:
324             rejmsg.append("%s: %s in Files field not recognised as source."
325                           % (dsc_filename, f))
326             continue
327
328         # Populate 'has' dictionary by resolving keys in lookup table
329         matched = False
330         for regex, keys in ftype_lookup:
331             if re.match(regex, m.group(3)):
332                 matched = True
333                 for key in keys:
334                     has[key] += 1
335                 break
336
337         # File does not match anything in lookup table; reject
338         if not matched:
339             rejmsg.append("%s: unexpected source file '%s'" % (dsc_filename, f))
340             break
341
342     # Check for multiple files
343     for file_type in ('orig_tar', 'orig_tar_sig', 'native_tar', 'debian_tar', 'debian_diff'):
344         if has[file_type] > 1:
345             rejmsg.append("%s: lists multiple %s" % (dsc_filename, file_type))
346
347     # Source format specific tests
348     try:
349         format = get_format_from_string(dsc['format'])
350         rejmsg.extend([
351             '%s: %s' % (dsc_filename, x) for x in format.reject_msgs(has)
352         ])
353
354     except UnknownFormatError:
355         # Not an error here for now
356         pass
357
358     return rejmsg
359
360 ################################################################################
361
362 # Dropped support for 1.4 and ``buggy dchanges 3.4'' (?!) compared to di.pl
363
364 def build_file_list(changes, is_a_dsc=0, field="files", hashname="md5sum"):
365     files = {}
366
367     # Make sure we have a Files: field to parse...
368     if not changes.has_key(field):
369         raise NoFilesFieldError
370
371     # Validate .changes Format: field
372     if not is_a_dsc:
373         validate_changes_format(parse_format(changes['format']), field)
374
375     includes_section = (not is_a_dsc) and field == "files"
376
377     # Parse each entry/line:
378     for i in changes[field].split('\n'):
379         if not i:
380             break
381         s = i.split()
382         section = priority = ""
383         try:
384             if includes_section:
385                 (md5, size, section, priority, name) = s
386             else:
387                 (md5, size, name) = s
388         except ValueError:
389             raise ParseChangesError(i)
390
391         if section == "":
392             section = "-"
393         if priority == "":
394             priority = "-"
395
396         (section, component) = extract_component_from_section(section)
397
398         files[name] = dict(size=size, section=section,
399                            priority=priority, component=component)
400         files[name][hashname] = md5
401
402     return files
403
404 ################################################################################
405
406 def send_mail (message, filename="", whitelists=None):
407     """sendmail wrapper, takes _either_ a message string or a file as arguments
408
409     @type  whitelists: list of (str or None)
410     @param whitelists: path to whitelists. C{None} or an empty list whitelists
411                        everything, otherwise an address is whitelisted if it is
412                        included in any of the lists.
413                        In addition a global whitelist can be specified in
414                        Dinstall::MailWhiteList.
415     """
416
417     maildir = Cnf.get('Dir::Mail')
418     if maildir:
419         path = os.path.join(maildir, datetime.datetime.now().isoformat())
420         path = find_next_free(path)
421         with open(path, 'w') as fh:
422             print >>fh, message,
423
424     # Check whether we're supposed to be sending mail
425     if Cnf.has_key("Dinstall::Options::No-Mail") and Cnf["Dinstall::Options::No-Mail"]:
426         return
427
428     # If we've been passed a string dump it into a temporary file
429     if message:
430         (fd, filename) = tempfile.mkstemp()
431         os.write (fd, message)
432         os.close (fd)
433
434     if whitelists is None or None in whitelists:
435         whitelists = []
436     if Cnf.get('Dinstall::MailWhiteList', ''):
437         whitelists.append(Cnf['Dinstall::MailWhiteList'])
438     if len(whitelists) != 0:
439         with open_file(filename) as message_in:
440             message_raw = modemail.message_from_file(message_in)
441
442         whitelist = [];
443         for path in whitelists:
444           with open_file(path, 'r') as whitelist_in:
445             for line in whitelist_in:
446                 if not re_whitespace_comment.match(line):
447                     if re_re_mark.match(line):
448                         whitelist.append(re.compile(re_re_mark.sub("", line.strip(), 1)))
449                     else:
450                         whitelist.append(re.compile(re.escape(line.strip())))
451
452         # Fields to check.
453         fields = ["To", "Bcc", "Cc"]
454         for field in fields:
455             # Check each field
456             value = message_raw.get(field, None)
457             if value != None:
458                 match = [];
459                 for item in value.split(","):
460                     (rfc822_maint, rfc2047_maint, name, email) = fix_maintainer(item.strip())
461                     mail_whitelisted = 0
462                     for wr in whitelist:
463                         if wr.match(email):
464                             mail_whitelisted = 1
465                             break
466                     if not mail_whitelisted:
467                         print "Skipping {0} since it's not whitelisted".format(item)
468                         continue
469                     match.append(item)
470
471                 # Doesn't have any mail in whitelist so remove the header
472                 if len(match) == 0:
473                     del message_raw[field]
474                 else:
475                     message_raw.replace_header(field, ', '.join(match))
476
477         # Change message fields in order if we don't have a To header
478         if not message_raw.has_key("To"):
479             fields.reverse()
480             for field in fields:
481                 if message_raw.has_key(field):
482                     message_raw[fields[-1]] = message_raw[field]
483                     del message_raw[field]
484                     break
485             else:
486                 # Clean up any temporary files
487                 # and return, as we removed all recipients.
488                 if message:
489                     os.unlink (filename);
490                 return;
491
492         fd = os.open(filename, os.O_RDWR|os.O_EXCL, 0o700);
493         os.write (fd, message_raw.as_string(True));
494         os.close (fd);
495
496     # Invoke sendmail
497     (result, output) = commands.getstatusoutput("%s < %s" % (Cnf["Dinstall::SendmailCommand"], filename))
498     if (result != 0):
499         raise SendmailFailedError(output)
500
501     # Clean up any temporary files
502     if message:
503         os.unlink (filename)
504
505 ################################################################################
506
507 def poolify (source, component=None):
508     if source[:3] == "lib":
509         return source[:4] + '/' + source + '/'
510     else:
511         return source[:1] + '/' + source + '/'
512
513 ################################################################################
514
515 def move (src, dest, overwrite = 0, perms = 0o664):
516     if os.path.exists(dest) and os.path.isdir(dest):
517         dest_dir = dest
518     else:
519         dest_dir = os.path.dirname(dest)
520     if not os.path.lexists(dest_dir):
521         umask = os.umask(00000)
522         os.makedirs(dest_dir, 0o2775)
523         os.umask(umask)
524     #print "Moving %s to %s..." % (src, dest)
525     if os.path.exists(dest) and os.path.isdir(dest):
526         dest += '/' + os.path.basename(src)
527     # Don't overwrite unless forced to
528     if os.path.lexists(dest):
529         if not overwrite:
530             fubar("Can't move %s to %s - file already exists." % (src, dest))
531         else:
532             if not os.access(dest, os.W_OK):
533                 fubar("Can't move %s to %s - can't write to existing file." % (src, dest))
534     shutil.copy2(src, dest)
535     os.chmod(dest, perms)
536     os.unlink(src)
537
538 def copy (src, dest, overwrite = 0, perms = 0o664):
539     if os.path.exists(dest) and os.path.isdir(dest):
540         dest_dir = dest
541     else:
542         dest_dir = os.path.dirname(dest)
543     if not os.path.exists(dest_dir):
544         umask = os.umask(00000)
545         os.makedirs(dest_dir, 0o2775)
546         os.umask(umask)
547     #print "Copying %s to %s..." % (src, dest)
548     if os.path.exists(dest) and os.path.isdir(dest):
549         dest += '/' + os.path.basename(src)
550     # Don't overwrite unless forced to
551     if os.path.lexists(dest):
552         if not overwrite:
553             raise FileExistsError
554         else:
555             if not os.access(dest, os.W_OK):
556                 raise CantOverwriteError
557     shutil.copy2(src, dest)
558     os.chmod(dest, perms)
559
560 ################################################################################
561
562 def which_conf_file ():
563     if os.getenv('DAK_CONFIG'):
564         return os.getenv('DAK_CONFIG')
565
566     res = socket.getfqdn()
567     # In case we allow local config files per user, try if one exists
568     if Cnf.find_b("Config::" + res + "::AllowLocalConfig"):
569         homedir = os.getenv("HOME")
570         confpath = os.path.join(homedir, "/etc/dak.conf")
571         if os.path.exists(confpath):
572             apt_pkg.read_config_file_isc(Cnf,confpath)
573
574     # We are still in here, so there is no local config file or we do
575     # not allow local files. Do the normal stuff.
576     if Cnf.get("Config::" + res + "::DakConfig"):
577         return Cnf["Config::" + res + "::DakConfig"]
578
579     return default_config
580
581 ################################################################################
582
583 def TemplateSubst(subst_map, filename):
584     """ Perform a substition of template """
585     with open_file(filename) as templatefile:
586         template = templatefile.read()
587     for k, v in subst_map.iteritems():
588         template = template.replace(k, str(v))
589     return template
590
591 ################################################################################
592
593 def fubar(msg, exit_code=1):
594     sys.stderr.write("E: %s\n" % (msg))
595     sys.exit(exit_code)
596
597 def warn(msg):
598     sys.stderr.write("W: %s\n" % (msg))
599
600 ################################################################################
601
602 # Returns the user name with a laughable attempt at rfc822 conformancy
603 # (read: removing stray periods).
604 def whoami ():
605     return pwd.getpwuid(os.getuid())[4].split(',')[0].replace('.', '')
606
607 def getusername ():
608     return pwd.getpwuid(os.getuid())[0]
609
610 ################################################################################
611
612 def size_type (c):
613     t  = " B"
614     if c > 10240:
615         c = c / 1024
616         t = " KB"
617     if c > 10240:
618         c = c / 1024
619         t = " MB"
620     return ("%d%s" % (c, t))
621
622 ################################################################################
623
624 def find_next_free (dest, too_many=100):
625     extra = 0
626     orig_dest = dest
627     while os.path.lexists(dest) and extra < too_many:
628         dest = orig_dest + '.' + repr(extra)
629         extra += 1
630     if extra >= too_many:
631         raise NoFreeFilenameError
632     return dest
633
634 ################################################################################
635
636 def result_join (original, sep = '\t'):
637     resultlist = []
638     for i in xrange(len(original)):
639         if original[i] == None:
640             resultlist.append("")
641         else:
642             resultlist.append(original[i])
643     return sep.join(resultlist)
644
645 ################################################################################
646
647 def prefix_multi_line_string(str, prefix, include_blank_lines=0):
648     out = ""
649     for line in str.split('\n'):
650         line = line.strip()
651         if line or include_blank_lines:
652             out += "%s%s\n" % (prefix, line)
653     # Strip trailing new line
654     if out:
655         out = out[:-1]
656     return out
657
658 ################################################################################
659
660 def join_with_commas_and(list):
661     if len(list) == 0: return "nothing"
662     if len(list) == 1: return list[0]
663     return ", ".join(list[:-1]) + " and " + list[-1]
664
665 ################################################################################
666
667 def pp_deps (deps):
668     pp_deps = []
669     for atom in deps:
670         (pkg, version, constraint) = atom
671         if constraint:
672             pp_dep = "%s (%s %s)" % (pkg, constraint, version)
673         else:
674             pp_dep = pkg
675         pp_deps.append(pp_dep)
676     return " |".join(pp_deps)
677
678 ################################################################################
679
680 def get_conf():
681     return Cnf
682
683 ################################################################################
684
685 def parse_args(Options):
686     """ Handle -a, -c and -s arguments; returns them as SQL constraints """
687     # XXX: This should go away and everything which calls it be converted
688     #      to use SQLA properly.  For now, we'll just fix it not to use
689     #      the old Pg interface though
690     session = DBConn().session()
691     # Process suite
692     if Options["Suite"]:
693         suite_ids_list = []
694         for suitename in split_args(Options["Suite"]):
695             suite = get_suite(suitename, session=session)
696             if not suite or suite.suite_id is None:
697                 warn("suite '%s' not recognised." % (suite and suite.suite_name or suitename))
698             else:
699                 suite_ids_list.append(suite.suite_id)
700         if suite_ids_list:
701             con_suites = "AND su.id IN (%s)" % ", ".join([ str(i) for i in suite_ids_list ])
702         else:
703             fubar("No valid suite given.")
704     else:
705         con_suites = ""
706
707     # Process component
708     if Options["Component"]:
709         component_ids_list = []
710         for componentname in split_args(Options["Component"]):
711             component = get_component(componentname, session=session)
712             if component is None:
713                 warn("component '%s' not recognised." % (componentname))
714             else:
715                 component_ids_list.append(component.component_id)
716         if component_ids_list:
717             con_components = "AND c.id IN (%s)" % ", ".join([ str(i) for i in component_ids_list ])
718         else:
719             fubar("No valid component given.")
720     else:
721         con_components = ""
722
723     # Process architecture
724     con_architectures = ""
725     check_source = 0
726     if Options["Architecture"]:
727         arch_ids_list = []
728         for archname in split_args(Options["Architecture"]):
729             if archname == "source":
730                 check_source = 1
731             else:
732                 arch = get_architecture(archname, session=session)
733                 if arch is None:
734                     warn("architecture '%s' not recognised." % (archname))
735                 else:
736                     arch_ids_list.append(arch.arch_id)
737         if arch_ids_list:
738             con_architectures = "AND a.id IN (%s)" % ", ".join([ str(i) for i in arch_ids_list ])
739         else:
740             if not check_source:
741                 fubar("No valid architecture given.")
742     else:
743         check_source = 1
744
745     return (con_suites, con_architectures, con_components, check_source)
746
747 ################################################################################
748
749 def arch_compare_sw (a, b):
750     """
751     Function for use in sorting lists of architectures.
752
753     Sorts normally except that 'source' dominates all others.
754     """
755
756     if a == "source" and b == "source":
757         return 0
758     elif a == "source":
759         return -1
760     elif b == "source":
761         return 1
762
763     return cmp (a, b)
764
765 ################################################################################
766
767 def split_args (s, dwim=True):
768     """
769     Split command line arguments which can be separated by either commas
770     or whitespace.  If dwim is set, it will complain about string ending
771     in comma since this usually means someone did 'dak ls -a i386, m68k
772     foo' or something and the inevitable confusion resulting from 'm68k'
773     being treated as an argument is undesirable.
774     """
775
776     if s.find(",") == -1:
777         return s.split()
778     else:
779         if s[-1:] == "," and dwim:
780             fubar("split_args: found trailing comma, spurious space maybe?")
781         return s.split(",")
782
783 ################################################################################
784
785 def gpg_keyring_args(keyrings=None):
786     if not keyrings:
787         keyrings = get_active_keyring_paths()
788
789     return " ".join(["--keyring %s" % x for x in keyrings])
790
791 ################################################################################
792
793 def gpg_get_key_addresses(fingerprint):
794     """retreive email addresses from gpg key uids for a given fingerprint"""
795     addresses = key_uid_email_cache.get(fingerprint)
796     if addresses != None:
797         return addresses
798     addresses = list()
799     try:
800         with open(os.devnull, "wb") as devnull:
801             output = daklib.daksubprocess.check_output(
802                 ["gpg", "--no-default-keyring"] + gpg_keyring_args().split() +
803                 ["--with-colons", "--list-keys", fingerprint], stderr=devnull)
804     except subprocess.CalledProcessError:
805         pass
806     else:
807         for l in output.split('\n'):
808             parts = l.split(':')
809             if parts[0] not in ("uid", "pub"):
810                 continue
811             try:
812                 uid = parts[9]
813             except IndexError:
814                 continue
815             try:
816                 # Do not use unicode_escape, because it is locale-specific
817                 uid = codecs.decode(uid, "string_escape").decode("utf-8")
818             except UnicodeDecodeError:
819                 uid = uid.decode("latin1") # does not fail
820             m = re_parse_maintainer.match(uid)
821             if not m:
822                 continue
823             address = m.group(2)
824             address = address.encode("utf8") # dak still uses bytes
825             if address.endswith('@debian.org'):
826                 # prefer @debian.org addresses
827                 # TODO: maybe not hardcode the domain
828                 addresses.insert(0, address)
829             else:
830                 addresses.append(address)
831     key_uid_email_cache[fingerprint] = addresses
832     return addresses
833
834 ################################################################################
835
836 def get_logins_from_ldap(fingerprint='*'):
837     """retrieve login from LDAP linked to a given fingerprint"""
838
839     LDAPDn = Cnf['Import-LDAP-Fingerprints::LDAPDn']
840     LDAPServer = Cnf['Import-LDAP-Fingerprints::LDAPServer']
841     l = ldap.open(LDAPServer)
842     l.simple_bind_s('','')
843     Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
844                        '(keyfingerprint=%s)' % fingerprint,
845                        ['uid', 'keyfingerprint'])
846     login = {}
847     for elem in Attrs:
848         login[elem[1]['keyFingerPrint'][0]] = elem[1]['uid'][0]
849     return login
850
851 ################################################################################
852
853 def get_users_from_ldap():
854     """retrieve login and user names from LDAP"""
855
856     LDAPDn = Cnf['Import-LDAP-Fingerprints::LDAPDn']
857     LDAPServer = Cnf['Import-LDAP-Fingerprints::LDAPServer']
858     l = ldap.open(LDAPServer)
859     l.simple_bind_s('','')
860     Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
861                        '(uid=*)', ['uid', 'cn', 'mn', 'sn'])
862     users = {}
863     for elem in Attrs:
864         elem = elem[1]
865         name = []
866         for k in ('cn', 'mn', 'sn'):
867             try:
868                 if elem[k][0] != '-':
869                     name.append(elem[k][0])
870             except KeyError:
871                 pass
872         users[' '.join(name)] = elem['uid'][0]
873     return users
874
875 ################################################################################
876
877 def clean_symlink (src, dest, root):
878     """
879     Relativize an absolute symlink from 'src' -> 'dest' relative to 'root'.
880     Returns fixed 'src'
881     """
882     src = src.replace(root, '', 1)
883     dest = dest.replace(root, '', 1)
884     dest = os.path.dirname(dest)
885     new_src = '../' * len(dest.split('/'))
886     return new_src + src
887
888 ################################################################################
889
890 def temp_filename(directory=None, prefix="dak", suffix="", mode=None, group=None):
891     """
892     Return a secure and unique filename by pre-creating it.
893
894     @type directory: str
895     @param directory: If non-null it will be the directory the file is pre-created in.
896
897     @type prefix: str
898     @param prefix: The filename will be prefixed with this string
899
900     @type suffix: str
901     @param suffix: The filename will end with this string
902
903     @type mode: str
904     @param mode: If set the file will get chmodded to those permissions
905
906     @type group: str
907     @param group: If set the file will get chgrped to the specified group.
908
909     @rtype: list
910     @return: Returns a pair (fd, name)
911     """
912
913     (tfd, tfname) = tempfile.mkstemp(suffix, prefix, directory)
914     if mode:
915         os.chmod(tfname, mode)
916     if group:
917         gid = grp.getgrnam(group).gr_gid
918         os.chown(tfname, -1, gid)
919     return (tfd, tfname)
920
921 ################################################################################
922
923 def temp_dirname(parent=None, prefix="dak", suffix="", mode=None, group=None):
924     """
925     Return a secure and unique directory by pre-creating it.
926
927     @type parent: str
928     @param parent: If non-null it will be the directory the directory is pre-created in.
929
930     @type prefix: str
931     @param prefix: The filename will be prefixed with this string
932
933     @type suffix: str
934     @param suffix: The filename will end with this string
935
936     @type mode: str
937     @param mode: If set the file will get chmodded to those permissions
938
939     @type group: str
940     @param group: If set the file will get chgrped to the specified group.
941
942     @rtype: list
943     @return: Returns a pair (fd, name)
944
945     """
946
947     tfname = tempfile.mkdtemp(suffix, prefix, parent)
948     if mode:
949         os.chmod(tfname, mode)
950     if group:
951         gid = grp.getgrnam(group).gr_gid
952         os.chown(tfname, -1, gid)
953     return tfname
954
955 ################################################################################
956
957 def is_email_alias(email):
958     """ checks if the user part of the email is listed in the alias file """
959     global alias_cache
960     if alias_cache == None:
961         aliasfn = which_alias_file()
962         alias_cache = set()
963         if aliasfn:
964             for l in open(aliasfn):
965                 alias_cache.add(l.split(':')[0])
966     uid = email.split('@')[0]
967     return uid in alias_cache
968
969 ################################################################################
970
971 def get_changes_files(from_dir):
972     """
973     Takes a directory and lists all .changes files in it (as well as chdir'ing
974     to the directory; this is due to broken behaviour on the part of p-u/p-a
975     when you're not in the right place)
976
977     Returns a list of filenames
978     """
979     try:
980         # Much of the rest of p-u/p-a depends on being in the right place
981         os.chdir(from_dir)
982         changes_files = [x for x in os.listdir(from_dir) if x.endswith('.changes')]
983     except OSError as e:
984         fubar("Failed to read list from directory %s (%s)" % (from_dir, e))
985
986     return changes_files
987
988 ################################################################################
989
990 Cnf = config.Config().Cnf
991
992 ################################################################################
993
994 def parse_wnpp_bug_file(file = "/srv/ftp-master.debian.org/scripts/masterfiles/wnpp_rm"):
995     """
996     Parses the wnpp bug list available at https://qa.debian.org/data/bts/wnpp_rm
997     Well, actually it parsed a local copy, but let's document the source
998     somewhere ;)
999
1000     returns a dict associating source package name with a list of open wnpp
1001     bugs (Yes, there might be more than one)
1002     """
1003
1004     line = []
1005     try:
1006         f = open(file)
1007         lines = f.readlines()
1008     except IOError as e:
1009         print "Warning:  Couldn't open %s; don't know about WNPP bugs, so won't close any." % file
1010         lines = []
1011     wnpp = {}
1012
1013     for line in lines:
1014         splited_line = line.split(": ", 1)
1015         if len(splited_line) > 1:
1016             wnpp[splited_line[0]] = splited_line[1].split("|")
1017
1018     for source in wnpp.keys():
1019         bugs = []
1020         for wnpp_bug in wnpp[source]:
1021             bug_no = re.search("(\d)+", wnpp_bug).group()
1022             if bug_no:
1023                 bugs.append(bug_no)
1024         wnpp[source] = bugs
1025     return wnpp
1026
1027 ################################################################################
1028
1029 def get_packages_from_ftp(root, suite, component, architecture):
1030     """
1031     Returns an object containing apt_pkg-parseable data collected by
1032     aggregating Packages.gz files gathered for each architecture.
1033
1034     @type root: string
1035     @param root: path to ftp archive root directory
1036
1037     @type suite: string
1038     @param suite: suite to extract files from
1039
1040     @type component: string
1041     @param component: component to extract files from
1042
1043     @type architecture: string
1044     @param architecture: architecture to extract files from
1045
1046     @rtype: TagFile
1047     @return: apt_pkg class containing package data
1048     """
1049     filename = "%s/dists/%s/%s/binary-%s/Packages.gz" % (root, suite, component, architecture)
1050     (fd, temp_file) = temp_filename()
1051     (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_file))
1052     if (result != 0):
1053         fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1054     filename = "%s/dists/%s/%s/debian-installer/binary-%s/Packages.gz" % (root, suite, component, architecture)
1055     if os.path.exists(filename):
1056         (result, output) = commands.getstatusoutput("gunzip -c %s >> %s" % (filename, temp_file))
1057         if (result != 0):
1058             fubar("Gunzip invocation failed!\n%s\n" % (output), result)
1059     packages = open_file(temp_file)
1060     Packages = apt_pkg.TagFile(packages)
1061     os.unlink(temp_file)
1062     return Packages
1063
1064 ################################################################################
1065
1066 def deb_extract_control(fh):
1067     """extract DEBIAN/control from a binary package"""
1068     return apt_inst.DebFile(fh).control.extractdata("control")
1069
1070 ################################################################################
1071
1072 def mail_addresses_for_upload(maintainer, changed_by, fingerprint):
1073     """mail addresses to contact for an upload
1074
1075     @type  maintainer: str
1076     @param maintainer: Maintainer field of the .changes file
1077
1078     @type  changed_by: str
1079     @param changed_by: Changed-By field of the .changes file
1080
1081     @type  fingerprint: str
1082     @param fingerprint: fingerprint of the key used to sign the upload
1083
1084     @rtype:  list of str
1085     @return: list of RFC 2047-encoded mail addresses to contact regarding
1086              this upload
1087     """
1088     addresses = [maintainer]
1089     if changed_by != maintainer:
1090         addresses.append(changed_by)
1091
1092     fpr_addresses = gpg_get_key_addresses(fingerprint)
1093     if len(fpr_addresses) > 0 and fix_maintainer(changed_by)[3] not in fpr_addresses and fix_maintainer(maintainer)[3] not in fpr_addresses:
1094         addresses.append(fpr_addresses[0])
1095
1096     encoded_addresses = [ fix_maintainer(e)[1] for e in addresses ]
1097     return encoded_addresses
1098
1099 ################################################################################
1100
1101 def call_editor(text="", suffix=".txt"):
1102     """run editor and return the result as a string
1103
1104     @type  text: str
1105     @param text: initial text
1106
1107     @type  suffix: str
1108     @param suffix: extension for temporary file
1109
1110     @rtype:  str
1111     @return: string with the edited text
1112     """
1113     editor = os.environ.get('VISUAL', os.environ.get('EDITOR', 'vi'))
1114     tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
1115     try:
1116         print >>tmp, text,
1117         tmp.close()
1118         daklib.daksubprocess.check_call([editor, tmp.name])
1119         return open(tmp.name, 'r').read()
1120     finally:
1121         os.unlink(tmp.name)
1122
1123 ################################################################################
1124
1125 def check_reverse_depends(removals, suite, arches=None, session=None, cruft=False, quiet=False, include_arch_all=True):
1126     dbsuite = get_suite(suite, session)
1127     overridesuite = dbsuite
1128     if dbsuite.overridesuite is not None:
1129         overridesuite = get_suite(dbsuite.overridesuite, session)
1130     dep_problem = 0
1131     p2c = {}
1132     all_broken = defaultdict(lambda: defaultdict(set))
1133     if arches:
1134         all_arches = set(arches)
1135     else:
1136         all_arches = set(x.arch_string for x in get_suite_architectures(suite))
1137     all_arches -= set(["source", "all"])
1138     removal_set = set(removals)
1139     metakey_d = get_or_set_metadatakey("Depends", session)
1140     metakey_p = get_or_set_metadatakey("Provides", session)
1141     params = {
1142         'suite_id':     dbsuite.suite_id,
1143         'metakey_d_id': metakey_d.key_id,
1144         'metakey_p_id': metakey_p.key_id,
1145     }
1146     if include_arch_all:
1147         rdep_architectures = all_arches | set(['all'])
1148     else:
1149         rdep_architectures = all_arches
1150     for architecture in rdep_architectures:
1151         deps = {}
1152         sources = {}
1153         virtual_packages = {}
1154         params['arch_id'] = get_architecture(architecture, session).arch_id
1155
1156         statement = '''
1157             SELECT b.package, s.source, c.name as component,
1158                 (SELECT bmd.value FROM binaries_metadata bmd WHERE bmd.bin_id = b.id AND bmd.key_id = :metakey_d_id) AS depends,
1159                 (SELECT bmp.value FROM binaries_metadata bmp WHERE bmp.bin_id = b.id AND bmp.key_id = :metakey_p_id) AS provides
1160                 FROM binaries b
1161                 JOIN bin_associations ba ON b.id = ba.bin AND ba.suite = :suite_id
1162                 JOIN source s ON b.source = s.id
1163                 JOIN files_archive_map af ON b.file = af.file_id
1164                 JOIN component c ON af.component_id = c.id
1165                 WHERE b.architecture = :arch_id'''
1166         query = session.query('package', 'source', 'component', 'depends', 'provides'). \
1167             from_statement(statement).params(params)
1168         for package, source, component, depends, provides in query:
1169             sources[package] = source
1170             p2c[package] = component
1171             if depends is not None:
1172                 deps[package] = depends
1173             # Maintain a counter for each virtual package.  If a
1174             # Provides: exists, set the counter to 0 and count all
1175             # provides by a package not in the list for removal.
1176             # If the counter stays 0 at the end, we know that only
1177             # the to-be-removed packages provided this virtual
1178             # package.
1179             if provides is not None:
1180                 for virtual_pkg in provides.split(","):
1181                     virtual_pkg = virtual_pkg.strip()
1182                     if virtual_pkg == package: continue
1183                     if not virtual_packages.has_key(virtual_pkg):
1184                         virtual_packages[virtual_pkg] = 0
1185                     if package not in removals:
1186                         virtual_packages[virtual_pkg] += 1
1187
1188         # If a virtual package is only provided by the to-be-removed
1189         # packages, treat the virtual package as to-be-removed too.
1190         removal_set.update(virtual_pkg for virtual_pkg in virtual_packages if not virtual_packages[virtual_pkg])
1191
1192         # Check binary dependencies (Depends)
1193         for package in deps:
1194             if package in removals: continue
1195             try:
1196                 parsed_dep = apt_pkg.parse_depends(deps[package])
1197             except ValueError as e:
1198                 print "Error for package %s: %s" % (package, e)
1199                 parsed_dep = []
1200             for dep in parsed_dep:
1201                 # Check for partial breakage.  If a package has a ORed
1202                 # dependency, there is only a dependency problem if all
1203                 # packages in the ORed depends will be removed.
1204                 unsat = 0
1205                 for dep_package, _, _ in dep:
1206                     if dep_package in removals:
1207                         unsat += 1
1208                 if unsat == len(dep):
1209                     component = p2c[package]
1210                     source = sources[package]
1211                     if component != "main":
1212                         source = "%s/%s" % (source, component)
1213                     all_broken[source][package].add(architecture)
1214                     dep_problem = 1
1215
1216     if all_broken and not quiet:
1217         if cruft:
1218             print "  - broken Depends:"
1219         else:
1220             print "# Broken Depends:"
1221         for source, bindict in sorted(all_broken.items()):
1222             lines = []
1223             for binary, arches in sorted(bindict.items()):
1224                 if arches == all_arches or 'all' in arches:
1225                     lines.append(binary)
1226                 else:
1227                     lines.append('%s [%s]' % (binary, ' '.join(sorted(arches))))
1228             if cruft:
1229                 print '    %s: %s' % (source, lines[0])
1230             else:
1231                 print '%s: %s' % (source, lines[0])
1232             for line in lines[1:]:
1233                 if cruft:
1234                     print '    ' + ' ' * (len(source) + 2) + line
1235                 else:
1236                     print ' ' * (len(source) + 2) + line
1237         if not cruft:
1238             print
1239
1240     # Check source dependencies (Build-Depends and Build-Depends-Indep)
1241     all_broken = defaultdict(set)
1242     metakey_bd = get_or_set_metadatakey("Build-Depends", session)
1243     metakey_bdi = get_or_set_metadatakey("Build-Depends-Indep", session)
1244     if include_arch_all:
1245         metakey_ids = (metakey_bd.key_id, metakey_bdi.key_id)
1246     else:
1247         metakey_ids = (metakey_bd.key_id,)
1248
1249     params = {
1250         'suite_id':    dbsuite.suite_id,
1251         'metakey_ids': metakey_ids,
1252     }
1253     statement = '''
1254         SELECT s.source, string_agg(sm.value, ', ') as build_dep
1255            FROM source s
1256            JOIN source_metadata sm ON s.id = sm.src_id
1257            WHERE s.id in
1258                (SELECT src FROM newest_src_association
1259                    WHERE suite = :suite_id)
1260                AND sm.key_id in :metakey_ids
1261            GROUP BY s.id, s.source'''
1262     query = session.query('source', 'build_dep').from_statement(statement). \
1263         params(params)
1264     for source, build_dep in query:
1265         if source in removals: continue
1266         parsed_dep = []
1267         if build_dep is not None:
1268             # Remove [arch] information since we want to see breakage on all arches
1269             build_dep = re_build_dep_arch.sub("", build_dep)
1270             try:
1271                 parsed_dep = apt_pkg.parse_src_depends(build_dep)
1272             except ValueError as e:
1273                 print "Error for source %s: %s" % (source, e)
1274         for dep in parsed_dep:
1275             unsat = 0
1276             for dep_package, _, _ in dep:
1277                 if dep_package in removals:
1278                     unsat += 1
1279             if unsat == len(dep):
1280                 component, = session.query(Component.component_name) \
1281                     .join(Component.overrides) \
1282                     .filter(Override.suite == overridesuite) \
1283                     .filter(Override.package == re.sub('/(contrib|non-free)$', '', source)) \
1284                     .join(Override.overridetype).filter(OverrideType.overridetype == 'dsc') \
1285                     .first()
1286                 key = source
1287                 if component != "main":
1288                     key = "%s/%s" % (source, component)
1289                 all_broken[key].add(pp_deps(dep))
1290                 dep_problem = 1
1291
1292     if all_broken and not quiet:
1293         if cruft:
1294             print "  - broken Build-Depends:"
1295         else:
1296             print "# Broken Build-Depends:"
1297         for source, bdeps in sorted(all_broken.items()):
1298             bdeps = sorted(bdeps)
1299             if cruft:
1300                 print '    %s: %s' % (source, bdeps[0])
1301             else:
1302                 print '%s: %s' % (source, bdeps[0])
1303             for bdep in bdeps[1:]:
1304                 if cruft:
1305                     print '    ' + ' ' * (len(source) + 2) + bdep
1306                 else:
1307                     print ' ' * (len(source) + 2) + bdep
1308         if not cruft:
1309             print
1310
1311     return dep_problem
1312
1313 ################################################################################
1314
1315 def parse_built_using(control):
1316     """source packages referenced via Built-Using
1317
1318     @type  control: dict-like
1319     @param control: control file to take Built-Using field from
1320
1321     @rtype:  list of (str, str)
1322     @return: list of (source_name, source_version) pairs
1323     """
1324     built_using = control.get('Built-Using', None)
1325     if built_using is None:
1326         return []
1327
1328     bu = []
1329     for dep in apt_pkg.parse_depends(built_using):
1330         assert len(dep) == 1, 'Alternatives are not allowed in Built-Using field'
1331         source_name, source_version, comp = dep[0]
1332         assert comp == '=', 'Built-Using must contain strict dependencies'
1333         bu.append((source_name, source_version))
1334
1335     return bu
1336
1337 ################################################################################
1338
1339 def is_in_debug_section(control):
1340     """binary package is a debug package
1341
1342     @type  control: dict-like
1343     @param control: control file of binary package
1344
1345     @rtype Boolean
1346     @return: True if the binary package is a debug package
1347     """
1348     section = control['Section'].split('/', 1)[-1]
1349     auto_built_package = control.get("Auto-Built-Package")
1350     return section == "debug" and auto_built_package == "debug-symbols"