]> git.decadent.org.uk Git - dak.git/blob - dak/process_policy.py
Merge remote-tracking branch 'ansgar/pu/multiarchive-2'
[dak.git] / dak / process_policy.py
1 #!/usr/bin/env python
2 # vim:set et ts=4 sw=4:
3
4 """ Handles packages from policy queues
5
6 @contact: Debian FTP Master <ftpmaster@debian.org>
7 @copyright: 2001, 2002, 2003, 2004, 2005, 2006  James Troup <james@nocrew.org>
8 @copyright: 2009 Joerg Jaspert <joerg@debian.org>
9 @copyright: 2009 Frank Lichtenheld <djpig@debian.org>
10 @copyright: 2009 Mark Hymers <mhy@debian.org>
11 @license: GNU General Public License version 2 or later
12 """
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ################################################################################
28
29 # <mhy> So how do we handle that at the moment?
30 # <stew> Probably incorrectly.
31
32 ################################################################################
33
34 import os
35 import datetime
36 import re
37 import sys
38 import traceback
39 import apt_pkg
40
41 from daklib.dbconn import *
42 from daklib import daklog
43 from daklib import utils
44 from daklib.dak_exceptions import CantOpenError, AlreadyLockedError, CantGetLockError
45 from daklib.config import Config
46 from daklib.archive import ArchiveTransaction
47 from daklib.urgencylog import UrgencyLog
48 from daklib.textutils import fix_maintainer
49
50 # Globals
51 Options = None
52 Logger = None
53
54 ################################################################################
55
56 def do_comments(dir, srcqueue, opref, npref, line, fn, transaction):
57     session = transaction.session
58     for comm in [ x for x in os.listdir(dir) if x.startswith(opref) ]:
59         lines = open(os.path.join(dir, comm)).readlines()
60         if len(lines) == 0 or lines[0] != line + "\n": continue
61
62         # If the ACCEPT includes a _<arch> we only accept that .changes.
63         # Otherwise we accept all .changes that start with the given prefix
64         changes_prefix = comm[len(opref):]
65         if changes_prefix.count('_') < 2:
66             changes_prefix = changes_prefix + '_'
67         else:
68             changes_prefix = changes_prefix + '.changes'
69
70         uploads = session.query(PolicyQueueUpload).filter_by(policy_queue=srcqueue) \
71             .join(PolicyQueueUpload.changes).filter(DBChange.changesname.startswith(changes_prefix)) \
72             .order_by(PolicyQueueUpload.source_id)
73         for u in uploads:
74             print "Processing changes file: %s" % u.changes.changesname
75             fn(u, srcqueue, "".join(lines[1:]), transaction)
76
77         if opref != npref:
78             newcomm = npref + comm[len(opref):]
79             transaction.fs.move(os.path.join(dir, comm), os.path.join(dir, newcomm))
80
81 ################################################################################
82
83 def try_or_reject(function):
84     def wrapper(upload, srcqueue, comments, transaction):
85         try:
86             function(upload, srcqueue, comments, transaction)
87         except Exception as e:
88             comments = 'An exception was raised while processing the package:\n{0}\nOriginal comments:\n{1}'.format(traceback.format_exc(), comments)
89             try:
90                 transaction.rollback()
91                 real_comment_reject(upload, srcqueue, comments, transaction)
92             except Exception as e:
93                 comments = 'In addition an exception was raised while trying to reject the upload:\n{0}\nOriginal rejection:\n{1}'.format(traceback.format_exc(), comments)
94                 transaction.rollback()
95                 real_comment_reject(upload, srcqueue, comments, transaction, notify=False)
96         if not Options['No-Action']:
97             transaction.commit()
98     return wrapper
99
100 ################################################################################
101
102 @try_or_reject
103 def comment_accept(upload, srcqueue, comments, transaction):
104     for byhand in upload.byhand:
105         path = os.path.join(srcqueue.path, byhand.filename)
106         if os.path.exists(path):
107             raise Exception('E: cannot ACCEPT upload with unprocessed byhand file {0}'.format(byhand.filename))
108
109     cnf = Config()
110
111     fs = transaction.fs
112     session = transaction.session
113     changesname = upload.changes.changesname
114     allow_tainted = srcqueue.suite.archive.tainted
115
116     # We need overrides to get the target component
117     overridesuite = upload.target_suite
118     if overridesuite.overridesuite is not None:
119         overridesuite = session.query(Suite).filter_by(suite_name=overridesuite.overridesuite).one()
120
121     def binary_component_func(db_binary):
122         override = session.query(Override).filter_by(suite=overridesuite, package=db_binary.package) \
123             .join(OverrideType).filter(OverrideType.overridetype == db_binary.binarytype) \
124             .join(Component).one()
125         return override.component
126
127     def source_component_func(db_source):
128         override = session.query(Override).filter_by(suite=overridesuite, package=db_source.source) \
129             .join(OverrideType).filter(OverrideType.overridetype == 'dsc') \
130             .join(Component).one()
131         return override.component
132
133     all_target_suites = [upload.target_suite]
134     all_target_suites.extend([q.suite for q in upload.target_suite.copy_queues])
135
136     for suite in all_target_suites:
137         if upload.source is not None:
138             transaction.copy_source(upload.source, suite, source_component_func(upload.source), allow_tainted=allow_tainted)
139         for db_binary in upload.binaries:
140             transaction.copy_binary(db_binary, suite, binary_component_func(db_binary), allow_tainted=allow_tainted, extra_archives=[upload.target_suite.archive])
141
142     # Copy .changes if needed
143     if upload.target_suite.copychanges:
144         src = os.path.join(upload.policy_queue.path, upload.changes.changesname)
145         dst = os.path.join(upload.target_suite.path, upload.changes.changesname)
146         fs.copy(src, dst, mode=upload.target_suite.archive.mode)
147
148     if upload.source is not None and not Options['No-Action']:
149         urgency = upload.changes.urgency
150         if urgency not in cnf.value_list('Urgency::Valid'):
151             urgency = cnf['Urgency::Default']
152         UrgencyLog().log(upload.source.source, upload.source.version, urgency)
153
154     print "  ACCEPT"
155     if not Options['No-Action']:
156         Logger.log(["Policy Queue ACCEPT", srcqueue.queue_name, changesname])
157
158     # Send announcement
159     subst = subst_for_upload(upload)
160     announce = ", ".join(upload.target_suite.announce or [])
161     tracking = cnf.get('Dinstall::TrackingServer')
162     if tracking and upload.source is not None:
163         announce = '{0}\nBcc: {1}@{2}'.format(announce, upload.changes.source, tracking)
164     subst['__ANNOUNCE_LIST_ADDRESS__'] = announce
165     message = utils.TemplateSubst(subst, os.path.join(cnf['Dir::Templates'], 'process-unchecked.announce'))
166     utils.send_mail(message)
167
168     # TODO: code duplication. Similar code is in process-upload.
169     if cnf.find_b('Dinstall::CloseBugs') and upload.changes.closes is not None and upload.source is not None:
170         for bugnum in upload.changes.closes:
171             subst['__BUG_NUMBER__'] = bugnum
172             message = utils.TemplateSubst(subst, os.path.join(cnf['Dir::Templates'], 'process-unchecked.bug-close'))
173             utils.send_mail(message)
174
175             del subst['__BUG_NUMBER__']
176
177     # TODO: code duplication. Similar code is in process-upload.
178     # Move .changes to done
179     src = os.path.join(upload.policy_queue.path, upload.changes.changesname)
180     now = datetime.datetime.now()
181     donedir = os.path.join(cnf['Dir::Done'], now.strftime('%Y/%m/%d'))
182     dst = os.path.join(donedir, upload.changes.changesname)
183     dst = utils.find_next_free(dst)
184     fs.copy(src, dst, mode=0o644)
185
186     remove_upload(upload, transaction)
187
188 ################################################################################
189
190 @try_or_reject
191 def comment_reject(*args):
192     real_comment_reject(*args)
193
194 def real_comment_reject(upload, srcqueue, comments, transaction, notify=True):
195     cnf = Config()
196
197     fs = transaction.fs
198     session = transaction.session
199     changesname = upload.changes.changesname
200     queuedir = upload.policy_queue.path
201     rejectdir = cnf['Dir::Reject']
202
203     ### Copy files to reject/
204
205     poolfiles = [b.poolfile for b in upload.binaries]
206     if upload.source is not None:
207         poolfiles.extend([df.poolfile for df in upload.source.srcfiles])
208     # Not beautiful...
209     files = [ af.path for af in session.query(ArchiveFile) \
210                   .filter_by(archive=upload.policy_queue.suite.archive) \
211                   .join(ArchiveFile.file) \
212                   .filter(PoolFile.file_id.in_([ f.file_id for f in poolfiles ])) ]
213     for byhand in upload.byhand:
214         path = os.path.join(queuedir, byhand.filename)
215         if os.path.exists(path):
216             files.append(path)
217     files.append(os.path.join(queuedir, changesname))
218
219     for fn in files:
220         dst = utils.find_next_free(os.path.join(rejectdir, os.path.basename(fn)))
221         fs.copy(fn, dst, link=True)
222
223     ### Write reason
224
225     dst = utils.find_next_free(os.path.join(rejectdir, '{0}.reason'.format(changesname)))
226     fh = fs.create(dst)
227     fh.write(comments)
228     fh.close()
229
230     ### Send mail notification
231
232     if notify:
233         subst = subst_for_upload(upload)
234         subst['__MANUAL_REJECT_MESSAGE__'] = ''
235         subst['__REJECT_MESSAGE__'] = comments
236
237         # Try to use From: from comment file if there is one.
238         # This is not very elegant...
239         match = re.match(r"\AFrom: ([^\n]+)\n\n", comments)
240         if match:
241             subst['__REJECTOR_ADDRESS__'] = match.group(1)
242             subst['__REJECT_MESSAGE__'] = '\n'.join(comments.splitlines()[2:])
243
244         message = utils.TemplateSubst(subst, os.path.join(cnf['Dir::Templates'], 'queue.rejected'))
245         utils.send_mail(message)
246
247     print "  REJECT"
248     if not Options["No-Action"]:
249         Logger.log(["Policy Queue REJECT", srcqueue.queue_name, upload.changes.changesname])
250
251     remove_upload(upload, transaction)
252
253 ################################################################################
254
255 def remove_upload(upload, transaction):
256     fs = transaction.fs
257     session = transaction.session
258     changes = upload.changes
259
260     # Remove byhand and changes files. Binary and source packages will be
261     # removed from {bin,src}_associations and eventually removed by clean-suites automatically.
262     queuedir = upload.policy_queue.path
263     for byhand in upload.byhand:
264         path = os.path.join(queuedir, byhand.filename)
265         if os.path.exists(path):
266             fs.unlink(path)
267         session.delete(byhand)
268     fs.unlink(os.path.join(queuedir, upload.changes.changesname))
269
270     session.delete(upload)
271     session.delete(changes)
272     session.flush()
273
274 ################################################################################
275
276 def subst_for_upload(upload):
277     # TODO: similar code in process-upload
278     cnf = Config()
279
280     maintainer_field = upload.changes.changedby or upload.changes.maintainer
281     if upload.source is not None:
282         addresses = utils.mail_addresses_for_upload(upload.changes.maintainer, maintainer_field, upload.changes.fingerprint)
283     else:
284         addresses = utils.mail_addresses_for_upload(upload.changes.maintainer, upload.changes.maintainer, upload.changes.fingerprint)
285
286     changes_path = os.path.join(upload.policy_queue.path, upload.changes.changesname)
287     changes_contents = open(changes_path, 'r').read()
288
289     bcc = 'X-DAK: dak process-policy'
290     if 'Dinstall::Bcc' in cnf:
291         bcc = '{0}\nBcc: {1}'.format(bcc, cnf['Dinstall::Bcc'])
292
293     subst = {
294         '__DISTRO__': cnf['Dinstall::MyDistribution'],
295         '__ADMIN_ADDRESS__': cnf['Dinstall::MyAdminAddress'],
296
297         '__CHANGES_FILENAME__': upload.changes.changesname,
298         '__SOURCE__': upload.changes.source,
299         '__VERSION__': upload.changes.version,
300         '__ARCHITECTURE__': upload.changes.architecture,
301         '__MAINTAINER__': maintainer_field,
302         '__MAINTAINER_FROM__': fix_maintainer(maintainer_field)[1],
303         '__MAINTAINER_TO__': ", ".join(addresses),
304         '__CC__': 'X-DAK-Rejection: manual or automatic',
305         '__REJECTOR_ADDRESS__': cnf['Dinstall::MyEmailAddress'],
306         '__BCC__': bcc,
307         '__BUG_SERVER__': cnf.get('Dinstall::BugServer'),
308         '__FILE_CONTENTS__': changes_contents,
309     }
310
311     override_maintainer = cnf.get('Dinstall::OverrideMaintainer')
312     if override_maintainer:
313         subst['__MAINTAINER_TO__'] = override_maintainer
314
315     return subst
316
317 ################################################################################
318
319 def remove_unreferenced_binaries(policy_queue, transaction):
320     """Remove binaries that are no longer referenced by an upload
321
322     @type  policy_queue: L{daklib.dbconn.PolicyQueue}
323
324     @type  transaction: L{daklib.archive.ArchiveTransaction}
325     """
326     session = transaction.session
327     suite = policy_queue.suite
328
329     query = """
330        SELECT b.*
331          FROM binaries b
332          JOIN bin_associations ba ON b.id = ba.bin
333         WHERE ba.suite = :suite_id
334           AND NOT EXISTS (SELECT 1 FROM policy_queue_upload_binaries_map pqubm
335                                    JOIN policy_queue_upload pqu ON pqubm.policy_queue_upload_id = pqu.id
336                                   WHERE pqu.policy_queue_id = :policy_queue_id
337                                     AND pqubm.binary_id = b.id)"""
338     binaries = session.query(DBBinary).from_statement(query) \
339         .params({'suite_id': policy_queue.suite_id, 'policy_queue_id': policy_queue.policy_queue_id})
340
341     for binary in binaries:
342         Logger.log(["removed binary from policy queue", policy_queue.queue_name, binary.package, binary.version])
343         transaction.remove_binary(binary, suite)
344
345 def remove_unreferenced_sources(policy_queue, transaction):
346     """Remove sources that are no longer referenced by an upload or a binary
347
348     @type  policy_queue: L{daklib.dbconn.PolicyQueue}
349
350     @type  transaction: L{daklib.archive.ArchiveTransaction}
351     """
352     session = transaction.session
353     suite = policy_queue.suite
354
355     query = """
356        SELECT s.*
357          FROM source s
358          JOIN src_associations sa ON s.id = sa.source
359         WHERE sa.suite = :suite_id
360           AND NOT EXISTS (SELECT 1 FROM policy_queue_upload pqu
361                                   WHERE pqu.policy_queue_id = :policy_queue_id
362                                     AND pqu.source_id = s.id)
363           AND NOT EXISTS (SELECT 1 FROM binaries b
364                                    JOIN bin_associations ba ON b.id = ba.bin
365                                   WHERE b.source = s.id
366                                     AND ba.suite = :suite_id)"""
367     sources = session.query(DBSource).from_statement(query) \
368         .params({'suite_id': policy_queue.suite_id, 'policy_queue_id': policy_queue.policy_queue_id})
369
370     for source in sources:
371         Logger.log(["removed source from policy queue", policy_queue.queue_name, source.source, source.version])
372         transaction.remove_source(source, suite)
373
374 ################################################################################
375
376 def main():
377     global Options, Logger
378
379     cnf = Config()
380     session = DBConn().session()
381
382     Arguments = [('h',"help","Process-Policy::Options::Help"),
383                  ('n',"no-action","Process-Policy::Options::No-Action")]
384
385     for i in ["help", "no-action"]:
386         if not cnf.has_key("Process-Policy::Options::%s" % (i)):
387             cnf["Process-Policy::Options::%s" % (i)] = ""
388
389     queue_name = apt_pkg.parse_commandline(cnf.Cnf,Arguments,sys.argv)
390
391     if len(queue_name) != 1:
392         print "E: Specify exactly one policy queue"
393         sys.exit(1)
394
395     queue_name = queue_name[0]
396
397     Options = cnf.subtree("Process-Policy::Options")
398
399     if Options["Help"]:
400         usage()
401
402     Logger = daklog.Logger("process-policy")
403     if not Options["No-Action"]:
404         urgencylog = UrgencyLog()
405
406     with ArchiveTransaction() as transaction:
407         session = transaction.session
408         try:
409             pq = session.query(PolicyQueue).filter_by(queue_name=queue_name).one()
410         except NoResultFound:
411             print "E: Cannot find policy queue %s" % queue_name
412             sys.exit(1)
413
414         commentsdir = os.path.join(pq.path, 'COMMENTS')
415         # The comments stuff relies on being in the right directory
416         os.chdir(pq.path)
417
418         do_comments(commentsdir, pq, "ACCEPT.", "ACCEPTED.", "OK", comment_accept, transaction)
419         do_comments(commentsdir, pq, "ACCEPTED.", "ACCEPTED.", "OK", comment_accept, transaction)
420         do_comments(commentsdir, pq, "REJECT.", "REJECTED.", "NOTOK", comment_reject, transaction)
421
422         remove_unreferenced_binaries(pq, transaction)
423         remove_unreferenced_sources(pq, transaction)
424
425     if not Options['No-Action']:
426         urgencylog.close()
427
428 ################################################################################
429
430 if __name__ == '__main__':
431     main()