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